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 9301269c15 [python] Avoid repeated catalog setup in parallel native
reads (#10118)
9301269c15 is described below
commit 9301269c15b59600881c78fd643d8209dbc97f59
Author: XiaoHongbo <[email protected]>
AuthorDate: Tue Sep 22 22:27:41 2026 +0800
[python] Avoid repeated catalog setup in parallel native reads (#10118)
---
paimon-python/pypaimon/read/native_plan.py | 30 +++--
paimon-python/pypaimon/read/table_read.py | 32 ++---
.../pypaimon/tests/native_plan_integration_test.py | 24 ++--
paimon-python/pypaimon/tests/native_read_test.py | 144 ++++++++++++++-------
4 files changed, 147 insertions(+), 83 deletions(-)
diff --git a/paimon-python/pypaimon/read/native_plan.py
b/paimon-python/pypaimon/read/native_plan.py
index e457bccb28..6eac710aaa 100644
--- a/paimon-python/pypaimon/read/native_plan.py
+++ b/paimon-python/pypaimon/read/native_plan.py
@@ -340,13 +340,13 @@ def _configure_native_read_builder(builder, predicate,
limit, projection,
return builder
-def native_read(table, splits, predicate: Optional[Predicate] = None,
- limit: Optional[int] = None,
- projection: Optional[List[str]] = None,
- blob_parallelism: Optional[int] = None,
- nested_projection: Optional[List[List[str]]] = None,
- include_row_kind: bool = False):
- """Read Rust ``Split`` objects into PyArrow ``RecordBatch`` objects."""
+def _prepare_native_read(table, predicate: Optional[Predicate] = None,
+ limit: Optional[int] = None,
+ projection: Optional[List[str]] = None,
+ blob_parallelism: Optional[int] = None,
+ nested_projection: Optional[List[List[str]]] = None,
+ include_row_kind: bool = False):
+ """Create one Rust reader reusable across split groups."""
if not native_reader_available():
raise RuntimeError(
"read.native.enabled needs the pypaimon-rust native reader API")
@@ -358,8 +358,20 @@ def native_read(table, splits, predicate:
Optional[Predicate] = None,
builder = builder.with_blob_parallelism(blob_parallelism)
reader = builder.new_read()
read_arrow = getattr(reader, 'read_arrow', None)
- return (read_arrow(splits) if callable(read_arrow)
- else reader.read(splits))
+ return read_arrow if callable(read_arrow) else reader.read
+
+
+def native_read(table, splits, predicate: Optional[Predicate] = None,
+ limit: Optional[int] = None,
+ projection: Optional[List[str]] = None,
+ blob_parallelism: Optional[int] = None,
+ nested_projection: Optional[List[List[str]]] = None,
+ include_row_kind: bool = False):
+ """Read Rust ``Split`` objects into PyArrow ``RecordBatch`` objects."""
+ read_splits = _prepare_native_read(
+ table, predicate, limit, projection, blob_parallelism,
+ nested_projection, include_row_kind)
+ return read_splits(splits)
def native_plan(
diff --git a/paimon-python/pypaimon/read/table_read.py
b/paimon-python/pypaimon/read/table_read.py
index fb2926a467..eb5e64752d 100644
--- a/paimon-python/pypaimon/read/table_read.py
+++ b/paimon-python/pypaimon/read/table_read.py
@@ -441,7 +441,7 @@ class TableRead:
return None
try:
from pypaimon.read.native_plan import (
- native_read, native_split_from_python)
+ _prepare_native_read, native_read, native_split_from_python)
except Exception as e:
logger.warning(
"Native read failed, falling back to the Python reader: %s", e)
@@ -464,15 +464,20 @@ class TableRead:
split_weights.append(self._native_split_weight(split))
if (parallelism is not None
and self._should_run_parallel(splits, parallelism)):
+ read_kwargs = self._native_read_kwargs(blob_parallelism)
+ try:
+ read_splits = _prepare_native_read(self.table, **read_kwargs)
+ except Exception as e:
+ logger.warning(
+ "Native read failed, falling back to the Python reader:
%s", e)
+ return None
if streaming:
groups = self._native_split_groups(
rust_splits, parallelism, split_weights)
- read_kwargs = self._native_read_kwargs(blob_parallelism)
readers = []
try:
for group in groups:
- readers.append(
- native_read(self.table, group, **read_kwargs))
+ readers.append(read_splits(group))
except Exception as e:
for reader in readers:
close = getattr(reader, 'close', None)
@@ -492,8 +497,8 @@ class TableRead:
return self._convert_native_batches(batches, schema)
try:
return self._native_batches_parallel(
- native_read, rust_splits, schema, parallelism,
- blob_parallelism, split_weights)
+ read_splits, rust_splits, schema, parallelism,
+ split_weights)
except _NativeReadSetupError as e:
logger.warning(
"Native read failed, falling back to the Python reader:
%s", e)
@@ -582,9 +587,9 @@ class TableRead:
return groups
def _native_batches_parallel(
- self, native_read, rust_splits, schema, effective,
- blob_parallelism, split_weights=None):
- """Read contiguous split groups with independent Rust readers."""
+ self, read_splits, rust_splits, schema, effective,
+ split_weights=None):
+ """Read contiguous split groups with independent Rust streams."""
groups = self._native_split_groups(
rust_splits, effective, split_weights)
workers = len(groups)
@@ -597,11 +602,10 @@ class TableRead:
futures = {
executor.submit(
self._read_native_split_group,
- native_read,
+ read_splits,
group,
schema,
remaining_state,
- blob_parallelism,
): index
for index, group in enumerate(groups)
}
@@ -700,13 +704,11 @@ class TableRead:
executor.shutdown(wait=True)
def _read_native_split_group(
- self, native_read, rust_splits, schema, remaining_state,
- blob_parallelism):
+ self, read_splits, rust_splits, schema, remaining_state):
if remaining_state.exhausted():
return []
try:
- read_kwargs = self._native_read_kwargs(blob_parallelism)
- batches = native_read(self.table, rust_splits, **read_kwargs)
+ batches = read_splits(rust_splits)
except Exception as e:
raise _NativeReadSetupError(str(e)) from e
result = []
diff --git a/paimon-python/pypaimon/tests/native_plan_integration_test.py
b/paimon-python/pypaimon/tests/native_plan_integration_test.py
index 5fb207ea4d..646ea5eb68 100644
--- a/paimon-python/pypaimon/tests/native_plan_integration_test.py
+++ b/paimon-python/pypaimon/tests/native_plan_integration_test.py
@@ -32,9 +32,9 @@ from pypaimon import CatalogFactory, Schema
from pypaimon.catalog.table_query_auth import TableQueryAuthResult
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.read.native_plan import (
- native_family_search_modes_available, native_method_available, native_read,
- native_reader_available, native_split_bridge_available,
- native_split_from_python,
+ _prepare_native_read, native_family_search_modes_available,
+ native_method_available, native_read, native_reader_available,
+ native_split_bridge_available, native_split_from_python,
)
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.table.row.blob import BlobDescriptor, BlobViewStruct
@@ -373,8 +373,8 @@ class NativePlanIntegrationTest(unittest.TestCase):
plan = builder.new_scan().plan()
self.assertEqual(len(plan.splits()), 4)
- with patch('pypaimon.read.native_plan.native_read',
- wraps=native_read) as rust_reads, \
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ wraps=_prepare_native_read) as prepare, \
patch(
'pypaimon.read.table_read.TableRead._create_split_read',
side_effect=AssertionError('Python reader was used')):
@@ -386,10 +386,10 @@ class NativePlanIntegrationTest(unittest.TestCase):
{'k': 3, 'v': 'c', 'dt': 'p3'},
{'k': 4, 'v': 'd', 'dt': 'p4'},
])
- self.assertEqual(rust_reads.call_count, 2)
+ prepare.assert_called_once()
- with patch('pypaimon.read.native_plan.native_read',
- wraps=native_read) as rust_reads, \
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ wraps=_prepare_native_read) as prepare, \
patch(
'pypaimon.read.table_read.TableRead._create_split_read',
side_effect=AssertionError('Python reader was used')):
@@ -402,7 +402,7 @@ class NativePlanIntegrationTest(unittest.TestCase):
{'k': 3, 'v': 'c', 'dt': 'p3'},
{'k': 4, 'v': 'd', 'dt': 'p4'},
])
- self.assertEqual(rust_reads.call_count, 2)
+ prepare.assert_called_once()
@unittest.skipUnless(native_reader_available(),
"pypaimon-rust native reader API not installed")
@@ -1527,10 +1527,10 @@ class NativePlanIntegrationTest(unittest.TestCase):
})
builder = native_table.new_read_builder()
plan = builder.new_scan().plan()
- with patch('pypaimon.read.native_plan.native_read',
- wraps=native_read) as read:
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ wraps=_prepare_native_read) as prepare:
rows = builder.new_read().to_arrow(plan.splits()).to_pylist()
- self.assertEqual(read.call_count, len(plan.splits()))
+ prepare.assert_called_once()
self.assertEqual(sorted(rows, key=lambda row: row['k']), [
{'k': 1, 'p': 'a/b'},
{'k': 2, 'p': 'a/b'},
diff --git a/paimon-python/pypaimon/tests/native_read_test.py
b/paimon-python/pypaimon/tests/native_read_test.py
index 63e49ed50b..8aa85566f4 100644
--- a/paimon-python/pypaimon/tests/native_read_test.py
+++ b/paimon-python/pypaimon/tests/native_read_test.py
@@ -223,7 +223,11 @@ def
test_native_read_uses_effective_parallelism_from_table_option():
worker_names = set()
worker_names_lock = threading.Lock()
- def read_group(table, rust_splits, **kwargs):
+ groups = []
+
+ def read_group(rust_splits):
+ groups.append(rust_splits)
+
def batches():
with worker_names_lock:
worker_names.add(threading.current_thread().name)
@@ -231,17 +235,41 @@ def
test_native_read_uses_effective_parallelism_from_table_option():
yield _id_batch(rust_splits)
return batches()
- with patch('pypaimon.read.native_plan.native_read',
- side_effect=read_group) as native:
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group) as prepare:
result = read.to_arrow(splits)
assert result.to_pydict() == {'id': [0, 1, 2, 3]}
- assert native.call_count == 2
- groups = sorted(call.args[1] for call in native.call_args_list)
- assert groups == [[0, 1], [2, 3]]
+ prepare.assert_called_once()
+ assert sorted(groups) == [[0, 1], [2, 3]]
assert len(worker_names) == 2
+def test_parallel_native_read_prepares_rust_reader_once():
+ read = _table_read()
+ splits = [_Split() for _ in range(4)]
+ for index, split in enumerate(splits):
+ split._native_split = index
+
+ groups = []
+
+ def read_group(rust_splits):
+ groups.append(rust_splits)
+ return [_id_batch(rust_splits)]
+
+ with patch(
+ 'pypaimon.read.native_plan._prepare_native_read',
+ create=True,
+ return_value=read_group) as prepare, patch(
+ 'pypaimon.read.native_plan.native_read',
+ side_effect=AssertionError('rebuilt native reader')):
+ result = read.to_arrow(splits, parallelism=2)
+
+ assert result.to_pydict() == {'id': [0, 1, 2, 3]}
+ prepare.assert_called_once()
+ assert sorted(groups) == [[0, 1], [2, 3]]
+
+
def test_native_batch_reader_uses_effective_parallelism():
read = _table_read()
read._read_parallelism = 2
@@ -253,7 +281,11 @@ def test_native_batch_reader_uses_effective_parallelism():
worker_names = set()
worker_names_lock = threading.Lock()
- def read_group(table, rust_splits, **kwargs):
+ groups = []
+
+ def read_group(rust_splits):
+ groups.append(rust_splits)
+
def batches():
with worker_names_lock:
worker_names.add(threading.current_thread().name)
@@ -261,14 +293,13 @@ def test_native_batch_reader_uses_effective_parallelism():
yield _id_batch(rust_splits)
return batches()
- with patch('pypaimon.read.native_plan.native_read',
- side_effect=read_group) as native:
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group) as prepare:
result = read.to_arrow_batch_reader(splits).read_all()
assert sorted(result.column('id').to_pylist()) == [0, 1, 2, 3]
- assert native.call_count == 2
- assert sorted(call.args[1] for call in native.call_args_list) == [
- [0, 1], [2, 3]]
+ prepare.assert_called_once()
+ assert sorted(groups) == [[0, 1], [2, 3]]
assert len(worker_names) == 2
@@ -316,17 +347,22 @@ def
test_native_batch_reader_caps_blob_parallelism_across_rust_readers():
for index, split in enumerate(splits):
split._native_split = index
+ groups = []
+
+ def read_group(group):
+ groups.append(group)
+ return [_id_batch(group)]
+
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]) as native:
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group) as prepare:
result = read.to_arrow_batch_reader(
splits, blob_parallelism=16, parallelism=16).read_all()
assert result.num_rows == 16
- assert native.call_count == 16
- assert {call.kwargs['blob_parallelism']
- for call in native.call_args_list} == {4}
+ prepare.assert_called_once()
+ assert prepare.call_args.kwargs['blob_parallelism'] == 4
+ assert len(groups) == 16
def test_parallel_native_stream_bounds_prefetch_per_reader():
@@ -541,15 +577,19 @@ def test_native_read_groups_splits_by_file_bytes():
for index, split in enumerate(splits):
split._native_split = index
+ groups = []
+
+ def read_group(group):
+ groups.append(group)
+ return [_id_batch(group)]
+
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]) as native:
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group):
result = read.to_arrow(splits, parallelism=2)
assert result.num_rows == 4
- assert sorted(call.args[1] for call in native.call_args_list) == [
- [0], [1, 2, 3]]
+ assert sorted(groups) == [[0], [1, 2, 3]]
def test_native_read_runtime_parallelism_overrides_table_option():
@@ -559,14 +599,20 @@ def
test_native_read_runtime_parallelism_overrides_table_option():
for index, split in enumerate(splits):
split._native_split = index
+ groups = []
+
+ def read_group(group):
+ groups.append(group)
+ return [_id_batch(group)]
+
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]) as native:
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group) as prepare:
result = read.to_arrow(splits, parallelism=2)
assert result.num_rows == 4
- assert native.call_count == 2
+ prepare.assert_called_once()
+ assert len(groups) == 2
def test_native_read_caps_blob_parallelism_across_split_readers():
@@ -575,17 +621,22 @@ def
test_native_read_caps_blob_parallelism_across_split_readers():
for index, split in enumerate(splits):
split._native_split = index
+ groups = []
+
+ def read_group(group):
+ groups.append(group)
+ return [_id_batch(group)]
+
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]) as native:
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_group) as prepare:
result = read.to_arrow(
splits, parallelism=16, blob_parallelism=16)
assert result.num_rows == 16
- assert native.call_count == 16
- assert {call.kwargs['blob_parallelism']
- for call in native.call_args_list} == {4}
+ prepare.assert_called_once()
+ assert prepare.call_args.kwargs['blob_parallelism'] == 4
+ assert len(groups) == 16
def test_parallel_native_read_shares_limit_across_readers():
@@ -596,9 +647,8 @@ def test_parallel_native_read_shares_limit_across_readers():
split._native_split = index
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]):
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=lambda group: [_id_batch(group)]):
result = read.to_arrow(splits)
assert result.num_rows == 3
@@ -612,9 +662,8 @@ def
test_parallel_native_batch_reader_shares_limit_across_readers():
split._native_split = index
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [
- _id_batch(group)]):
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=lambda group: [_id_batch(group)]):
result = read.to_arrow_batch_reader(splits).read_all()
assert result.num_rows == 3
@@ -627,7 +676,7 @@ def
test_parallel_native_reader_setup_failure_falls_back(streaming):
for split in splits:
split._native_split = object()
- with patch('pypaimon.read.native_plan.native_read',
+ with patch('pypaimon.read.native_plan._prepare_native_read',
side_effect=RuntimeError('setup failed')):
assert read._try_native_batches(
splits,
@@ -644,9 +693,10 @@ def
test_parallel_native_stream_setup_failure_closes_started_readers():
split._native_split = object()
started = Mock()
+ read_splits = Mock(side_effect=[started, RuntimeError('setup failed')])
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=[started, RuntimeError('setup failed')]):
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=read_splits):
assert read._try_native_batches(
splits,
pa.schema([('id', pa.int32())]),
@@ -663,14 +713,14 @@ def test_parallel_native_stream_error_propagates():
for split in splits:
split._native_split = object()
- def broken_stream(table, group, **kwargs):
+ def broken_stream(group):
def batches():
raise RuntimeError('stream failed')
yield
return batches()
- with patch('pypaimon.read.native_plan.native_read',
- side_effect=broken_stream), \
+ with patch('pypaimon.read.native_plan._prepare_native_read',
+ return_value=broken_stream), \
pytest.raises(RuntimeError, match='stream failed'):
read._try_native_batches(
splits, pa.schema([('id', pa.int32())]), parallelism=2)
@@ -777,8 +827,8 @@ def
test_native_read_preserves_parallel_large_binary_blob_schema():
split._native_split = index
with patch(
- 'pypaimon.read.native_plan.native_read',
- side_effect=lambda table, group, **kwargs: [pa.record_batch(
+ 'pypaimon.read.native_plan._prepare_native_read',
+ return_value=lambda group: [pa.record_batch(
[pa.array([bytes(group)], type=pa.large_binary())],
names=['payload'])]):
result = read.to_arrow(splits, parallelism=2)