v4 Cache.get: a caller that joins an abandoned lookup fails with an interruption it did not ask for
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 42/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
Research direction
The report points to Cache.get and EntryImpl.await; start by tracing how callers join an entry, how awaiters is updated, and how the lookup fiber or Deferred completes. Reproduce the timing in the issue’s TypeScript example, then compare the behavior on the v4 branch and main. Done means a caller that did not leave receives the lookup’s result or its own failure, not another caller’s interruption.
Written by the indexing model from the issue text.
Description
Version: effect 4.0.0-beta.107 (the v4 line developed in Effect-TS/effect-smol, which does not take issues from outside the org).
Cache.get can fail a caller with an interrupt-only cause even though nothing interrupted that caller. It happens when the caller joins a lookup that another caller abandoned.
Reproduction (effect 4.0.0-beta.107, Bun 1.4.3)
import { Cache, Cause, Effect, Exit, Fiber } from "effect"
const program = Effect.gen(function* () {
let lookups = 0
const cache = yield* Cache.make({
capacity: 10,
// A lookup that cannot stop at once, like a spawned process or a blocked query
lookup: (_key: string) =>
Effect.sync(() => ++lookups).pipe(
Effect.andThen(Effect.sleep("300 millis").pipe(Effect.uninterruptible)),
Effect.as("value")
)
})
const a = yield* Cache.get(cache, "k").pipe(Effect.forkChild)
yield* Effect.sleep("50 millis")
yield* Fiber.interrupt(a).pipe(Effect.forkChild) // the only waiting caller gives up
yield* Effect.sleep("50 millis")
const b = yield* Effect.exit(Cache.get(cache, "k")) // b is never interrupted
console.log(Exit.isFailure(b) && Cause.hasInterruptsOnly(b.cause)) // true
const c = yield* Effect.exit(Cache.get(cache, "k"))
console.log(Exit.isSuccess(c), lookups) // true 2
})
Effect.runPromise(program)
What happens
When the last caller waiting on an entry leaves, the cache interrupts the lookup fiber (in 4.0.0-beta.107, EntryImpl.await calls fiberInterrupt once awaiters reaches 0). A caller that calls get before that fiber has exited finds the entry still in the map. It joins the fiber and receives its interrupted exit. Any catchCause above that caller then sees an interrupt-only cause, which looks exactly like a cancellation of the caller itself.
On main the lookup runs inside the first caller's fiber (effect.onExit(self.lookup(key), …) completes the shared Deferred with that fiber's exit). Interrupting that first caller would complete the Deferred with its interruption for every joiner. That conclusion comes from reading the code; I have not run it.
Expected
A caller that was not interrupted should get the lookup's value, or a failure of the lookup's own. One way to get there: the lookup is owned by the cache rather than by its first caller, and a caller that leaves stops only its own wait.
Impact
We hit this in a desktop app: a request that joined a repository lookup, which a timed-out snapshot had abandoned, ended interrupted. That stopped the user's first message from starting a session until they sent a second one. Thanks to @dahli4, whose trace from their own machine pinned it down. On our side we now run lookups in a scope the cache's owner holds, and retry a joiner that got an interruption it did not ask for.
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 10h 7m
- Merged PRs (30d)
- 808
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
Support {self: this} for fnUntracedPossibly taken @ArjunCodess claimed this 32 days ago. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Effect-TS/effect#8101 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Typed unix socket and TLS options for `BunHttpClient` and `DenoHttpClient`Possibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement platform
Effect-TS/effect#9687 · 1 assignee ·
Maintainers usually reply within 1 day
-
RFC 9457 Problem Details for HttpApi errorsPossibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement
Difficulty 5/5 Over a week Newbie friendliness 20/100
Maintainers usually reply within 1 day
-
Typed parsers and formatters for common HTTP header valuesPossibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement
Effect-TS/effect#9685 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
endora-commerce/endora-commerce#397 ·
Maintainers usually reply within 1 day
-
level/task
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
wazuh/wazuh-dashboard-plugins#9472 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 63/100
Maintainers usually reply within 1 day
-
ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
xavxyz/bei-shan-lin-shui#57 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
opennextjs/opennextjs-cloudflare#1461 ·
Maintainers usually reply within 1 day