Skip to content

[Bug]: Unpersist cached Datasets after evaluation in Spark Structured Streaming runner - #40265

Open
akashchamp wants to merge 2 commits into
apache:masterfrom
akashchamp:spark-ss-unpersist-cached-datasets-40243
Open

akashchamp wants to merge 2 commits into
apache:masterfrom
akashchamp:spark-ss-unpersist-cached-datasets-40243

Conversation

@akashchamp

Copy link
Copy Markdown

Fixes #40243

Problem

In the Spark Structured Streaming runner, ParDoTranslatorBatch explicitly
persist()s the intermediate, wide-row Dataset it builds for a multi-output
ParDo (so the per-mapPartitions transformation backing every tagged output
isn't re-evaluated once per tag). The code even carried a FIXME acknowledging
the gap:

// FIXME What's the strategy to unpersist Datasets / RDDs?

No corresponding unpersist() was ever called, so every multi-output ParDo
in a pipeline leaves its cached Dataset in Spark's storage memory for the
lifetime of the SparkSession. On a large batch pipeline with several such
transforms this accumulates without bound and leads to storage-memory
exhaustion, disk spilling, and OOMs.

Fix

  • Added PipelineTranslator.TranslationState#cacheDataset(Dataset, StorageLevel),
    a small helper translators use instead of calling Dataset#persist directly
    when they need to cache a dataset that isn't going through the normal
    putDataset path. It persists the dataset and registers it for later
    cleanup.
  • PipelineTranslator.TranslatingVisitor now tracks every dataset cached this
    way and threads that collection through to EvaluationContext.
  • EvaluationContext#evaluate() now unpersists all such datasets in a
    finally block once evaluation of the leaves has finished — whether it
    completed normally, was stopped early, or an evaluation failed with an
    exception — so a cached dataset never outlives the pipeline run that created
    it.
  • ParDoTranslatorBatch now calls cxt.cacheDataset(...) instead of calling
    persist() directly on the wide-row Dataset, removing the FIXME.

Testing

  • EvaluationContextTest: added two new tests asserting that datasets
    registered via the new cache-tracking collection are unpersist()-ed once
    evaluate() finishes, both on the happy path and when evaluating a leaf
    throws.
  • ParDoTest: updated the two existing tests that assert a multi-output
    ParDo produces a cached RDD/Dataset (testPardoWithOutputTagsCachedRDD /
    ...CachedDataset, renamed to ...UnpersistsRDDAfterRun /
    ...UnpersistsDatasetAfterRun) so that, after the pipeline run completes,
    they assert the opposite: SESSION.hasCachedData() is now false, i.e. the
    dataset was unpersisted rather than left cached.
  • Reproduced the leak first: ran the two updated ParDoTest regression tests
    (with their new "unpersisted after run" assertions) against the unfixed
    production code — both fail with
    AssertionError: Expected cached data to be unpersisted after run,
    confirming SESSION.hasCachedData() is still true after the pipeline run
    completes, i.e. the Dataset really is left cached.
  • Re-applied the fix and ran the full EvaluationContextTest and ParDoTest
    suites via ./gradlew :runners:spark:3:test --tests ...EvaluationContextTest --tests ...batch.ParDoTest: 14 tests, 14 passed,
    0 failed. I did not run the runners:spark:4 variant locally (shares the
    same src/ sources via runners/spark/4); CI covers it.

Notes

This only changes the Spark Structured Streaming runner's translation-time
bookkeeping; it does not change what gets computed or written, only when the
intermediate cached Dataset used to fan out a multi-output ParDo into its
per-tag Datasets is released.

@github-actions

Copy link
Copy Markdown
Contributor

Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment assign set of reviewers

@github-actions

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @chamikaramj added as fallback since no labels match configuration

Note: If you would like to opt out of this review, comment assign to next reviewer.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

The PR bot will only process comments in the main thread (not review comments).

@akashchamp
akashchamp force-pushed the spark-ss-unpersist-cached-datasets-40243 branch 2 times, most recently from 3393a1c to 8f2f937 Compare September 25, 2026 17:58
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @chamikaramj

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @chamikaramj added as fallback since no labels match configuration

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions
github-actions Bot requested a review from chamikaramj October 7, 2026 12:35
@akashchamp
akashchamp force-pushed the spark-ss-unpersist-cached-datasets-40243 branch 3 times, most recently from 3351a92 to b20bf04 Compare October 8, 2026 18:34
@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @chamikaramj

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @kennknowles added as fallback since no labels match configuration
Backup reviewer: damccorm (if the main reviewer is unresponsive, you can escalate to them by tagging their username)

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@kennknowles kennknowles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems generally good. We'll want to make sure everything is green. We can also run extra tests by touching the files for SparkRunner in ~/.github/trigger_files/

@codecov

codecov Bot commented Oct 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.07407% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.73%. Comparing base (76f6e6d) to head (b20bf04).
⚠️ Report is 249 commits behind head on master.

Files with missing lines Patch % Lines
...cturedstreaming/translation/EvaluationContext.java 61.11% 5 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #40265      +/-   ##
============================================
+ Coverage     56.16%   58.73%   +2.56%     
- Complexity     2288    13920   +11632     
============================================
  Files          1124     2585    +1461     
  Lines        178336   271705   +93369     
  Branches       1489    11243    +9754     
============================================
+ Hits         100170   159589   +59419     
- Misses        75645   106112   +30467     
- Partials       2521     6004    +3483     
Flag Coverage Δ
java 65.26% <74.07%> (-10.52%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@akashchamp
akashchamp force-pushed the spark-ss-unpersist-cached-datasets-40243 branch from b20bf04 to 154c9b1 Compare October 22, 2026 12:20
…ing runner

ParDoTranslatorBatch persists an intermediate, wide-row Dataset when a
multi-output ParDo needs to be divided into its per-tag Datasets, to avoid
re-evaluating the parent mapPartitions transformation once per tag. It never
called unpersist() on that cached Dataset, which the code even acknowledged
with a "FIXME What's the strategy to unpersist Datasets / RDDs?" comment.
Every multi-output ParDo therefore leaked its cached Dataset into Spark's
storage memory for the lifetime of the SparkSession, causing unbounded growth,
disk spilling and OOMs on large batch pipelines.

Add PipelineTranslator.TranslationState#cacheDataset(Dataset, StorageLevel), a
helper translators use instead of calling Dataset#persist directly when
caching a dataset outside the normal putDataset path. Track every dataset
cached this way and thread that collection through to EvaluationContext, which
now unpersists all of them in a finally block once leaf evaluation has
finished, whether it completed normally, was stopped early, or failed with an
exception. ParDoTranslatorBatch now goes through cacheDataset instead of
calling persist() directly, removing the FIXME.

Fixes apache#40243
runners/spark/4 keeps its own source-override copies of
PipelineTranslatorStreaming and StreamingEvaluationContext because the
Spark 4 streaming runner needs a different createEvaluationContext /
EvaluationContext than the shared batch translator. The previous commit
added a cachedDatasets parameter to
PipelineTranslator.TranslationState#createEvaluationContext and to the
EvaluationContext constructor, but didn't update these two Spark 4
overrides, which broke compilation of :runners:spark:4.

Thread cachedDatasets through both override files the same way the base
PipelineTranslator and EvaluationContext now do. Since
StreamingEvaluationContext#evaluate() fully replaces the base evaluate()
(it drives streaming queries instead of a per-leaf loop) rather than
calling super.evaluate(), also unpersist the cached datasets once all
queries have terminated, in its own finally block, so
Datasets cached via TranslationState#cacheDataset (used by the shared,
stateless-ParDo translator this streaming runner reuses) don't leak for
streaming pipelines either. Expose EvaluationContext#unpersistCachedDatasets
as protected so the subclass can call it.

Also reflow two doc comments in EvaluationContext.java and
PipelineTranslator.java to satisfy Spotless line-wrapping.
@akashchamp
akashchamp force-pushed the spark-ss-unpersist-cached-datasets-40243 branch from 154c9b1 to 791fb5a Compare October 24, 2026 15:58
@github-actions github-actions Bot added the size/m M: 50-249 lines changed label Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Spark Structured Streaming Runner memory leak: Datasets are persisted but never unpersisted in ParDoTranslatorBatch

2 participants