Skip to content

flaky: recovery-rehearsal "effect 9 after" blocks in audit-dispatch with "exact value read unavailable" under load #874

Open

Description

@blove

What happened

On 2026-09-29, one case failed during a local pnpm test:release-controller run (3950/3952 passed; the 2 failures were this one case and its parent):

test at scripts/release/test/recovery-rehearsal.test.mjs:89:11
✖ effect 9 after (23346.5775ms)
  Error: HTTP recovery rehearsal failed in audit-dispatch; service errors: []; Recovery writer blocked: Recovery observation blocked: exact value read unavailable
      at Object.drive (scripts/release/test/support/recovery-rehearsal.mjs:632:13)
    [cause]: Error: Recovery writer blocked: Recovery observation blocked: exact value read unavailable
        at requireThat (scripts/release/recovery/writer.mjs:41:21)
        at observe (scripts/release/recovery/writer.mjs:237:5)
        at async scripts/release/recovery/writer.mjs:948:25
        at async transaction (scripts/release/recovery/writer.mjs:401:14)
        at async withRecoveryPayloadReuse (scripts/release/recovery/payload-reuse.mjs:225:12)
        at async resume (scripts/release/test/support/recovery-rehearsal.mjs:423:14)
        at async Object.drive (scripts/release/test/support/recovery-rehearsal.mjs:465:28)
  • Branch: the branch of fix(release): let detect resume correlate-audit from an attached successful attempt #867, whose only change is to scripts/release/observe.mjs. That file isn't imported by the recovery path, which uses only scripts/release/recovery/observe.mjs.
  • Machine: macOS, Node 24.20.0, during a full-suite run that took about 17 minutes.
  • Rerun on its own, node --test scripts/release/test/recovery-rehearsal.test.mjs passed 70/70. Two later full-suite runs (3959/3959 and 3975/3975) didn't reproduce it, and it hasn't been seen in CI.

Likely cause (not confirmed)

The message comes from readerContext in scripts/release/recovery/observe.mjs (around line 107-130): envelope() requires runRecoveryRead(...) to return PRESENT. That reader uses a real wall-clock phaseDeadline = Date.now() + RECOVERY_RETRY.phaseDeadlineMs and a real setTimeout sleep, not the rehearsal's controlled clock. The case took 23s. With the test's concurrency: 2 and the whole suite competing for the machine, a read against the local HTTP rehearsal server probably ran past the deadline or used up its retries, so it came back non-PRESENT. resume() in the rehearsal only retries errors matching /uncertain|stopped/, so "read unavailable" failed the case.

This fits the load-sensitive pattern in #535 / #715 (flakes that reproduce under process-spawn contention, not CPU spin).

Suggested next steps

  • Try to reproduce by running recovery-rehearsal.test.mjs next to process-spawn-heavy load (as in test(k8s-compat): size the docker smoke watchdog for a wedge, not a stopwatch #715), and log the underlying runRecoveryRead result (status/code), which the current error message drops.
  • If it's the deadline, either inject the rehearsal's clock into readerContext, or give the rehearsal a longer phaseDeadlineMs. Don't loosen what production accepts.
  • Keep the diagnostic regardless: include the envelope's status/code in the exact ${key} read unavailable message, so the next occurrence says why.

🤖 Generated with Claude Code

Activity

  1. blove commented on Sep 30, 2026

    @blove
    ContributorAuthor

    Findings from #877. I'm leaving this issue open because the natural trigger is still unconfirmed.

    • The phase deadline can't explain it. phaseDeadlineMs is 20 minutes. The limits a single observe read can hit in real time are the 15s per-attempt race timer and the 15s HTTP timeout.
    • The mechanism is confirmed. Holding one loopback listReleaseAssets GET in audit-dispatch for 16s during effect 9 after reproduces the reported message exactly. The underlying envelope was ERROR READ_TIMEOUT_UNSETTLED. The effect-9 fault had already been resumed in five-lanes, so resume() could not retry again.
    • The natural trigger is not reproduced. I ran the test under ten spawn loops, the test(k8s-compat): size the docker smoke watchdog for a wedge, not a stopwatch #715 recipe, with load average around 147. Cases slowed to 50–120s, but no observe read came back non-PRESENT. Two triggers are still possible:
      • a real loopback stall of 15s or more, which would give READ_TIMEOUT_UNSETTLED;
      • a wall-clock jump such as laptop sleep. Date.now deadlines count sleep time but the test runner's monotonic duration does not, which could explain a 23s case. That would give RECOVERY_DEADLINE or TIMEOUT.
    • What test(release): run recovery rehearsal observation reads on an injected clock #877 changes. The error now names the envelope's status, code and operation, so the next occurrence will say which trigger it was. The rehearsal now runs observe reads on an injected fixture clock, with a 100s read-timer floor.
    • Still exposed. The writer and audit runRecoveryRead calls still use a real 15s race timer.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions