This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a10cf23b8 [python] Speed up LeRobot worker startup with lazy fallback 
splits (#10732)
5a10cf23b8 is described below

commit 5a10cf23b8a0d2a7146a153172fbf3a4b59e7609
Author: XiaoHongbo <[email protected]>
AuthorDate: Thu Oct 22 09:06:40 2026 +0800

    [python] Speed up LeRobot worker startup with lazy fallback splits (#10732)
---
 .../pypaimon/multimodal/lerobot/reader.py          | 16 +++-
 .../pypaimon/tests/lerobot_native_reader_test.py   | 88 ++++++++++++++++++++++
 2 files changed, 101 insertions(+), 3 deletions(-)

diff --git a/paimon-python/pypaimon/multimodal/lerobot/reader.py 
b/paimon-python/pypaimon/multimodal/lerobot/reader.py
index fe53da6e46..f85269095d 100644
--- a/paimon-python/pypaimon/multimodal/lerobot/reader.py
+++ b/paimon-python/pypaimon/multimodal/lerobot/reader.py
@@ -42,7 +42,8 @@ class _PaimonTableFrameReader:
         self._table = self._table._copy_with_snapshot(snapshot)
         self.num_rows = snapshot.next_row_id
         self.file_io = self._table.file_io
-        self._locator = _FrameLocator(self._table, snapshot, splits)
+        self._locator = _FrameLocator(
+            self._table, snapshot, splits, projection=columns)
 
     def read_indices(self, indices, columns):
         builder = self._table.new_read_builder().with_projection(columns)
@@ -74,9 +75,10 @@ class _PaimonTableFrameReader:
 class _FrameLocator:
     """Locate LeRobot frame rows in one fixed Paimon snapshot."""
 
-    def __init__(self, table, snapshot, splits):
+    def __init__(self, table, snapshot, splits, *, projection):
         self._table = table
         self._snapshot = snapshot
+        self._projection = list(projection)
         self._scanner = None
         self._scanner_initialized = False
         self._process_id = os.getpid()
@@ -143,7 +145,11 @@ class _FrameLocator:
             return
         self._scanner = None
         self._scanner_initialized = False
-        self._set_splits(self._splits)
+        splits = self._splits
+        if splits is None:
+            splits = self._table.new_read_builder().with_projection(
+                self._projection).new_scan().plan().splits()
+        self._set_splits(splits)
         self._process_id = process_id
 
     def _index_scanner(self, predicate):
@@ -171,6 +177,10 @@ class _FrameLocator:
         state["_process_id"] = None
         state["_split_ranges"] = None
         state["_split_range_index"] = None
+        if (self._table.options.native_read_enabled()
+                or self._table.options.native_plan_enabled()):
+            # Native reads do not need fallback splits in the worker payload.
+            state["_splits"] = None
         return state
 
     def __del__(self):
diff --git a/paimon-python/pypaimon/tests/lerobot_native_reader_test.py 
b/paimon-python/pypaimon/tests/lerobot_native_reader_test.py
index f5940360cb..c8f7a3ad17 100644
--- a/paimon-python/pypaimon/tests/lerobot_native_reader_test.py
+++ b/paimon-python/pypaimon/tests/lerobot_native_reader_test.py
@@ -256,3 +256,91 @@ def 
test_native_lookup_preserves_query_authorization(frames, denied):
             assert calls
     finally:
         current.close()
+
+
[email protected]("option", ["scan.native-plan.enabled", 
"read.native.enabled"])
+def test_pickled_native_locator_rebuilds_pinned_fallback_once(frames, option):
+    from pypaimon.globalindex import DataEvolutionGlobalIndexScanner
+    frames.add(pa.table({"index": [40], "value": [4]}))
+    current = reader(frames, **{
+        "scan.native-plan.enabled": "false", "read.native.enabled": "false",
+        option: "true"})
+    restored = pickle.loads(pickle.dumps(current))
+    assert restored._locator._splits is None
+    frames.add(pa.table({"index": [60], "value": [6]}))
+    try:
+        with patch("pypaimon.execution.native.read.plan.native_plan",
+                   side_effect=RuntimeError("native unavailable")), \
+                patch.object(DataEvolutionGlobalIndexScanner, "create",
+                             wraps=DataEvolutionGlobalIndexScanner.create) as 
create:
+            for unused in range(3):
+                result = restored.read_indices([10, 40, 60], ["value", 
"index"])
+                assert result.column_names == ["value", "index"]
+                assert sorted(result["index"].to_pylist()) == [10, 40]
+                assert sorted(result["value"].to_pylist()) == [1, 4]
+            assert create.call_count == 1
+            assert restored._locator._splits is not None
+    finally:
+        current.close()
+        restored.close()
+
+
+def test_pickled_native_locator_rebuilds_original_projection(tmp_path):
+    from pypaimon.catalog.table_query_auth import TableQueryAuthResult
+    from pypaimon.read.table_scan import TableScan
+
+    conn = pm.connect(options={"warehouse": str(tmp_path)})
+    table = conn.create_table("projected_frames", data=pa.table({
+        "index": [10, 20, 30],
+        "value": [1, 2, 3],
+        "secret": [4, 5, 6],
+    }), options={
+        "row-tracking.enabled": "true",
+        "data-evolution.enabled": "true",
+        "global-index.enabled": "true",
+        "bucket": "-1",
+    })
+    table.raw_table.create_global_index("index", index_type="btree")
+    selected = []
+
+    def authorize(columns):
+        selected.append(columns)
+        if "secret" in columns:
+            raise PermissionError("secret column denied")
+        return TableQueryAuthResult(None, None)
+
+    original_init = TableScan.__init__
+
+    def init_with_auth(scan, *args, **kwargs):
+        original_init(scan, *args, **kwargs)
+        scan._query_auth_fn = authorize
+
+    with patch.object(TableScan, "__init__", init_with_auth):
+        current = reader(table, **{
+            "scan.native-plan.enabled": "false",
+            "read.native.enabled": "true",
+        })
+    restored = pickle.loads(pickle.dumps(current))
+    try:
+        assert restored._locator._splits is None
+        with patch.object(TableScan, "__init__", init_with_auth), \
+                
patch("pypaimon.execution.native.runtime.native_runtime_available",
+                      return_value=False):
+            result = restored.read_indices([20], ["value"])
+        assert result["value"].to_pylist() == [2]
+        assert selected == [["index", "value"], ["index", "value"]]
+    finally:
+        current.close()
+        restored.close()
+
+
+def test_pickled_python_locator_keeps_fallback_splits(frames):
+    current = reader(frames, **{
+        "scan.native-plan.enabled": "false", "read.native.enabled": "false"})
+    restored = pickle.loads(pickle.dumps(current))
+    try:
+        assert restored._locator._splits is not None
+        assert restored.read_indices([20], ["value"])["value"].to_pylist() == 
[2]
+    finally:
+        current.close()
+        restored.close()

Reply via email to