Ordered loader: a superseded request's failure can block refills after its repair finishes (model-predicted), and the state refactor it suggests
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 12/100
- Type d'issue
- Refactorisation
- Clarté
- À clarifier
- Activité
- Active
- Stack technique
- typescript
Piste de recherche
The work centres on the ordered loader's request and retirement logic, and on ordered-source-loader-state-oracle.test.ts, which the issue asks to extend with a generated-history lane. Read the TLA+ model and the progressive-source-readiness-tla/ precedent first, because the proposed rules are defined there. Success means the pinned 9-step replay is reproduced against the real loader, and every existing oracle still passes unchanged.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Summary
A TLA+ model of the ordered loader's request authority and acquisition ledger predicts a bug introduced by #2154's overlap rule. A bounded repair can supersede a finite request and then finish. Then a second ordering invalidation can arrive before the loader reacts. If the old request then fails, the loader records the failure as its own. That blocks ordinary refills until a window move. The law says the full-source read that the overlap replaced must still run. This is a model prediction. No code witness exists yet.
Predicted trace
- A page settles. A second page is in flight.
- An ordering invalidation opens a bounded debt, and the repair starts. The repair supersedes the in-flight page.
- The repair finishes, so the debt is cleared.
- A second invalidation arrives, and the loader's
loadMoreis deferred. Pending joined work can defer it, and so can a report whose graph run is scheduled later. - The old page fails.
recoversAfterRepairis false, because debt is pending again.failedRequestis set, soloadMorereturns early. Nothing restarts the loader until a window move.
Proposed rule (model-checked)
A superseded request's failure blocks nothing. It only raises the debt to full, and ordinary loading then starts the full-source read. The repair's own failures and full-source failures still block, as the failure law requires. This rule replaces recoversAfterRepair and the "abandon, then start full" branch.
A single normalization step can also replace three retirement special cases: at repair finish, at full-source completion, and for a stale request that settles late. The rule: when loading is quiet, release every settled acquisition older than the request that established coverage.
Model results
These come from TLC (field-lab container), with MaxEpoch = 2 and four request slots. The deadlock check is off, because full coverage is terminal.
- Current code's rules: fail "a superseded failure never strands" in 9 steps (the trace above).
- Proposed rules: pass, exhaustively over 25,172 distinct states. With the special cases removed and only the normalization step, they pass over 26,048 states.
- Weakened controls, each with its expected counterexample:
- the pre-fix stale-prefix rule breaks "the repair keeps its own acquisition"
- holding a stale request until the next repair breaks "no leak"
- the pre-fix recovery rule strands a failure
- removing the normalization step breaks "no leak".
Not modeled:
- the scheduler (staged continuations, deferred refills, the graph-revision check)
- cursor memo state
- tie and refill sub-steps
- same-epoch prefix supersession
- truncate replay
- publication holds.
Plan
This is the follow-up agreed after #2154 and #2177:
-
Add a generated-history lane to
ordered-source-loader-state-oracle.test.ts. It interleaves page, tie, repair and full-source settlements, failures, invalidations, deferredloadMoreand window moves, and checks the model's three invariants against the real loader. Its mutants are the pre-fix rules. Start with the 9-step trace above as a pinned replay. -
Refactor with form F2:
- add a
classify(stamp)helper, so staleness is one check, not seven - add the single normalization step
- treat superseded failures as raising debt only.
Keep every existing oracle unchanged as the equivalence check. Move to F1 (typed work slot plus debt lattice) only if the phase fields stay tangled.
- add a
-
Add the model under
docs/contributing/oracle-reviews/ordered-loader-ledger-tla/, following the precedent ofprogressive-source-readiness-tla/. -
Then model the publication barrier, after #2177 merges.
Model source
LoaderLedger.tla
---- MODULE LoaderLedger ----
\* Abstraction of OrderedSourceLoader request authority and acquisition retirement.
\* Grammar trace: debt lattice (I1), staleness classification (I2), ledger retirement (I3).
\* Not modeled: scheduling (I4), cursor memo (O5), tie/refill sub-steps of the chain, window ops.
EXTENDS Naturals, FiniteSets, TLC
CONSTANTS MaxEpoch, Ids, Rules
\* Rules: "current" = the code's patched case rules; "unified" = one normalize step;
\* "oldRetire" | "oldLeak" | "oldRecover" = the three pre-fix rules (weakened controls).
VARIABLES epoch, debt, phase, chainEpoch, failure, coverage, req, settledOnce, coverEpoch
\* coverEpoch: the epoch stamp of the request that established the current coverage.
vars == <<epoch, debt, phase, chainEpoch, failure, coverage, req, settledOnce, coverEpoch>>
Unused == [kind |-> "none", ep |-> 0, st |-> "unused", sup |-> FALSE]
Init == /\ epoch = 0 /\ debt = "none" /\ phase = "idle" /\ chainEpoch = 0
/\ failure = "none" /\ coverage = "nothing" /\ settledOnce = FALSE
/\ req = [i \in Ids |-> Unused] /\ coverEpoch = 0
Inflight(k) == {i \in Ids : req[i].st = "inflight" /\ req[i].kind = k}
AnyInflight == \E i \in Ids : req[i].st = "inflight"
Free == {i \in Ids : req[i].st = "unused"}
New(k) == [kind |-> k, ep |-> epoch, st |-> "inflight", sup |-> FALSE]
\* Authoritative work started while a finite request is in flight supersedes it (P8).
Supersede(r) == [j \in Ids |-> IF r[j].st = "inflight" /\ r[j].kind = "finite" THEN [r[j] EXCEPT !.sup = TRUE] ELSE r[j]]
\* Unified rule: in a quiescent coverage state, any settled entry older than the epoch is redundant.
Normalize(r, ph, db, ce) ==
IF Rules \in {"unified", "unifiedMin"} /\ ph = "idle" /\ db = "none"
THEN [i \in Ids |-> IF r[i].st = "settled" /\ r[i].ep < ce
THEN [r[i] EXCEPT !.st = "released"] ELSE r[i]]
ELSE r
IssueFinite ==
/\ phase = "idle" /\ debt = "none" /\ failure = "none" /\ coverage # "full"
/\ Inflight("finite") = {}
/\ \E i \in Free : req' = [req EXCEPT ![i] = New("finite")]
/\ UNCHANGED <<epoch, debt, phase, chainEpoch, failure, coverage, settledOnce, coverEpoch>>
Invalidate ==
/\ epoch < MaxEpoch /\ coverage # "full"
/\ epoch' = epoch + 1
/\ debt' = IF debt = "none" /\ settledOnce /\ phase = "idle" THEN "bounded"
ELSE IF debt = "none" THEN "full" ELSE debt
/\ UNCHANGED <<phase, chainEpoch, failure, coverage, req, settledOnce, coverEpoch>>
StartRepair ==
/\ phase = "idle" /\ debt = "bounded" /\ failure = "none" /\ Inflight("chain") = {}
/\ \E i \in Free : req' = [Supersede(req) EXCEPT ![i] = New("chain")]
/\ phase' = "repair" /\ chainEpoch' = epoch
/\ UNCHANGED <<epoch, debt, failure, coverage, settledOnce, coverEpoch>>
StartFull ==
/\ phase = "idle" /\ debt = "full" /\ failure = "none" /\ Inflight("full") = {}
/\ \E i \in Free : req' = [Supersede(req) EXCEPT ![i] = New("full")]
/\ phase' = "fullheld"
/\ UNCHANGED <<epoch, debt, chainEpoch, failure, coverage, settledOnce, coverEpoch>>
\* An explicit window move retries a recorded failure (P3).
\* The same synchronous call releases the failed acquisitions and starts the
\* full-source read, so nothing settles between them.
ExplicitRetry ==
/\ failure # "none" /\ phase = "idle" /\ debt = "full" /\ Inflight("full") = {}
/\ failure' = "none"
/\ LET cleared == [i \in Ids |-> IF req[i].st = "failed" THEN [req[i] EXCEPT !.st = "released"] ELSE req[i]]
IN \E k \in {i \in Ids : cleared[i].st \in {"unused", "released"}} :
req' = [Supersede(cleared) EXCEPT ![k] = New("full")]
/\ phase' = "fullheld"
/\ UNCHANGED <<epoch, debt, chainEpoch, coverage, settledOnce, coverEpoch>>
ReleaseAllSettledExcept(r, keep) ==
[j \in Ids |-> IF j # keep /\ r[j].st = "settled" THEN [r[j] EXCEPT !.st = "released"] ELSE r[j]]
SettleFinite(i) ==
LET r == req[i]
current == r.ep = epoch /\ phase = "idle" /\ failure = "none"
s == [req EXCEPT ![i].st = "settled"]
finished == phase = "idle" /\ debt = "none"
self == IF ~current /\ finished /\ Rules \notin {"oldLeak", "unifiedMin", "unifiedMinNoNorm"}
THEN [s EXCEPT ![i].st = "released"] ELSE s
\* Pre-fix rule: a stale prefix supersedes every smaller prefix, including the repair's.
nr == IF ~current /\ Rules = "oldRetire"
THEN [j \in Ids |-> IF self[j].kind = "chain" /\ self[j].st = "settled"
THEN [self[j] EXCEPT !.st = "released"] ELSE self[j]]
ELSE self
IN /\ r.st = "inflight" /\ r.kind = "finite"
/\ coverEpoch' = IF current THEN epoch ELSE coverEpoch
/\ req' = Normalize(nr, phase, debt, IF current THEN epoch ELSE coverEpoch)
/\ coverage' = IF current THEN "pages" ELSE coverage
/\ settledOnce' = (settledOnce \/ current)
/\ UNCHANGED <<epoch, debt, phase, chainEpoch, failure>>
SettleChain(i) ==
LET r == req[i] IN
/\ r.st = "inflight" /\ r.kind = "chain"
/\ IF chainEpoch # epoch
THEN \* a newer invalidation: start the replacement chain (P5)
\E k \in Free :
/\ req' = [[req EXCEPT ![i].st = "settled"] EXCEPT ![k] = New("chain")]
/\ chainEpoch' = epoch
/\ UNCHANGED <<epoch, debt, phase, failure, coverage, settledOnce, coverEpoch>>
ELSE IF failure # "none" \/ (Rules \in {"unified", "unifiedNoNorm", "unifiedMin", "unifiedMinNoNorm"} /\ debt = "full")
THEN IF Rules = "oldRecover"
THEN \* abandon: debt stays full, nothing restarts
/\ req' = [req EXCEPT ![i].st = "settled"]
/\ phase' = "idle" /\ debt' = "full"
/\ UNCHANGED <<epoch, chainEpoch, failure, coverage, settledOnce, coverEpoch>>
ELSE \* abandon and start the full-source read the overlap replaced
\E k \in Free :
/\ req' = [[req EXCEPT ![i].st = "settled"] EXCEPT ![k] = New("full")]
/\ phase' = "fullheld" /\ debt' = "full"
/\ UNCHANGED <<epoch, chainEpoch, failure, coverage, settledOnce, coverEpoch>>
ELSE LET settled == [req EXCEPT ![i].st = "settled"]
nr == IF Rules \in {"unifiedMin", "unifiedMinNoNorm"} THEN settled ELSE ReleaseAllSettledExcept(settled, i) IN
/\ req' = Normalize(nr, "idle", "none", epoch)
/\ phase' = "idle" /\ debt' = "none" /\ coverage' = "chain" /\ settledOnce' = TRUE
/\ coverEpoch' = epoch
/\ UNCHANGED <<epoch, chainEpoch, failure>>
SettleFull(i) ==
/\ req[i].st = "inflight" /\ req[i].kind = "full"
/\ req' = IF Rules \in {"unifiedMin", "unifiedMinNoNorm"}
THEN Normalize([req EXCEPT ![i].st = "settled"], "idle", "none", req[i].ep)
ELSE ReleaseAllSettledExcept([req EXCEPT ![i].st = "settled"], i)
/\ phase' = "idle" /\ debt' = "none" /\ coverage' = "full" /\ failure' = "none"
/\ settledOnce' = TRUE /\ coverEpoch' = req[i].ep
/\ UNCHANGED <<epoch, chainEpoch>>
Fail(i) ==
LET r == req[i]
finishedBefore == phase = "idle" /\ debt = "none" /\ failure = "none"
\* Authoritative work failing is the loader's own failure: it waits for an
\* explicit or backoff retry, even if a superseded request failed first.
f == IF r.kind \in {"chain", "full"} THEN "own"
ELSE IF failure = "none" THEN (IF r.sup THEN "superseded" ELSE "own") ELSE failure
ph == IF r.kind \in {"chain", "full"} THEN "idle" ELSE phase
\* Unified: a superseded request's failure blocks nothing. Its partial
\* writes only raise the debt to full; ordinary loading then recovers.
upgradeOnly == Rules \in {"unified", "unifiedNoNorm", "unifiedMin", "unifiedMinNoNorm"} /\ r.kind = "finite" /\ r.sup
recover == ~upgradeOnly /\ r.sup /\ finishedBefore /\ Rules # "oldRecover"
failedReq == [req EXCEPT ![i].st = "failed"]
IN /\ r.st = "inflight"
/\ failure' = IF upgradeOnly THEN failure ELSE f
/\ debt' = IF upgradeOnly /\ coverage = "full" /\ phase = "idle" /\ debt = "none"
THEN debt ELSE "full"
/\ \/ /\ recover
/\ \E k \in Free : req' = [failedReq EXCEPT ![k] = New("full")]
/\ phase' = "fullheld"
\/ /\ ~recover
/\ req' = failedReq
/\ phase' = IF upgradeOnly THEN phase ELSE ph
/\ UNCHANGED <<epoch, chainEpoch, coverage, settledOnce, coverEpoch>>
Next ==
\/ IssueFinite \/ Invalidate \/ StartRepair \/ StartFull \/ ExplicitRetry
\/ \E i \in Ids : SettleFinite(i) \/ SettleChain(i) \/ SettleFull(i) \/ Fail(i)
Spec == Init /\ [][Next]_vars
\* P4/P10: a finished repair's own acquisition is still held.
ChainHeld == (phase = "idle" /\ debt = "none" /\ coverage = "chain")
=> \E i \in Ids : req[i].kind = "chain" /\ req[i].ep = coverEpoch /\ req[i].st = "settled"
\* P10: in a quiescent coverage state, nothing older than the coverage epoch is still held.
NoLeak == (phase = "idle" /\ debt = "none" /\ ~AnyInflight)
=> \A i \in Ids : req[i].st = "settled" => req[i].ep >= coverEpoch
\* P3/P8: a request that authoritative work superseded never strands its failure.
\* (A failure of a request nothing superseded may block until an explicit retry: the failure law.)
NoStrandedStaleFailure == (failure = "superseded") => (phase # "idle" \/ AnyInflight)
TypeOK == debt \in {"none", "bounded", "full"} /\ phase \in {"idle", "repair", "fullheld"}
====
Configuration (vary `Rules`)
CONSTANTS MaxEpoch = 2
Ids = {1, 2, 3, 4}
Rules = "unifiedMin" \* or current | unified | oldRetire | oldLeak | oldRecover | unifiedMinNoNorm
SPECIFICATION Spec
INVARIANTS TypeOK ChainHeld NoLeak NoStrandedStaleFailure
CHECK_DEADLOCK FALSE
This issue was written by Isaac.
- Langage dominant
- TypeScript
- Étoiles
- 3.9k
- Forks
- 272
- Merge moyen
- 22 h 54 min
- PR mergées (30 j)
- 236
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de TanStack/db
-
Difficulté 4/5 3-5 jours Accessibilité débutants 22/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 8/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 3-5 jours Accessibilité débutants 25/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 5/100
TanStack/db#2186 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
query-db: a Query refetch that joins a loader's refetch acquisition does not report shifted pagesOuverte
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 8/100
TanStack/db#2185 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de TanStack/db
Issues similaires
-
bug engine
Difficulté 2/5 1-3 heures Accessibilité débutants 86/100
frostney/GocciaScript#1746 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 67/100
agilepathway/label-checker#739 · 4 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
Wrong key location at startupOuverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 68/100
CopilotKit/OpenBot#800 ·
Les mainteneurs répondent en général sous 1 jour
-
Endpoint links in the tag operations list are below the 24px target sizePeut-être pris @AdzerKI l’a pris aujourd’hui. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 79/100
ehmpathy/rhachet-roles-bhuild#471 ·
Les mainteneurs répondent en général sous 1 jour