Skip to content

fix: settle await on first settled condition incl. rejection (#399) - #785

Open
xepozz wants to merge 1 commit into
masterfrom
fix/issue-399-await-promise-settlement
Open

fix: settle await on first settled condition incl. rejection (#399)#785
xepozz wants to merge 1 commit into
masterfrom
fix/issue-399-await-promise-settlement

Conversation

@xepozz

@xepozz xepozz commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What was changed

Multi-condition await/awaitWithTimeout now unblocks on the first settled condition and
propagates a rejected promise (Promise::anyPromise::race), gated behind
FeatureFlags::$settleAwaitOnFirstSettledCondition (default off = old behaviour).

Why?

A rejected awaited promise was swallowed until the timeout. Flag-gated for replay safety,
like sdk-java's CANCEL_AWAIT_TIMER_ON_CONDITION.

Checklist

  1. Closes [Not a Bug] await doesn't interrupts on the activity with error #399

  2. How was this tested:

Unit tests for both flag states + baseline. Unit suite green.

  1. Any docs updates needed?

No.

await()/awaitWithTimeout() combined multiple conditions with Promise::any,
which unblocks only on the first *fulfilled* condition and ignores rejections
until every condition rejects (or the timer fires). A rejected promise
condition therefore never woke the await, so awaitWithTimeout(t, rejecting)
blocked for the full timeout instead of surfacing the failure.

The single-condition path already propagated rejections, so the behavior was
inconsistent. Switch the multi-condition path to Promise::race (unblock on the
first *settled* condition and propagate its rejection) and cancel the internal
awaitWithTimeout timer when a condition rejects.

Because this changes the command sequence for in-flight histories, it is gated
behind the new FeatureFlags::$settleAwaitOnFirstSettledCondition flag
(default false = legacy Promise::any behavior), mirroring how sdk-java gates
CANCEL_AWAIT_TIMER_ON_CONDITION behind an SdkFlag. Also guard the degenerate
empty-conditions await so it fails fast with LengthException instead of
hanging, and document the Promise::any() escape hatch on Workflow::await().

Tests cover both flag states, the empty-await guard, and the flag-independent
baseline (closures, fulfilled promise, single rejection).
@xepozz
xepozz requested review from a team, roxblnfk and wolfy-j as code owners July 31, 2026 12:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dadfc77067

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return Promise::race($result)->then(
$onResolved,
function (\Throwable $reason) use ($conditionGroupId): never {
$this->rejectConditionGroup($conditionGroupId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Settle callable conditions discarded after a rejected race

When the feature flag is enabled and a rejected promise wins a race that also contains a closure or Mutex, this only removes the condition group from $awaits; it does not reject the Deferred objects created by ScopeContext::addCondition(). Those deferreds were registered with Scope::onAwait(), whose cancellation callback retains each deferred until it settles, but after the group is removed its callable can never settle it. A long-running workflow that catches and repeatedly retries these rejections therefore accumulates pending callbacks and promise chains until the scope ends; settle the group's deferreds before discarding it.

Useful? React with 👍 / 👎.

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.

[Not a Bug] await doesn't interrupts on the activity with error

1 participant