Uh oh!
There was an error while loading. Please reload this page.
[SPARK-55416][SS][PYTHON] Streaming Python Data Source memory leak when end-offset is not updated - #54237
[SPARK-55416][SS][PYTHON] Streaming Python Data Source memory leak when end-offset is not updated#54237vinodkc wants to merge 7 commits into
Conversation
HeartSaVioR
left a comment
There was a problem hiding this comment.
Looks good in overall, only minor and nits. Nice finding!
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
HeartSaVioR
commented
Feb 13, 2026
Thanks! Merging to master/4.1/4.0. |
I could only merge to master. @vinodkc There is merge conflict in 4.1/4.0. Could you please submit a PR for these branches? It'd probably good to have it in all available version line branches since it's a memory leak issue. |
vinodkc
commented
Feb 17, 2026
@HeartSaVioR , Thanks for the details. I've raised two PRs for branch 4.1 and 4.0. |
…en end-offset is not updated
### What changes were proposed in this pull request?
In `_SimpleStreamReaderWrapper.latestOffset()`, validate that custom implementation of datasource based on `SimpleDataSourceStreamReader.read()` does not return a non-empty batch with end == start. If it does, raise PySparkException with error class `SIMPLE_STREAM_READER_OFFSET_DID_NOT_ADVANCE` before appending to the cache. Empty batches with end == start remain allowed.
### Why are the changes needed?
When a user implements read(start) incorrectly and returns:
- Same offset for both: end = start (e.g. both {"offset": 0}).
- Non-empty iterator: e.g. 2 rows.
If a reader returns end == start with data (e.g. return (it, {"offset": start_idx})), the wrapper keeps appending to its prefetch cache on every trigger while commit(end) never trims entries (first matching index is 0). The cache grows without bound and driver (non-JVM) memory increases until OOM. Validating and raising error before appending stops this and fails fast with a clear error.
Empty batches with end == start remain allowed , it will allow the Python data source to represent that there is no data to read.
### Does this PR introduce _any_ user-facing change?
Yes. Implementations that return end == start with a non-empty iterator now get PySparkException instead of unbounded memory growth. Empty batches with end == start are unchanged.
### How was this patch tested?
Added unit test in `test_python_streaming_datasource.py`
### Was this patch authored or co-authored using generative AI tooling?
No.
Closesapache#54237 from vinodkc/br_SPARK-55416.
Authored-by: vinodkc <vinod.kc.in@gmail.com>
Signed-off-by: Jungtaek Lim <kabhwan.opensource@gmail.com>…ak when end-offset is not updated ### What changes were proposed in this pull request? Backport #54237 to branch-4.1 In `_SimpleStreamReaderWrapper.latestOffset()`, validate that custom implementation of datasource based on `SimpleDataSourceStreamReader.read()` does not return a non-empty batch with end == start. If it does, raise PySparkException with error class `SIMPLE_STREAM_READER_OFFSET_DID_NOT_ADVANCE` before appending to the cache. Empty batches with end == start remain allowed. ### Why are the changes needed? When a user implements read(start) incorrectly and returns: - Same offset for both: end = start (e.g. both {"offset": 0}). - Non-empty iterator: e.g. 2 rows. If a reader returns end == start with data (e.g. return (it, {"offset": start_idx})), the wrapper keeps appending to its prefetch cache on every trigger while commit(end) never trims entries (first matching index is 0). The cache grows without bound and driver (non-JVM) memory increases until OOM. Validating and raising error before appending stops this and fails fast with a clear error. Empty batches with end == start remain allowed , it will allow the Python data source to represent that there is no data to read. ### Does this PR introduce _any_ user-facing change? Yes. Implementations that return end == start with a non-empty iterator now get PySparkException instead of unbounded memory growth. Empty batches with end == start are unchanged. ### How was this patch tested? Added unit test in `test_python_streaming_datasource.py` ### Was this patch authored or co-authored using generative AI tooling? No. Closes#54321 from vinodkc/br_SPARK-55416_4.1. Authored-by: vinodkc <vinod.kc.in@gmail.com> Signed-off-by: Jungtaek Lim <kabhwan.opensource@gmail.com>
…ak when end-offset is not updated ### What changes were proposed in this pull request? Backport #54237 to branch-4.0 In `_SimpleStreamReaderWrapper.latestOffset()`, validate that custom implementation of datasource based on `SimpleDataSourceStreamReader.read()` does not return a non-empty batch with end == start. If it does, raise PySparkException with error class `SIMPLE_STREAM_READER_OFFSET_DID_NOT_ADVANCE` before appending to the cache. Empty batches with end == start remain allowed. ### Why are the changes needed? When a user implements read(start) incorrectly and returns: - Same offset for both: end = start (e.g. both {"offset": 0}). - Non-empty iterator: e.g. 2 rows. If a reader returns end == start with data (e.g. return (it, {"offset": start_idx})), the wrapper keeps appending to its prefetch cache on every trigger while commit(end) never trims entries (first matching index is 0). The cache grows without bound and driver (non-JVM) memory increases until OOM. Validating and raising error before appending stops this and fails fast with a clear error. Empty batches with end == start remain allowed , it will allow the Python data source to represent that there is no data to read. ### Does this PR introduce _any_ user-facing change? Yes. Implementations that return end == start with a non-empty iterator now get PySparkException instead of unbounded memory growth. Empty batches with end == start are unchanged. ### How was this patch tested? Added unit test in `test_python_streaming_datasource.py` ### Was this patch authored or co-authored using generative AI tooling? No. Closes#54317 from vinodkc/br_SPARK-55416_4.0. Authored-by: vinodkc <vinod.kc.in@gmail.com> Signed-off-by: Jungtaek Lim <kabhwan.opensource@gmail.com>
What changes were proposed in this pull request?
In
_SimpleStreamReaderWrapper.latestOffset(), validate that custom implementation of datasource based onSimpleDataSourceStreamReader.read()does not return a non-empty batch with end == start. If it does, raise PySparkException with error classSIMPLE_STREAM_READER_OFFSET_DID_NOT_ADVANCEbefore appending to the cache. Empty batches with end == start remain allowed.Why are the changes needed?
When a user implements read(start) incorrectly and returns:
If a reader returns end == start with data (e.g. return (it, {"offset": start_idx})), the wrapper keeps appending to its prefetch cache on every trigger while commit(end) never trims entries (first matching index is 0). The cache grows without bound and driver (non-JVM) memory increases until OOM. Validating and raising error before appending stops this and fails fast with a clear error.
Empty batches with end == start remain allowed , it will allow the Python data source to represent that there is no data to read.
Does this PR introduce any user-facing change?
Yes. Implementations that return end == start with a non-empty iterator now get PySparkException instead of unbounded memory growth. Empty batches with end == start are unchanged.
How was this patch tested?
Added unit test in
test_python_streaming_datasource.pyWas this patch authored or co-authored using generative AI tooling?
No.