Repository navigation
test(Atom): family finalizer must not evict a newer atom for the same key (failing) - #429
Open
Tanner-Scadden wants to merge 1 commit into
Open
Tanner-Scadden wants to merge 1 commit into
Tanner-Scadden wants to merge 1 commit into
Conversation
… key Adds a failing regression test. When an atom created by Atom.family is collected, its FinalizationRegistry callback removes the cache entry by key without checking whether the entry still refers to the collected atom. If a newer atom for the same key was cached before the callback ran, it is evicted and the next lookup creates a third atom. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a failing regression test for a race in
Atom.family'sWeakRef/FinalizationRegistrycache. It contains only the test, as a reproduction. A suggested fix is below, and I'm happy to push it here or in a follow-up if you want it.The bug
familyregisters each new atom with aFinalizationRegistrywhose callback removes the cache entry by key:The callback doesn't check that the entry still points at the atom that was collected. Finalizer callbacks run at some point after collection, not right away, so this sequence can happen:
Kis created, then released.ref.deref()now returnsundefined, but A's finalizer callback hasn't run yet.family(K)sees the deadWeakRef, creates atom B, and cachesK -> WeakRef(B).K's entry, which now holds B.family(K)creates atom C, even though B is still alive and in use.Impact
After step 5, code that kept a reference to B (for example a poller calling
registry.refresh(b), or a callback that captured the atom) and code that readsfamily(K)again (for example components re-rendering) have different atoms for the same key. Refreshes and writes to B are never seen by readers of C, so the UI silently stops updating. It only happens under particular GC timing, which makes it intermittent and hard to reproduce in real apps.The test
The test stubs
WeakRefandFinalizationRegistrywithvitest.stubGlobalso it doesn't depend on real GC timing. It marks A as collected, looks up the key again to get B, then calls A's captured finalizer callback by hand, and asserts thatfamily(1)still returns B.Run with
pnpm vitest run packages/atom/test/Atom.test.ts -t family.Suggested fix (not included in this PR)
Only remove the entry if it is still the
WeakRefthat was registered for the collected atom:The held value holds only the
WeakRef, not the atom, so registering it doesn't keep the atom alive. I checked locally that with this change the new test passes, along with the rest ofAtom.test.ts(76/76).Notes
patchchangeset for@effect-atom/atom.Atom.familycan silently lose its dependency edge to GC #426 (a nestedfamilylosing its inner cache to GC). This PR is about the finalizer evicting a newer entry in a single-level family.packages/effect/src/reactivity/Atom.ts). That fix removes the entry only when itsWeakRefis empty. Aderef() === undefinedcheck like that would also work here, as an alternative to the identity check above.🤖 Generated with Claude Code