-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[python] Fix RecursionError when reading a shard or slice of a large file #10113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JingsongLi
merged 1 commit into
apache:master
from
jackylee-ch:py-shard-reader-recursion
Sep 24, 2026
+103
−4
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| import unittest | ||
|
|
||
| import pyarrow as pa | ||
|
|
||
| from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader | ||
| from pypaimon.read.reader.shard_batch_reader import ShardBatchReader | ||
|
|
||
|
|
||
| class _BatchReader(RecordBatchReader): | ||
| """A non-blob reader that replays an explicit list of arrow batches.""" | ||
|
|
||
| format_reader = None # not a FormatBlobReader -> ShardBatchReader takes the row-range path | ||
|
|
||
| def __init__(self, batches): | ||
| self._batches = iter(batches) | ||
|
|
||
| def read_arrow_batch(self): | ||
| return next(self._batches, None) | ||
|
|
||
| def close(self): | ||
| pass | ||
|
|
||
|
|
||
| def _single_row_batches(count): | ||
| return [pa.record_batch([pa.array([i])], names=["id"]) for i in range(count)] | ||
|
|
||
|
|
||
| def _read_all(reader): | ||
| got = [] | ||
| while True: | ||
| batch = reader.read_arrow_batch() | ||
| if batch is None: | ||
| break | ||
| got.extend(batch.column("id").to_pylist()) | ||
| return got | ||
|
|
||
|
|
||
| class ShardBatchReaderTest(unittest.TestCase): | ||
|
|
||
| def test_slice_deep_into_file_does_not_recurse(self): | ||
| # A slice/shard whose range sits many batches into a file must not recurse | ||
| # once per skipped batch. With the default parquet batch_size of 1024 rows a | ||
| # slice starting ~1M rows in skips >1000 batches; recursing there overflowed | ||
| # the stack with RecursionError. 2000 single-row batches reproduce that. | ||
| batch_count = 2000 | ||
| reader = ShardBatchReader( | ||
| _BatchReader(_single_row_batches(batch_count)), batch_count - 1, batch_count) | ||
|
|
||
| batch = reader.read_arrow_batch() | ||
|
|
||
| self.assertIsNotNone(batch) | ||
| self.assertEqual(batch.column("id").to_pylist(), [batch_count - 1]) | ||
| self.assertIsNone(reader.read_arrow_batch()) | ||
|
|
||
| def test_slice_returns_only_rows_in_range(self): | ||
| # Semantics guard: with single-row batches, slice [2, 5) yields rows 2, 3, 4 | ||
| # and nothing else, so the loop refactor preserves the range filtering. | ||
| reader = ShardBatchReader(_BatchReader(_single_row_batches(8)), 2, 5) | ||
|
|
||
| self.assertEqual(_read_all(reader), [2, 3, 4]) | ||
|
|
||
| def test_slice_straddling_batch_boundaries(self): | ||
| # Multi-row batches so the two slice() branches are exercised: the first | ||
| # batch straddles start_pos (2 in [0,4)) and the last straddles end_pos | ||
| # (9 in [8,12)); slice [2, 9) must yield exactly rows 2..8. | ||
| batches = [ | ||
| pa.record_batch([pa.array([0, 1, 2, 3])], names=["id"]), | ||
| pa.record_batch([pa.array([4, 5, 6, 7])], names=["id"]), | ||
| pa.record_batch([pa.array([8, 9, 10, 11])], names=["id"]), | ||
| ] | ||
| reader = ShardBatchReader(_BatchReader(batches), 2, 9) | ||
|
|
||
| self.assertEqual(_read_all(reader), [2, 3, 4, 5, 6, 7, 8]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop removes the recursion failure, but it still drains every batch after
end_pos. Callers such asTableRead.to_arrow()read until this method returnsNone, so for a[0, 1)slice of a 2,000-batch file, the first call returns row 0 and the second call reads the remaining 1,999 batches plus EOF. This means a head slice still scans the entire file, and an error in data outside the selected range is surfaced even though that data should never be read. Please returnNonebefore calling the underlying reader oncecurrent_pos >= end_pos(or track an exhausted flag). Add a counting/raising reader test for a large[0, 1)slice and assert that the second call returnsNonewithout another underlying read.Reproduction: a counting reader with 2,000 one-row batches required 1 call to return
[0], then reached 2,001 total calls before the patched reader returnedNone. A reader configured to throw on its second batch returned the selected first row and then incorrectly propagatedOSError: tail should not be read.