Skip to content

fix(storage): stop a stall verdict vouching for the connection it stalled on (DAB-1177) - #33

Open
matalina wants to merge 1 commit into
masterfrom
dab-1177-stall-guard-review-fixes
Open

fix(storage): stop a stall verdict vouching for the connection it stalled on (DAB-1177)#33
matalina wants to merge 1 commit into
masterfrom
dab-1177-stall-guard-review-fixes

Conversation

@matalina

@matalina matalina commented Sep 1, 2026

Copy link
Copy Markdown
Member

Follow-up to #32, which merged before these two landed. Both were caught by @jacwright in review there, and both are real.

Neither is reachable at the shipped defaultstransactionTimeout is 0, so nothing arms the hard timer. That is why they were correctly flagged non-blocking. They bite the moment it is enabled, which is DAB-1177 step 2.

1. A stall verdict vouched for the connection it stalled on

The timeout path called settleReject, which runs the wrapped settle and stamps lastSettleAt — so a guard firing counted as evidence the connection was alive, for every other guard on it.

The abort made it worse than it first looks: abortTarget provokes an abort that arrives back through the ordinary onabort handler and stamped a second time.

On a wedged connection with several operations in flight — the exact case this guard exists for — each timeout vouched for the remaining ones and they kept deferring, up to MAX_DEFER_WINDOWS apiece. Bounded, so never a hang, but it delayed precisely the verdicts the guard is meant to deliver.

Now a timedOut flag, set before the abort so neither the rejection nor the abort it provokes stamps. Only genuine settles vouch.

2. Late-fire misread ordinary scheduling lag as a suspended tab

The heuristic compared elapsed time against the window actually scheduled. After a defer that window is only the remainder of a budget and can be a few milliseconds, so budget * LATE_FIRE_FACTOR was trivially cleared by event-loop lag on a main thread busy with bulk IndexedDB work — misreading jitter as suspension, spending the single re-arm on a full extra window, and stamping lateFire: true on an error measured wide awake. That last part is the damaging one: lateFire is the flag the duration stats get filtered by, so a wrong one quietly corrupts the measurement this whole feature exists to produce.

Took both suggestions from the review rather than either: an absolute LATE_FIRE_FLOOR_MS = 500 and measuring against the full hardBudget, factored into a shared firedLate().

This also removed a latent flake in the existing suite — reports a slow transaction used a 20ms budget and asserted lateFire === false, which 25ms of jitter would have flipped.

budget became write-only after the change and is gone.

Testing

tsc --noEmit   clean
npm test       46 passed | 3 skipped   (was 43)

Both fixes carry a regression test. I verified they are not vacuous by reverting both fixes and re-running — exactly those two fail, and pass again once restored.

…lled on (DAB-1177)

Review follow-up to #32, which merged before these landed. Neither bug is
reachable at the shipped defaults — `transactionTimeout` is 0, so nothing arms
the hard timer — but both bite the moment it is enabled.

A guard's own timeout stamped `lastSettleAt`, so a stall counted as evidence
the connection was alive for every sibling guard on it. The abort the timeout
provokes made it worse: it arrives back as an ordinary `onabort` and stamped a
second time. On a wedged connection with several operations in flight — the
case this guard exists for — each timeout vouched for the rest and they kept
deferring, up to `MAX_DEFER_WINDOWS` each. Bounded, so never a hang, but it
delayed exactly the verdicts the guard is meant to deliver. Only genuine
settles vouch now.

The late-fire heuristic also compared against the window actually scheduled.
After a defer that is only the remainder of a budget and can be a few
milliseconds, so ordinary event-loop lag on a main thread busy with bulk
IndexedDB work cleared it — misreading jitter as a suspended tab, spending the
single re-arm, and stamping `lateFire` on an error measured wide awake, which
corrupts the flag the duration stats are filtered by. Now measured against the
full budget and required to clear an absolute floor as well.

`budget` became write-only and is gone.

Both fixes carry a regression test, each confirmed to fail without its fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant