Skip to content

fix(Deferred): clear resumes before resuming waiters on completion - #7364

Merged
tim-smart merged 1 commit into
Effect-TS:mainfrom
kitlangton:deferred-resume-starvation
Aug 20, 2026
Merged

tim-smart merged 1 commit into
Effect-TS:mainfrom
kitlangton:deferred-resume-starvation

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7365

Summary

Completing a Deferred whose completion effect carries an interrupt cause can permanently starve later waiters. doneUnsafe iterates self.resumes live:

if (self.resumes) {
  for (let i = 0; i < self.resumes.length; i++) {
    self.resumes[i](effect)
  }
  self.resumes = undefined
}

When the completion effect is e.g. Exit.failCause(Cause.interrupt()), resuming a suspended waiter kills that waiter fiber synchronously inside resume(effect). The dying waiter's Deferred.await cleanup then runs while self.resumes is still defined and splices its own entry out of the array mid-iteration. The next waiter shifts down into the just-visited index, the loop index moves past it, and that waiter is never resumed — it hangs forever.

Concretely: with two suspended waiters, waiter 0 resumes (and dies of interruption), its cleanup splices resumes[0], waiter 1 becomes resumes[0], the loop advances to i = 1 against length = 1, and waiter 1 is skipped. Observed empirically in [email protected].

This is the starvation counterpart to #7194: that fix guarded the _await cleanup against running after completion (resumes === undefined), but the cleanup still mutates the array when it fires during the completion loop.

Fix

Snapshot and clear resumes before resuming, matching the swap-before-resume pattern Latch already uses for exactly this reentrancy hazard:

if (self.resumes) {
  const resumes = self.resumes
  self.resumes = undefined
  for (let i = 0; i < resumes.length; i++) {
    resumes[i](effect)
  }
}

Clearing first also makes a dying waiter's cleanup a no-op during completion (the resumes === undefined guard from #7193 short-circuits), so the loop iterates a stable snapshot.

All completion paths (done, complete, completeWith, fail, die, interrupt, into, and external doneUnsafe callers in PubSub, ScopedCache, FiberMap, FiberHandle, Socket) funnel through doneUnsafe, so this single change covers them. Queue resumes takers via Set iteration and Latch already snapshots, so neither shares the bug.

Reproducer

The added test forks two waiters, lets both suspend, then completes the Deferred with Exit.failCause(Cause.interrupt()). On unpatched main the second waiter never settles and the test's timeout fires; with the fix both waiters settle with the interrupt cause.

Testing

  • pnpm vitest run --project effect test/Deferred.test.ts — new test fails before the fix, all 16 pass after
  • pnpm vitest run --project effect test/Latch.test.ts test/PubSub.test.ts test/Queue.test.ts test/ScopedCache.test.ts test/FiberMap.test.ts test/FiberHandle.test.ts — 204 passed
  • pnpm check in packages/effect, repo lint on touched files

@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 27daa66

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 6.96 KB 6.96 KB 0.00 KB (0.00%)
batching.ts 9.82 KB 9.82 KB 0.00 KB (0.00%)
brand.ts 6.55 KB 6.55 KB 0.00 KB (0.00%)
cache.ts 10.73 KB 10.73 KB 0.00 KB (0.00%)
config.ts 21.08 KB 21.08 KB 0.00 KB (0.00%)
differ.ts 20.02 KB 20.02 KB 0.00 KB (0.00%)
http-client.ts 21.66 KB 21.66 KB 0.00 KB (0.00%)
logger.ts 10.91 KB 10.91 KB 0.00 KB (0.00%)
metric.ts 8.95 KB 8.95 KB 0.00 KB (0.00%)
optic.ts 6.71 KB 6.71 KB 0.00 KB (0.00%)
pubsub.ts 14.99 KB 15.00 KB -0.01 KB (-0.03%)
queue.ts 11.67 KB 11.67 KB 0.00 KB (0.00%)
schedule.ts 10.84 KB 10.84 KB 0.00 KB (0.00%)
schema-class.ts 19.65 KB 19.65 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 29.85 KB 29.85 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.81 KB 25.81 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.53 KB 13.53 KB 0.00 KB (0.00%)
schema-string.ts 11.03 KB 11.03 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.33 KB 15.33 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 21.77 KB 21.77 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.19 KB 24.19 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 18.99 KB 18.99 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 18.81 KB 18.81 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.67 KB 18.67 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 23.09 KB 23.09 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.29 KB 19.29 KB 0.00 KB (0.00%)
schema.ts 18.90 KB 18.90 KB 0.00 KB (0.00%)
stm.ts 12.72 KB 12.72 KB 0.00 KB (0.00%)
stream.ts 9.76 KB 9.76 KB 0.00 KB (0.00%)

This branch had an error being deployed

1 failed deployment
fork — 27daa66f Deployed Aug 20, 2026 by kitlangton via approval-gate #12262
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0 bug Something isn't working ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deferred completion with an interrupt cause skips suspended waiters (waiter never resumes)

2 participants