Skip to content

fix(sdk): throw when getting a skipped parallel branch - #799

Open
hippi345 wants to merge 8 commits into
aws:mainfrom
hippi345:fix/752-skipped-branch-get
Open

hippi345 wants to merge 8 commits into
aws:mainfrom
hippi345:fix/752-skipped-branch-get

Conversation

@hippi345

Copy link
Copy Markdown

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Issue Link, if available

Fixes #752

Description

get() on a parallel branch recorded as SKIPPED no longer waits until the SDK suspends with no pending operations. It throws ParallelBranchSkippedException, which names the branch, on the first invocation and on replay.

This covers both cases from the issue:

  1. A branch that never started because firstSuccessful() (or another early-completion config) finished the parallel first.
  2. A branch still running at completion that is later replayed as SKIPPED. get() now throws on that first invocation as well, so the first run and replay agree.

docs/core/parallel.md documents skipped(), statuses(), and the new exception.

ExecutionManager is unchanged. The skip settlement lives in ParallelOperation plus a small ChildContextOperation / BaseDurableOperation wait path so get() can complete without a checkpointed Operation. DurableContext is unchanged.

Demo/Screenshots

N/A

Checklist

  • I have filled out every section of the PR template
  • I have thoroughly tested this change

Testing

Unit Tests

Have unit tests been written for these changes?

Yes. ParallelBranchSkippedExceptionTest covers the named-branch message. ParallelOperationTest.minSuccessful_notExecuteSkippedBranchWhenReplay now asserts that get() on the skipped branch throws ParallelBranchSkippedException.

Integration Tests

Have integration tests been written for these changes?

Yes. Local runner tests in ParallelIntegrationTest use firstSuccessful() with two branches and call get() on the skipped one, with a bounded timeout:

  • never started (maxConcurrency(1)), first run and replay
  • still running at completion, first run and replay after a wait

testInvalidSuspensionWhenWaitingOnSkippedBranch now expects the named skipped exception instead of invalid suspension.

Examples

Has a new example been added for the change? (if applicable)

No. Existing parallel examples still pass; the behavior is documented in docs/core/parallel.md.

This change was prepared with AI assistance and reviewed by the author.

get() on a parallel branch recorded as SKIPPED now throws
ParallelBranchSkippedException instead of suspending with no pending
operations. This covers branches that never started and in-flight
branches skipped at completion, on the first invocation and on replay.
Move the existing waitForOperationCompletion javadoc back onto that
method and document waitForCompletionFuture separately.
@hippi345
hippi345 marked this pull request as ready for review October 17, 2026 17:05
@hippi345
hippi345 requested a review from a team October 17, 2026 17:05
@hippi345
hippi345 deployed to ai-pr-review-runtime October 17, 2026 17:05 — with GitHub Actions Active

@zhongkechen zhongkechen 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.

Found three issues in this revision. Validation: 97 existing focused tests passed. Targeted integration repros confirmed all three findings; the anyOf replay and changing-result repros also passed against the parent commit.

return enqueueItem(name, func, resultType, serDes, OperationSubType.PARALLEL_BRANCH, skip);
var branch = enqueueItem(name, func, resultType, serDes, OperationSubType.PARALLEL_BRANCH, skip);
if (skip) {
branch.completeAsSkipped();

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.

[P1] Preserve anyOf() results across replay

Immediately completing skipped futures changes which branch wins DurableFuture.anyOf(). With firstSuccessful() and maxConcurrency(1), register the winning branch first, then call anyOf(skipped, winner) while it is running. This returns the winner initially, but after a later suspension it throws ParallelBranchSkippedException because replay immediately settles the skipped future. Previously successful handler code now fails on replay.

A deterministic integration repro observes [winner, winner] on the parent commit and [winner, SKIPPED:skipped] on this PR. Preserve the observable winner when skipped futures participate in anyOf().

statuses);
var serializedResult = serializeAndDeserializeResult(cachedResult);
cachedResult = serializedResult.deserialized();
completeSkippedBranches(items, statuses);

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.

[P2] Make the completion snapshot and skip transition atomic

A branch can finish after the status snapshot records it as SKIPPED but before completeSkippedBranches() runs, particularly during result serialization. Its get() can then return a successful value before completeAsSkipped() changes that same future to throw.

A controlled integration repro gates ParallelResult serialization while branch B finishes: the first b.get() returns "B", but after parallel.get(), the second throws ParallelBranchSkippedException. The parent commit returns "B" both times. Coordinate the snapshot and skip transition with branch completion so an already-observed outcome cannot change.

