Content temporarily unavailable
Stale reacquired Query ownership oracle review#
Reviewed source: branch investigate-finite-stale-reacquisition, based on origin/main at 31dad2dd3. The first runs used acaaf4c0a. The counts, RED/GREEN table, and mutant table were last rerun on the commit that follows 67987ce10, which extends the reacquired lease table and records the open cells; that tree was measured before it was committed. The independent reviews read the acaaf4c0a tree.
Contract and defect#
PR #2198 (db0ac815e) made a released Query keep an empty queryToRows entry until its release publishes, so metadata that has not published the release cannot restore it as an owner. The result handler's already-applied guard and the on-demand direct-write skip also read entry presence as "the active observer lease has applied a result". When a stale Query was reacquired while its release was held, the refetch-start notification repeated the cached result, the guard read the release's entry as applied, and the reacquired lease owned nothing. The release then removed the row, and a failed refetch never restored it.
Laws checked, each stated beside its model in packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts:
- A reacquired observer lease owns the Query's latest successful data from reacquisition until its own release. A successful refetch replaces that result; a failed one keeps it. staleTime decides only whether a refetch starts.
- Published owners never name a released lease, including when another lease claims the row while the release is held.
- An accepted on-demand direct write patches the cache of an active Query whose lease has not applied a result.
- An accepted direct write survives a refetch start of an empty current result.
- A release whose durable commit fails is terminal for its sync run, and a restart begins from clean ownership.
Authority: the file's opening ownership and direct-write laws, the existing reacquisition history, TanStack Query's stale-mount and failed-refetch rules, the DbClient hydration history, the glossary's cleanup definition, and the AGENTS.md fail-fast rules.
Repair#
hasAppliedOwnership reports an entry only when it is not a release's entry. removeQueryOwnership marks its empty entry in a WeakSet. The guard and the on-demand direct-write skip use the helper. The result-application rollback snapshot keeps the entry by reference (CR-1 below). The production diff is 15 lines added and 6 removed in src/query.ts.
RED and GREEN#
| Witness | main 31dad2dd3 | Repair |
|---|---|---|
| Reacquired lease table (42 cells: 7 refetch histories × 0–2 peers × 1–2 rows) | 20 fail. Example: published: [] and settled: [] where the model gives ["shared"]. With peers, shared is lost only at peerReleased, while a two-row result loses solo at publication | 42 pass |
| Published owners table (8 cells) | 8 pass | 8 pass |
| Reacquired cache patch behind deferDataRefresh | Fails: cache ["Shared"], model ["Manual"] | Passes |
| Empty-result direct insert (eager, on-demand) | 2 pass | 2 pass |
| Failed release commit (3 cells) | 3 pass | 3 pass |
| Reacquired commit fails after a held release | Passes | Passes |
The oracle file passes 585 of 585 tests with the repair and 564 on main. The Query Collection package passes 1460 tests with 2 skipped, and the type check reports no errors. Three repeated runs of the new tables gave identical results.
Mutant results#
Each mutant was applied to the repaired source, run against the whole oracle file, and reverted. Counts are for the final oracle file.
| Mutant | Outcome | Killing witness |
|---|---|---|
| Guard reads entry presence (the defect) | Assertion failure, 20 tests | Reacquired lease table |
| Release entry not marked | Assertion failure, 21 tests | Reacquired lease table, cache patch |
| Direct-write skip reads entry presence | Assertion failure, 1 test | Cache patch (Query cache only; public rows agree) |
| Applied means non-empty entry | Assertion failure, 2 tests | Empty-result direct insert: on-demand loses the insert at its receipt; eager keeps Manual after Newer |
| Delete entry at release | Assertion failure, 4 tests | Published owners while-held: owner count 2 instead of 1; without a peer the hydrated row leaks |
| Delete the released entry when the Query is reacquired | Assertion failure, 2 tests | Published owners while-held, reacquired: the reacquired lease has no cached result and its fetch fails, so b copies a back from unpublished metadata |
| Replace the released entry with an unmarked empty Set at reacquisition | Assertion failure, 21 tests | Reacquired lease table, cache patch |
| Mark the reacquired lease's own application entry as released | Assertion failure, 5 tests | Persisted direct-write refetch-start histories and the direct cache patch bound |
| Reuse the released Set object for the next application | Assertion failure, 25 tests | Reacquisition history and reacquired lease table: the reused entry keeps its mark |
| Guard removed (false &&) | Assertion failure, 4 tests | Persisted direct-write refetch-start histories |
| Hostile storage that swallows a failed commit | Assertion failure, 2 cells and 1 test | Both failed-commit witnesses |
| Cleanup skips entries without an observer | Survives the oracle file; assertion failure, 1 test, in the package suite | tests/query.test.ts "should clear retained ownership during explicit collection cleanup": a retained Query's retention marker survives cleanup |
The last four rows came from a later independent review. It reported two survivors without a distinguishing history; the concrete versions above were written from its descriptions. Deleting the released entry at reacquisition survived every test until the reacquired published-owner histories were added; it fails them and passes nothing else differently.
CodeRabbit review of 8ff1db49f#
Review 5600814169 had one inline finding and a summary. Ledger:
| ID | Claim | Verdict | Action |
|---|---|---|---|
| CR-1 | The rollback snapshot in result application clones the entry, so a failed application that restores it drops a release's mark | True at the code level. No public consequence reached: in persisted histories the release retires its entry before the reacquired application snapshots (instrumented with the release commit gated and ungated), and in non-persisted histories the held commit rejects only by cancellation, which the restore guards skip | Fixed: the snapshot keeps the entry by reference, because application replaces the entry rather than mutating it. No witness distinguishes this; reverting it passes all 557 tests |
| CR-2 | Add a history with reacquisition during a held release, a commit failure before publication, and a failed refetch; the row must remain until the reacquired lease is released | The history runs; its expected result conflicts with the fail-loud law. The release is durable and the reacquired result is not, so the row leaves and the Collection enters error | Added "fails loudly when a reacquired lease's commit fails after a held release"; a storage adapter that swallows the failure fails it |
| CR-3 | Docstring coverage 0% for touched functions | True; the touched closure helpers use line comments like their neighbours | No change, per AGENTS.md's rule to match surrounding comment density |
| CR-4 | CodeRabbit's ESLint tool timed out | A tool note, not a code claim; the repository Lint check passed on 8ff1db49f | No change |
| CR-5 | Title, description, and issue checks passed | Informational | No change |
ORC outcomes#
- ORC-001: Each table states its law, authority, and limits before mechanics.
- ORC-002: The models keep lease results and owner sets. They do not import queryToRows, the guard, or sync transactions.
- ORC-003: Prose beside each model, grammar, driver, and comparison. The prose was updated with each grammar change.
- ORC-004: Fixed enumerations, not generated properties. Each axis has a stated contribution: staleTime starts the refetch; outcome and placement separate keeping, replacing, and losing the row; peer hides a lost result until peerReleased; claims reaches the unpublished-metadata premise. Excluded: eager reacquisition, whose premise is unreachable (probed: cleanup removes the Query from the cache, so a restart fetches); persisted retention, which keeps released rows by design.
- ORC-005: Public loadSubset, unloadSubset, direct writes, DbClient dehydrate and hydrate, cleanup, and preload. Observations are public rows, stored rows, Collection status, rejection messages, dehydrated owner counts, and QueryClient.getQueriesData. No missing-information hypothesis was identified.
- ORC-006: The mutant table above. Every kill is an assertion failure at the named comparison.
- ORC-007: Not applicable; no generated property.
- ORC-008: The reacquisition model keeps one result per active lease; the peer axis needs the union, shown by the peer cells that differ only at peerReleased. The owner model keeps a set; the owner count distinguishes peer histories that public rows do not.
- ORC-009: Glossary terms observer lease, acquisition release, publication, accepted sync transaction, and cleanup. a, b, c, and blocker are declared local names.
- ORC-010: Each history registers cleanup that releases its holds and Query clients; comparisons are single structured assertions.
- ORC-011: Not applied. The named shared-fault risk (an entry read as applied) is separated by the empty-result and cache-patch witnesses, which observe different boundaries; no further formulation was identified.
- ORC-012: This record.
- ORC-013: The reusable law "an empty entry is applied unless it belongs to a release" has a premise witness (reacquired lease table) and a nearby boundary witness (empty-result direct insert) that rejects the non-empty rule.
- ORC-014: The controlled storage adapter supplies the commit failure. A receiving probe on Node SQLite aborted the release's delete with a SQLite trigger. The Collection entered error and storage kept the row, but the public Collection dropped it, while a peer's row stayed. The controlled adapter keeps the row public, whether it rejects at once or after a delay. The real SQLite adapter drops it on every sync-present configuration tried: managed, resolved for sync-present, and adapter-only without a coordinator. The controlled premise does not hold on the real host, so the cross-boundary claim is contradicted, not established. The divergence belongs to the SQLite persistence owner; see Open cells.
Bug-class boundary#
Contract: Query ownership after release and reacquisition. Histories: an on-demand lease with a one- or two-row result and a held release, a stale or fresh reacquisition, zero to two peers, an optional refresh barrier, a direct write, and a failed release commit. Path: Query Collection over a real QueryClient and a controlled persistence adapter. Observations: public rows, stored rows, owners, status, and Query cache at the named checkpoints.
Open cells:
- CR-1, unreached. Core rejects a committed, held sync transaction only in state.cleanup(), which runs after sync cleanup has removed the observers, so the restore guard returns. The remaining route is a duplicate-key replay inside the reacquired application's own open transaction, which no history here constructs.
- SQLite durability failure. On the Node SQLite adapter, a sync transaction (here, a release) whose durable step fails still removes the row from the public Collection, while storage keeps it. This contradicts the glossary's accepted-sync-transaction rule, which accepts a wrapping sync's transaction only after its durable step. It belongs to the SQLite persistence owner and is outside this Query DB change; tracked in #2210.