Skip to content

fix(effect): treat interruption as abandonment in Effect.cached* and Cache - #8719

Merged
tim-smart merged 14 commits into
mainfrom
agent/nelson/b9dc01a37229
Oct 4, 2026
Merged

tim-smart merged 14 commits into
mainfrom
agent/nelson/b9dc01a37229

Conversation

@tim-smart

@tim-smart tim-smart commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Interruption is now treated as abandonment in Effect.cached, Effect.cachedWithTTL, Effect.cachedInvalidateWithTTL, Cache, ScopedCache, RcRef, RcMap and the cluster ResourceMap.

Effect.cached*

Previously the first caller ran the effect on its own fiber. Interrupting that caller cancelled the computation for everyone, and the interrupt exit was cached and replayed.

All three APIs now share one implementation that runs each computation on a detached fiber and counts the callers waiting on it:

  • A caller that leaves only decrements the count. The computation is interrupted once the last caller leaves.
  • Exits containing interrupts are never cached, and the TTL callback isn't invoked for them.
  • When a run is abandoned, it is detached before it is interrupted. A call made while its finalizers are still running starts a fresh run. Results are only published by the run that is still current, so an abandoned run can't clear or overwrite its replacement.

Cache and ScopedCache

Cache.get already shared cancellation between callers. The gap was after the last caller left: the abandoned entry stayed in the map until its lookup fiber exited, so a new get could join it and get the interrupt. Entries created by get and refresh now remove themselves from the map (only if they're still the current entry for the key) before the lookup is interrupted.

ScopedCache gets the same treatment. It also now counts waiters that arrive before the lookup fiber starts, which covers the capacity-eviction window.

RcRef

Acquisition used to run on the first caller's fiber under a semaphore. Interrupting that caller cancelled the acquisition, and the next queued caller started it again from scratch. Acquisition now runs on a detached fiber in a new Acquiring state, with a count of the callers waiting on it:

  • A caller that leaves only decrements the count. The acquisition is interrupted when the last caller leaves, and the ref goes back to Empty before that happens, so a new get starts a fresh acquisition.
  • On success, every caller still waiting holds a reference. A caller interrupted after the acquisition finishes but before it resumes releases its reference.
  • Closing the ref interrupts a pending acquisition, and its scope is still closed with the acquisition's exit.

ResourceMap (cluster)

Lookups used to run on the first caller's fiber, so interrupting that caller failed every other waiter. Lookups now run on a detached fiber and count their waiters, with the same abandonment rules as above. Removing a failed or abandoned entry now checks entry identity. Before this, a lookup abandoned after remove(key) could delete the replacement entry. Closing the map also interrupts pending lookups.

RcMap

Lookups were already detached and reference-counted. A lookup that ends in interruption is now dropped from the map, so with a non-zero idleTimeToLive the next get looks up again instead of replaying the interrupt. If no borrower holds the entry, its scope is closed.

Notes

  • The JSDoc for cachedWithTTL used to say interruption exits were cached for the returned TTL. I updated it, along with the cached docs, to describe the new behaviour.
  • The regression tests were added before the corresponding fixes, then reduced to 13 focused cases in 14bb77f7cf. The old replays the owner's interrupted exit test was removed because it asserted the behaviour this change reverses.
  • Layer MemoMap is not in this PR. Sharing builds on a detached fiber breaks layers that capture their building fiber with Fiber.getCurrent() and interrupt it to shut down. RpcServer.makeProtocolStdio and makeProtocolWorkerRunner do this, and the MCP stdio conformance tests fail because of it.

Validation

  • pnpm vitest --run --project effect: 324 files, 11053 tests passed
  • pnpm vitest --run packages/platform packages/vitest packages/atom packages/sql/sqlite-node packages/opentelemetry: passed
  • pnpm check, pnpm lint: clean

Closes EFF-1692

@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fda02af

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

This PR includes changesets to release 32 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node 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/docgen Patch
@effect/doctest Patch
@effect/openapi-generator 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

@effect-janitor effect-janitor Bot added the 4.0 label Oct 4, 2026
Comment thread packages/effect/src/Cache.ts Outdated
@github-actions

github-actions Bot commented Oct 4, 2026 •

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
arbitrary-combinators.ts 38.90 KB 38.90 KB 0.00 KB (0.00%)
basic.ts 7.04 KB 7.04 KB 0.00 KB (0.00%)
batching.ts 10.13 KB 10.13 KB 0.00 KB (0.00%)
brand.ts 6.72 KB 6.72 KB 0.00 KB (0.00%)
cache.ts 10.88 KB 10.84 KB +0.03 KB (+0.31%)
config.ts 22.15 KB 22.15 KB 0.00 KB (0.00%)
differ.ts 21.29 KB 21.29 KB 0.00 KB (0.00%)
http-client.ts 22.43 KB 22.27 KB +0.16 KB (+0.74%)
http-router.ts 33.74 KB 33.55 KB +0.18 KB (+0.55%)
logger.ts 11.08 KB 11.08 KB 0.00 KB (0.00%)
metric.ts 9.02 KB 9.02 KB 0.00 KB (0.00%)
optic.ts 6.95 KB 6.95 KB 0.00 KB (0.00%)
pubsub.ts 15.16 KB 15.16 KB 0.00 KB (0.00%)
queue.ts 12.08 KB 12.08 KB 0.00 KB (0.00%)
schedule.ts 11.18 KB 11.18 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.73 KB 13.73 KB 0.00 KB (0.00%)
schema-binary.ts 39.95 KB 39.95 KB 0.00 KB (0.00%)
schema-class.ts 21.03 KB 21.03 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 32.06 KB 32.06 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 27.24 KB 27.24 KB 0.00 KB (0.00%)
schema-string-transformation.ts 14.46 KB 14.46 KB 0.00 KB (0.00%)
schema-string.ts 11.99 KB 11.99 KB 0.00 KB (0.00%)
schema-template-literal.ts 16.05 KB 16.05 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.44 KB 38.44 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 25.26 KB 25.26 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 20.21 KB 20.21 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 20.41 KB 20.41 KB 0.00 KB (0.00%)
schema-toFormatter.ts 20.54 KB 20.54 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 25.00 KB 25.00 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 20.50 KB 20.50 KB 0.00 KB (0.00%)
schema.ts 20.25 KB 20.25 KB 0.00 KB (0.00%)
stm.ts 13.08 KB 13.08 KB 0.00 KB (0.00%)
stream.ts 9.98 KB 9.98 KB 0.00 KB (0.00%)

@effect-janitor effect-janitor Bot added the bug Something isn't working label Oct 4, 2026
- ScopedCache: drop the per-entry onInterrupt hook (which also leaked into the
  public Entry interface) and share one removeEntry helper between the
  awaiting side and the lookup's exit handler
- RcRef and ResourceMap: assign the acquisition/lookup fiber unconditionally
  instead of guarding and resetting an optional field; count awaiters in one
  place
- Effect.cached: skip the clock lookup on the hit path, drop the dead
  expiresAt reset on invalidate, hoist the infinite TTL constant
- RcMap: fork the interrupted-entry cleanup from the lookup context instead
  of retaining the caller fiber
- Cache.refresh: reuse removeEntry
@tim-smart
tim-smart merged commit bc44526 into main Oct 4, 2026
13 checks passed
@tim-smart
tim-smart deleted the agent/nelson/b9dc01a37229 branch October 4, 2026 08:08
juliusmarminge added a commit to pingdotgg/t3code that referenced this pull request Oct 5, 2026
Effect 4.0.1 stops Effect.cached* from keeping an interrupted run
(Effect-TS/effect#8719), so the seven sites that only needed that go
back to Effect.cached. Two changes stay:

- The mobile DPoP key no longer keeps a failed SecureStore read, so the
  next relay request loads it again. Effect.cached still keeps failures.
- A fresh provider maintenance read starts a new resolution instead of
  joining one already running, so an update click never uses ownership
  resolved before it. invalidate did not detach the running one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant