fix: le rejeu relit la barriere au lieu de la capturer

Dernier chemin pouvant remettre une barriere deja oubliee. Le rejeu
lisait `lastState` a la souscription et le livrait une micro-tache plus
tard ; l'etablissement peut echouer dans cet intervalle, et
`reportSetupFailure` desavoue son `State` avec lui.

La fenetre est d'exactement UN saut de micro-tache, mesuree et non
raisonnee : a zero saut l'effacement n'a pas encore eu lieu, a deux ou
plus `lastState` est deja nul donc aucun rejeu n'est mis en file et
l'arrivant ouvre une tentative neuve. `isCurrentAttempt` ne couvre pas
ce cas : cet echec n'incremente pas `attempt`, donc l'entree et la
tentative sont toutes deux encore les courantes.

DIAGNOSTIC, et cadre comme tel : `getSyncState` lit l'enregistrement et
non la poussee, donc il repond toujours `"timed-out"` et aucune lecture
ne gagne d'autorite. Ce que la livraison perimee cassait est l'attente
bootstrap de `open-repo`, qui franchissait sa barriere dessus et
journalisait `BARRIER <nuri> synced` pour une barriere qui n'existait
plus - or cette ligne de trace est celle qui distingue une absence reelle
d'une lecture non synchronisee.

J'avais ecrit ce remede puis l'avais RETIRE, faute de test qui
discrimine. L'ordonnancement ci-dessus est ce qui manquait ; le test
echoue sans le correctif et passe avec.
This commit is contained in:
Sylvain Duchesne
2026-09-24 14:36:06 +02:00
parent 6de87a9edd
commit 7736de0d68
2 changed files with 65 additions and 3 deletions
+16 -3
View File
@@ -697,11 +697,24 @@ export function subscribeDocUnguarded(
// Joining a document somebody else already opened: hand this listener the `State` its
// own `doc_subscribe` would have pushed it. Asynchronously, like the real push, so a
// caller that sets itself up after this call still sees it.
//
// RE-READ inside the microtask, never captured out here, because the barrier can be forgotten
// in between: the establish can reject in that gap and {@link reportSetupFailure} disowns its
// `State` with it. One microtask hop is the whole window - measured, not reasoned - and this
// is the last path that could hand out a barrier after it had been forgotten. `isCurrentAttempt`
// does not cover it: that failure does not bump `attempt`, so the entry and the attempt are
// both still the current ones.
//
// Diagnostic rather than an authority leak: `getSyncState` reads the record and not the push,
// so it still answers `"timed-out"`. What the stale delivery broke is the bootstrap wait in
// `emulated-verifier/open-repo.ts`, which crossed its barrier on it and logged
// `BARRIER <nuri> synced` for a barrier that no longer existed - and that trace line is the one
// this codebase uses to tell a genuine absence from a not-yet-synced read.
if (joined.lastState) {
const { resp, type } = joined.lastState;
queueMicrotask(() => {
if (fanOuts.get(nuri) === joined && joined.listeners.has(listener)) {
deliver(nuri, listener, resp, type);
const standing = joined.lastState;
if (standing && fanOuts.get(nuri) === joined && joined.listeners.has(listener)) {
deliver(nuri, listener, standing.resp, standing.type);
}
});
}