Skip to content

[Prism] Schedule consumers of a self checkpointing source - #39572

Merged
Abacn merged 4 commits into
apache:masterfrom
Eliaaazzz:fix-39446-prism-continuation-watermark
Aug 21, 2026
Merged

[Prism] Schedule consumers of a self checkpointing source#39572
Abacn merged 4 commits into
apache:masterfrom
Eliaaazzz:fix-39446-prism-continuation-watermark

Conversation

@Eliaaazzz

Copy link
Copy Markdown
Contributor

Fixes #39446.

The bug

An unbounded source SDF that returns ProcessContinuation.resume() has its residual re-queued by reElementResiduals as a pending element carrying the input element's event time, which for an Impulse rooted source is mtime.MinTimestamp. That pins minPendingTimestampLocked, so the stage's watermark never advances.

updateWatermarks returns no refreshes when the output watermark does not move, and PersistBundle marked only the producing stage as changed. So a consumer that had just been handed pending elements was never inserted into em.changedStages, bundleReady was never called for it, and its elements accumulated forever.

This is worse than the issue describes: it starves plain ordinary consumers too, not only stateful ones, and it survives transitively through a chain of stages. checkForQuiescence cannot catch it, because the source stage stays schedulable, so the job live locks rather than failing fast.

Reproduced at the element manager level before fixing. Pre-fix, across 5 source bundles:

round 0: src out=-inf | sink pending=1 scheduled=0
round 4: src out=-inf | sink pending=5 scheduled=0

The fix

  1. PersistBundle records the consumers that accepted data into stageState.consumersWithNewData, and updateWatermarks hands that set to the scheduler on the newOut <= ss.output path, clearing it on the advancing path where the normal return already lists every consumer. Routing the wake-up through updateWatermarks is what keeps a pipeline whose watermark advances on exactly its previous schedule.
  2. The recording is gated on em.sawResidual, latched the first time any bundle returns a residual. A pipeline that never self checkpoints never populates the set, so its bundle scheduling is unchanged. This matters: an earlier revision that recorded unconditionally changed bundle packing and broke TestRunner_Pipelines/flatten_to_sideInput at -count=20.
  3. bundleReady lets a stateful stage with no side inputs run on pending data alone, since statefulStageKind.buildEventTimeBundle already takes data at any watermark and gates only timers. Its stillSchedulable now requires buildable work so a key holding only a future timer cannot spin against the relaxed gate, and such a key no longer consumes the OneKeyPerBundle slot, is not marked in progress, and does not hold the bundle's minimum timestamp.

What is deliberately unchanged

  • The watermark value. BundleApplication.output_watermarks says an unreported estimate means MIN_TIMESTAMP, and Java's ProcessFn holds at futureOutputWatermark ?? elementTimestamp. A pinned MIN_TIMESTAMP output watermark for a source that reports no estimate is the spec, so the TODO the issue points at is not where the defect is.
  • Aggregate stages stay on the watermark gate. A GBK genuinely needs window closure or a trigger.
  • Any stage that reads a side input stays on the watermark gate, so such a consumer still waits under a residual pinned source. Side input readiness is derived from the main watermark, and GetSideData filters non global windows by the bundle watermark, so relaxing this would let a bundle read a partial or empty side input. That is worse than waiting. Fixing it properly means changing how side inputs are materialized for every stage, which I would rather do separately with your input.

Testing

New elementmanager_continuation_test.go, all teeth checked by reverting each half of the fix independently:

  • TestPersistBundle_ContinuationResidualConsumers, ordinary and stateful consumers of a source that self checkpoints forever.
  • TestPersistBundle_ContinuationResidualTransitive, src self checkpoints, mid returns no residual of its own, sink two stages down.
  • TestPersistBundle_ContinuationResidualWatermark, pins the output_watermarks contract in both directions.
  • TestStatefulBuildEventTimeBundle_OneKeyPerBundle, a key headed by a future timer must not consume the single key slot, and must not enter newKeys or hold minTs.

Green: go test ./pkg/beam/runners/prism/... -count=1, -run "TestRunner_Pipelines/flatten" -count=20, and the new tests at -count=50.


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Choose reviewer(s) and mention them in a comment (R: @username).
  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@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

Fixes apache#39446.

An unbounded source SDF that returns a process continuation residual has
that residual re-queued as a pending element carrying the input element's
event time, MinTimestamp for an Impulse rooted source, so the stage's
watermark never advances. updateWatermarks returns no refreshes when the
output watermark does not move, and PersistBundle marked only the
producing stage as changed, so a consumer that was just handed pending
elements was never surfaced to the scheduler. Its elements accumulated
forever. checkForQuiescence cannot catch this, because the source stays
schedulable, so the job live locks instead of failing fast.

PersistBundle now records the consumers that accepted data, and
updateWatermarks hands them to the scheduler on the path where the output
watermark does not advance. Recording is gated on any bundle having
returned a residual, so a pipeline that never self checkpoints keeps its
previous bundle scheduling.

bundleReady additionally lets a stateful stage with no side inputs run on
pending data alone, under the same gate, since
statefulStageKind.buildEventTimeBundle takes data at any watermark and
gates only timers. Its stillSchedulable now requires buildable work, and
a key that supplies nothing no longer consumes the OneKeyPerBundle slot,
is not marked in progress, and does not hold the bundle's minimum
timestamp.

A consumer that reads a side input still waits on the watermark, since
side input readiness is derived from it.
@Eliaaazzz
Eliaaazzz force-pushed the fix-39446-prism-continuation-watermark branch from d147583 to feddf87 Compare August 1, 2026 00:09
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @jrmccluskey for label go.

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).

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @jrmccluskey @shunping

@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: @lostluck for label go.

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)

@Abacn Abacn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tested locally and on CI: https://github.com/apache/beam/actions/runs/32273187172/attempts/1 . Local test found previously failing test now passing consistently. However, same commit running on CI still has a failing test. Likely there are other flakiness

// hasBuildableDataLocked reports whether a key that isn't in progress heads its
// heap with data, or with a timer the watermark has reached. Callers hold ss.mu.
func (ss *stageState) hasBuildableDataLocked(watermark mtime.Time) bool {
for k, dnt := range ss.pendingByKeys {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here it iterates over pendingByKeys again. Would there be any performance concern?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It stops at the first key it could build, so the full pass over pendingByKeys only happens when the answer is no, which is exactly the case that previously rescheduled empty bundles without bound; one scan to park the stage replaces that loop. On the productive path it usually stops at the first key that heads with data. The bundleReady call site is behind em.sawResidual, so pipelines without a self checkpointing source never evaluate it, and the builder call site is short circuited by timerCleared.

The cost that remains is one scan per unproductive build on a stateful stage with many pending keys headed by future timers. An index of buildable keys would remove it, but buildability depends on the current watermark, since a key headed by a future timer becomes buildable when the watermark moves, so the index needs its own bookkeeping on every watermark advance. I would leave that until a profile shows this scan, and can file it as a follow up if you prefer.

@Abacn Abacn Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

which is exactly the case that previously rescheduled empty bundles...

Is that correct that full scan only happens when there is no new data? In this case the performance concern would be minimum then

@Eliaaazzz Eliaaazzz Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's right. It only runs to the end when nothing is buildable, and returns at the first buildable key otherwise.

@Eliaaazzz

Copy link
Copy Markdown
Contributor Author

Looked into the run. Attempt 1 failed on ActiveMQJmsIOTest.test_xlang_jms_write_read_queue with 0 != 100 and the rerun of the same commit passed. That suite runs the Python DirectRunner with a Java expansion service, so prism is not on the execution path. The same workflow failed twice on master in its last twenty runs without this change, both in the same file with the same 0 != 100 shape, IbmMqJmsIOTest.test_xlang_jms_write_read_queue_client_ack on Aug 16 and Aug 17. So I read it as pre existing flakiness in the new JMS suite rather than something this change introduced. Happy to file a flaky test issue for it.

@Abacn

Abacn commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Looked into the run. Attempt 1 failed on ActiveMQJmsIOTest.test_xlang_jms_write_read_queue with 0 != 100 and the rerun of the same commit passed. That suite runs the Python DirectRunner with a Java expansion service, so prism is not on the execution path. The same workflow failed twice on master in its last twenty runs without this change, both in the same file with the same 0 != 100 shape, IbmMqJmsIOTest.test_xlang_jms_write_read_queue_client_ack on Aug 16 and Aug 17. So I read it as pre existing flakiness in the new JMS suite rather than something this change introduced. Happy to file a flaky test issue for it.

(Sorry for replying to wrong thread previously)

These tests do use prism runner:

def usePrismRunnerSuites = ["messagingCrossLanguage"]

My comment was about this does fix the flakiness seen in the JmsIO test to some extent. Since this PR closes #39446, can we apply f274b1b here (remove 5s delay) and trigger PostCommit Python Xlang Messaging Direct? Thanks!

The test pre-published part of the records before running the pipeline
to work around the starvation this change fixes. All records are now
published after the pipeline starts, and the duplicate-tolerant
assertion tightens back to an exact count. Also bumps the messaging
postcommit trigger file.
Comment thread sdks/python/apache_beam/io/external/xlang_jmsio_it_test.py Outdated
@Eliaaazzz

Copy link
Copy Markdown
Contributor Author

Looked into the run. Attempt 1 failed on ActiveMQJmsIOTest.test_xlang_jms_write_read_queue with 0 != 100 and the rerun of the same commit passed. That suite runs the Python DirectRunner with a Java expansion service, so prism is not on the execution path. The same workflow failed twice on master in its last twenty runs without this change, both in the same file with the same 0 != 100 shape, IbmMqJmsIOTest.test_xlang_jms_write_read_queue_client_ack on Aug 16 and Aug 17. So I read it as pre existing flakiness in the new JMS suite rather than something this change introduced. Happy to file a flaky test issue for it.

(Sorry for replying to wrong thread previously)

These tests do use prism runner:

def usePrismRunnerSuites = ["messagingCrossLanguage"]

My comment was about this does fix the flakiness seen in the JmsIO test to some extent. Since this PR closes #39446, can we apply f274b1b here (remove 5s delay) and trigger PostCommit Python Xlang Messaging Direct? Thanks!

Thanks for the good call. I have done that!

@Abacn Abacn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

@Abacn
Abacn merged commit 3b9e647 into apache:master Aug 21, 2026
108 checks passed
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.

[Prism] Watermark deadlock when unbounded source is initially empty

2 participants