childContextOperation.get();
return ParallelResult.Status.SUCCEEDED;
} catch (ParallelBranchSkippedException e) {
return ParallelResult.Status.SKIPPED;

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.

[P2] Keep propagated nested exceptions classified as failures

An outer branch can fail by calling get() on a skipped inner branch. Its checkpoint correctly records FAILED, but this catch classifies the outer branch as SKIPPED, reports failed() == 0, and replaces the inner exception with one naming the outer branch.

A nested-parallel integration repro confirms that the outer checkpoint is FAILED while the returned statuses are [SKIPPED] and outerBranch.get() names outer-branch instead of inner-skipped. Use the branch's explicit isSkipped() state to identify skips; an exception propagated from its body remains a failure.

zhongkechen and others added 2 commits October 21, 2026 13:05
anyOf ignores a branch that settled only because it was skipped, so replay
returns the same winner. The parallel snapshot marks an incomplete branch
skipped under the same lock that publishes its result, and a nested skip
exception stays a failure of the branch that propagated it.
@hippi345

Copy link
Copy Markdown
Author

The Oct 21 merge only brings in main, so the three comments were still open. They're fixed in aed55d3. anyOf no longer lets a skipped branch win while another future can still settle, so replay returns the same winner. The parallel snapshot and the skip happen under the same lock as branch completion, so get() can't return a value and then throw. A nested skip exception stays a failure of the outer branch; only an explicit skip is SKIPPED, and get() still names the inner branch.

hippi345 and others added 4 commits October 26, 2026 12:35
firstSuccessful() can checkpoint a parallel operation before a later
branch is registered. get() on that branch then suspends with no pending
operation. Decide the skip and publish the completion snapshot under one
lock, and settle any branch still missing from that result.
Comment on lines +287 to +290
private void markSkipped() {
skipped.set(true);
markAlreadyCompleted();
}

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.

Codex AI review · Finding arf_v1_kc5klhh4uqegosyk6mmj4px7do

[P1] Publish skipped results only after the parallel checkpoint succeeds

settleForParallelSnapshot() calls this before ParallelOperation.handleCompletion() serializes or submits its SUCCEED checkpoint. A caller waiting in branch.get() can therefore catch the skip and checkpoint subsequent work before persisted state records that skip. If the invocation is interrupted in this window, replay sees an incomplete parent and can execute and complete the previously skipped branch, changing control flow against those subsequent checkpoints. Reserve the skip under the lock, but delay both future completion and the exception exposed by get() until the parent checkpoint succeeds. Add an interruption/replay test covering this window.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

Comment on lines +713 to +716
if (eligible.isEmpty() || !isSkipSettlement(settled)) {
return settled;
}
pending.remove(settled);

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.

Codex AI review · Finding arf_v1_dp6iycqxibkyp44yj6sjqf6w7i

[P1] Handle exhaustion when the last waiting candidate becomes skipped

Consider DurableFuture.anyOf(b) called before another branch wins a firstSuccessful() parallel. When b becomes skipped, this iteration removes it because eligible was nonempty. The next iteration passes an empty list to awaitFirstSettledOperation(), whose CompletableFuture.anyOf() never completes. The caller can hang or trigger invalid suspension instead of receiving ParallelBranchSkippedException; replay with an already-skipped branch behaves differently. Retain a skipped fallback and return it when candidates are exhausted. Test a branch that becomes skipped after the wait begins.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

Comment on lines +162 to +164
if (skipped.get()) {
return;
}

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.

Codex AI review · Finding arf_v1_x2dftr772dldvhbwsumgln4mwz

[P2] Preserve the terminal plugin hook for started skipped branches

A running NestingType.FLAT branch has already emitted onOperationStart. Once it is marked skipped, this return—and the equivalent failure path—bypasses its existing fireOnOperationEnd() call. markSkipped() emits no replacement, so the hook never arrives even when the branch's user code finishes. This drops the branch span entirely in ExecutionOtelPlugin, which creates operation spans only in onOperationEnd, and bypasses custom plugin cleanup. Emit the terminal hook exactly once for started skipped branches while continuing to suppress result publication, and cover both success and failure exits.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

Three actionable findings:

  • Premature skip publication: sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java:288.
  • Empty-candidate wait: sdk/src/main/java/software/amazon/lambda/durable/execution/ExecutionManager.java:716.
  • Missing plugin completion: sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java:162.

Tests were not executed under the read-only review constraints.

Reviewed commit 2f3c402eaea8cab95da6e3be61e382c6cf0470ab. Workflow run

This branch was successfully deployed

1 active (outdated) deployment
ai-pr-review-runtime — 0b805a42 Deployed Oct 17, 2026 by hippi345 via notify / Generate notification summary #256
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: get() on a SKIPPED parallel branch never returns, and the execution fails

2 participants