Repository navigation
[Java fn-execution] Drain bundle output before waiting for the ProcessBundleResponse - #40721
PabloPauling wants to merge 3 commits into
Conversation
…sBundleResponse ActiveBundle.close() waited for the ProcessBundleResponse and only then consumed the bundle's output. The inbound queue holds 100 messages, and since apache#38407 the Python SDK's send queue is bounded too (100 MB). The SDK does not complete a bundle until its output has been sent, so a bundle whose output exceeds both buffers deadlocks. On Flink batch one bundle is a whole partition, so this triggers at about 1 GB of output per subtask. close() now drains the inbound observer before waiting for the response. Elements embedded in the response are delivered from an async callback, and a failed response closes the observer so the drain cannot wait for an is_last that will never arrive. The response's error stays primary. Fixes apache#40720. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
📋 Review Summary
This PR resolves a deadlock in SdkHarnessClient.ActiveBundle.close() where bundles producing more output than the combined SDK outbound buffer and runner BeamFnDataInboundObserver queue capacity (~100 messages) would block indefinitely because the runner waited for ProcessBundleResponse before draining the inbound data queue. Draining the inbound observer prior to awaiting the response, delivering embedded response elements via whenCompleteAsync, and closing the inbound observer on response failure are well-reasoned and thoroughly tested.
🔍 General Feedback
- The ordering of
inboundObserver.awaitCompletion(),MoreFutures.get(response), and exception aggregation preserves primary SDK failure causes while preventing deadlocks on both the data stream and embedded response paths. - One minor edge case worth considering is avoiding completion/checkpoint/finalization callbacks when
drainException != null(i.e., when draining the output failed even though the SDK completedProcessBundleResponsenormally).
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
| } | ||
| // After the response, so that a failed bundle reports the SDK's error rather than the | ||
| // cancelled queue it caused. | ||
| if (drainException != null) { |
There was a problem hiding this comment.
🟢 Low: If inboundObserver.awaitCompletion() fails (e.g., a downstream output receiver throws an exception or the data stream closes prematurely) while the SDK's ProcessBundleResponse completes normally, drainException is non-null but exception remains null during the MoreFutures.get(response) block above (lines 556–567). As a result, progressHandler.onCompleted, checkpointHandler.onCheckpoint, and finalizationHandler.requestsFinalization will still be invoked before stateRegistration.abort() is called and drainException is thrown.
Consider guarding the completion/checkpoint/finalization callbacks at lines 561–567 with if (drainException == null) so that a bundle whose output drain failed does not report completion or register finalization/residuals.
There was a problem hiding this comment.
Good point, fixed in 5b2c223. The completion, checkpoint and finalization callbacks now only run when the output drain succeeded. A bundle whose drain failed reports neither residuals nor finalization (its state registration was already aborted). Covered by testFailedOutputDrainDoesNotReportCompletion. The original code also ran these callbacks before draining, so this tightens pre-existing behaviour too.
If consuming the bundle's output fails while the SDK completes the ProcessBundleResponse normally, the bundle has still failed. Skip progressHandler.onCompleted, the checkpoint and the finalization request in that case, so a failed bundle does not register residuals or finalization. The state registration is already aborted. Addresses review feedback on apache#40721. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/gemini review |
|
Assigning reviewers: R: @shunping added as fallback since no labels match configuration This pull request likely touches a core component ("core" label). Please review with scrutiny. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
There was a problem hiding this comment.
📋 Review Summary
This PR resolves a deadlock in SdkHarnessClient.ActiveBundle.close() where bundles producing more output than the bounded inbound queue and SDK send buffer could hold would hang indefinitely. By draining BeamFnDataInboundObserver before awaiting the ProcessBundleResponse and handling embedded response elements and failure states asynchronously, the fix cleanly breaks the circular wait.
🔍 General Feedback
- The asynchronous handling of
ProcessBundleResponseviawhenCompleteAsyncproperly prevents deadlocking on the draining thread when the response is already completed with embedded elements. - Error propagation and suppression order are well-preserved, and the new unit tests thoroughly cover large outputs, embedded response elements, and receiver failures during draining.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
Fixes #40720.
ActiveBundle.close()waited for theProcessBundleResponseand only thenconsumed the bundle's output. The inbound queue holds 100 messages, and since
#38407 the Python SDK's send queue is bounded too (100 MB). The SDK does not
complete a bundle until its output has been sent. So a bundle whose output
exceeds both buffers deadlocks: the SDK blocks sending, and the runner blocks
waiting for a response that never comes. On Flink batch, one bundle is a whole
partition, so this triggers at about 1 GB of output per subtask.
Changes in
close():awaitCompletion()on the inbound observer beforeMoreFutures.get(response), so output is consumed while the SDK is stillproducing it.
whenCompleteAsynccallback. It has to be async: if the response is alreadycomplete, a synchronous callback would run on the draining thread and could
block putting into the queue that thread must drain.
an
is_lastthat will never arrive. The response's error stays the primaryexception, and the cancelled-queue exception is added as suppressed. The
existing
handleCleanup*tests still pass unchanged.Tests:
testOutputLargerThanTheInboundQueueDoesNotDeadlock: the SDK sends 150 datamessages and only then the response. Times out without the fix.
testOutputEmbeddedInAnAlreadyCompletedResponseIsDelivered: covers theembedded-elements path.
Also adds a
CHANGES.mdentry under 2.78.0 Bugfixes.cc @scwhittle (author of #38407)
🤖 Generated with Claude Code