fix: une barriere appartient a la tentative qui l'a produite
Seconde revue adverse, sur le correctif precedent - et elle a raison : celui-ci MASQUAIT la barriere perimee au lieu de l'oublier. `reportSetupFailure` effacait `establishing` et laissait `lastState`. Le premier correctif avait ajoute « et un canal tient » du cote du LECTEUR, ce qui ne cachait la barriere morte que tant que rien ne tenait. La souscription suivante sur le meme fan-out remettait `establishing` a vrai, le masque tombait - et le fan-out rejouait ce `State` au nouvel arrivant. Mesure : l'ouverture bootstrap franchissait sa barriere en 2 ms sur un `State` d'un appel qui avait ECHOUE, journalisait `BARRIER ... synced`, et `getSyncState` repondait `"synced"` - donc autorite, donc `noRowsMeansAbsent` prenait une lecture a zero ligne pour une absence definitive. Le depot detruit que le commit precedent pretendait empecher. Corrige a la source : une tentative qui echoue oublie sa barriere, comme le font deja `releaseFanOut` et `resubscribeDocs`. Le garde du lecteur est RETIRE avec, et pas garde en ceinture-bretelles : il etait devenu inatteignable, et c'est lui qui avait cache la faute. Apres : la barriere est oubliee, l'ouverture attend la sienne (202 ms), `"timed-out"`. Et le test que j'avais ecrit affirmait le defaut - `barrierCrossed` vrai - au lieu de l'etat correct. Corrige, plus un second test qui conduit le rejeu de bout en bout.
This commit is contained in:
@@ -299,6 +299,14 @@ export function docChannel(nuri: Nuri): DocChannel {
|
||||
// two together are the only thing that separates "asked" from "open" - and recording the
|
||||
// first as if it were the second is the defect this accessor exists to make impossible.
|
||||
const state: DocChannelState = !entry.establishing ? "none" : entry.realUnsub !== null ? "open" : "opening";
|
||||
// `lastState` alone, and deliberately NOT `&& state !== "none"`. Every path that stops an
|
||||
// attempt standing forgets the barrier with it - {@link releaseFanOut}, {@link resubscribeDocs}
|
||||
// and, since this was found the hard way, {@link reportSetupFailure} - so the two can no longer
|
||||
// disagree, and a guard here would be unreachable. It would also be a MASK: the first repair of
|
||||
// that defect put exactly this condition in the reader instead of forgetting the barrier at the
|
||||
// source, and it hid the fault while the fan-out went on replaying a dead `State` to the next
|
||||
// joiner. A fact that is wrong at the source has to be fixed at the source; a reader that
|
||||
// papers over it only moves the lie.
|
||||
return { state, barrierCrossed: entry.lastState !== null, incarnation: entry.incarnation };
|
||||
}
|
||||
|
||||
@@ -383,6 +391,15 @@ function reportSetupFailure(nuri: Nuri, entry: DocFanOut, error: unknown, attemp
|
||||
console.error("[subscribe] doc_subscribe failed for", nuri, error);
|
||||
if (!isCurrentAttempt(nuri, entry, attempt)) return;
|
||||
entry.establishing = false;
|
||||
// A BARRIER BELONGS TO THE ATTEMPT THAT PRODUCED IT, and this one failed. The push may well
|
||||
// have arrived - the callback is driven locally while the call's own answer travels the RPC -
|
||||
// but the call did not take, so there is no subscription behind that `State` and nothing will
|
||||
// follow it. Left here, the fan-out replayed it to the next joiner: a bootstrap open crossed
|
||||
// its barrier in 2ms on a `State` from a call that had FAILED, logged `BARRIER ... synced`,
|
||||
// and `getSyncState` answered `"synced"` - which is authority, so a zero-row read over the
|
||||
// document became a definitive absence and a deposit was destroyed. {@link releaseFanOut}
|
||||
// and {@link resubscribeDocs} both forget it for the same reason; this path did not.
|
||||
entry.lastState = null;
|
||||
for (const listener of [...entry.listeners]) {
|
||||
const tell = listener.onSetupFailed;
|
||||
if (tell === null || !entry.listeners.has(listener)) continue;
|
||||
|
||||
Reference in New Issue
Block a user