fix(asyncify): consume-once root suspensions — replaces the wake-window deferral

The deferral (9ca2ac1) modeled the wrong condition and taxed every parked
fiber completion with a macrotask hop: under CI load that stretched
three-client apply chains and flaked drift-trio S4 twice consecutively
(26/26 green locally under stress) — retracted.

The actual fatal state, readable in all four prod stacks once seen: a SECOND
rewind of the same root suspension. Root suspends once per fiber_swap out of
it; two parked fibers completing against one root suspension epoch (a tool
fiber + a collab fiber both waking around open:settled) each drive
finishContextSwitch(root) — the second rewinds already-consumed data →
"unreachable executed" → poisoned runtime, with the wake-side "index out of
bounds" as the sibling symptom.

Cure: stop exempting root from the validity check the shim already keeps.
First consumption proceeds synchronously — zero added latency anywhere; the
second is refused ([wx-asyncify] "root suspension already consumed") — the
yielded fiber stays properly suspended and resumable, root continues via its
real pending resume, libcontext's ghost-epoch contract enforced one layer
lower. Root remains exempt only from the internally-parked quarantine (its
yield park is routine).

.ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim).

Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios
kicad-chromium 26/26 under 3-worker stress, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
Gergő Törcsvári 2026-08-01 15:53:18 +02:00
commit ae33a100c2
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
3 changed files with 40 additions and 42 deletions

View file

@ -297,48 +297,32 @@ if (typeof Fibers !== "undefined"
}
var isRoot = newFiber === Fibers.__rootFiber;
// The prod killer (four identical trap stacks, v0.1.1922): a fiber
// completes and re-enters the ROOT while a sleep wake's own rewind is
// still on the stack — two "resume main" paths interleaved in one tick,
// doRewind(root) replays over live state, "unreachable executed",
// poisoned runtime. Root entry is legal and constant in healthy flow;
// ONLY the wake-window overlap is fatal. Defer it by one macrotask so
// the wake settles first — ordering change only, nothing is dropped.
if (isRoot && (Asyncify.__inSleepWake || 0) > 0) {
var deferred = newFiber;
Fibers.__rootDeferrals = (Fibers.__rootDeferrals || 0) + 1;
if (Fibers.__rootDeferrals <= 10 || Fibers.__rootDeferrals % 100 === 0) {
console.warn("[wx-asyncify] root-entry-deferred: fiber completion landed inside a "
+ "sleep-wake window; retrying next tick (occurrence "
+ Fibers.__rootDeferrals + ")");
}
__fcsRec("defer root new=" + deferred);
var retry = function() {
if (Fibers.trampolineRunning || Fibers.nextFiber) {
setTimeout(retry, 0); // another switch in flight — wait our turn
return;
}
__fcsRec("defer-retry new=" + deferred);
Fibers.nextFiber = deferred;
Fibers.trampoline();
};
setTimeout(retry, 0);
return;
}
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
// Root is exempt from THIS check only: it "parks" in the main loop's
// yield as a matter of course (quarantining it starved every coroutine
// return — 19 collab e2e reds on the first guard build).
__refuseFiber(newFiber, "is asyncify-parked mid-body (sleep in flight)");
return;
}
if (!isRoot && entryPoint === 0) {
// Suspended-fiber path: about to rewind newFiber+20. (The root is
// exempt: re-entering it with an older suspension is the long-standing
// ghost-resume flow, resolved by libcontext's epoch machinery.)
if (entryPoint === 0) {
// Suspended-fiber path: about to rewind newFiber+20 — root INCLUDED.
// Consume-once semantics are the actual prod killer's cure (all four
// trap stacks, v0.1.1922): each fiber_swap suspension is rewindable
// exactly once. Two fibers completing against ONE root suspension
// epoch (a tool fiber and a collab fiber both waking around
// open:settled) makes the second finishContextSwitch(root) rewind
// already-consumed data → "unreachable executed" → poisoned runtime.
// Refusing the second consumption loses nothing: the fiber that
// yielded stays properly suspended (recorded above), and the root
// continues via its real pending resume (its own sleep wake or the
// next fresh JS entry) — the same contract as libcontext's
// ghost-resume epochs, enforced one layer lower.
if (!Fibers.__validSuspensions.has(newFiber)) {
__refuseFiber(newFiber, "has no live suspension - rewinding would replay stale data");
__refuseFiber(newFiber, isRoot
? "root suspension already consumed - a second rewind would replay stale frames"
: "has no live suspension - rewinding would replay stale data");
return;
}
Fibers.__validSuspensions.delete(newFiber);