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:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -819,3 +819,52 @@ test("a State that lands AFTER the call was reported failed is not a barrier", a
|
||||
expect(docChannel(A).state).toBe("none");
|
||||
unsub();
|
||||
});
|
||||
|
||||
// A JOINER IS NEVER HANDED A BARRIER THE FAN-OUT HAS ALREADY FORGOTTEN.
|
||||
//
|
||||
// The replay reads the remembered `State` when a joiner subscribes and delivers it one microtask
|
||||
// later. Read once and CAPTURED, it was delivered even when the barrier was forgotten in
|
||||
// between - and there is exactly one hop in which that happens, measured rather than reasoned:
|
||||
// the call pushes its `State`, then rejects, and a second `subscribeDoc` on the same NURI lands
|
||||
// after the rejection is dispatched and before `reportSetupFailure`'s continuation does. At zero
|
||||
// hops the clear has not happened yet; at two or more `lastState` is already null, so no replay
|
||||
// is queued at all and the joiner simply opens a fresh attempt. One hop is the whole window.
|
||||
//
|
||||
// DIAGNOSTIC, and scoped as such: `getSyncState` reads the record and not the push, so it still
|
||||
// answers `"timed-out"` and no read gains authority. What the stale delivery breaks is
|
||||
// `emulated-verifier/open-repo.ts`'s bootstrap wait, which crosses its barrier on it and logs
|
||||
// `BARRIER <nuri> synced` for a barrier that no longer exists - and that trace line is the one
|
||||
// this codebase uses to tell a genuine absence from a not-yet-synced read.
|
||||
test("a joiner is never replayed a barrier the fan-out has already forgotten", async () => {
|
||||
let refuse: ((e: Error) => void) | null = null;
|
||||
const doc_subscribe = mock(async (_n: string, _s: unknown, cb: (r: unknown) => void) => {
|
||||
cb({ V0: { State: {} } }); // the barrier is remembered...
|
||||
return await new Promise<never>((_resolve, reject) => {
|
||||
refuse = reject; // ...while the call itself is still unanswered
|
||||
});
|
||||
});
|
||||
configure({ ng: { doc_subscribe } as any, useShape: (() => {}) as any });
|
||||
configureStoreRegistry({ getSession: async () => SESSION });
|
||||
const first = subscribeDoc(A, () => {});
|
||||
await tick();
|
||||
expect(docChannel(A).barrierCrossed).toBe(true);
|
||||
|
||||
// The call fails, and the joiner arrives one microtask later - inside the window.
|
||||
refuse!(new Error("RepoNotFound: the call did not take"));
|
||||
const handedAForgottenBarrier: boolean[] = [];
|
||||
let joiner: Unsubscribe = () => {};
|
||||
await new Promise<void>((resolve) => {
|
||||
queueMicrotask(() => {
|
||||
joiner = subscribeDoc(A, () => {
|
||||
handedAForgottenBarrier.push(!docChannel(A).barrierCrossed);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(docChannel(A).barrierCrossed).toBe(false); // the barrier is forgotten...
|
||||
expect(handedAForgottenBarrier).toEqual([]); // ...so nobody is handed it
|
||||
first();
|
||||
joiner();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user