Skip to content

test_ui_dr_outlive fires the request deadline inside the drain, not on wall time (BACKLOG #3871) - #2438

Merged
wshallwshall merged 3 commits into
mainfrom
b3871-dr-outlive-deflake
Oct 20, 2026
Merged

wshallwshall merged 3 commits into
mainfrom
b3871-dr-outlive-deflake

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Vault BACKLOG #3871, PARTIAL: makes packaging/messagefoundry-webconsole/tests/test_ui_dr_outlive.py::test_a_console_release_cut_off_by_the_deadline_still_completes deterministic. It failed three times on 2026-10-19 on three unrelated PRs, and two of them (2436, 2435) were evicted from the merge queue. Own PR, so it can land ahead of the rest (the Lander asked for this). Test file only; no production code changed.

The race

The request deadline is wall time from the start of the request. When the auth dependencies (session, MFA, step-up) took longer than 0.2 s, the 503 came before the handler reached outliving.run. Nothing was registered, the wait on outliving_operations.inflight returned at once, and coord.active was still True.

Evidence:

  • The failed ubuntu job log (117814912890) holds only the request-timeout warning, never outlive.run's cancellation warning.
  • The original test failed 1 in 200 serial runs locally, and 50 of 50 with the first step-up check slowed by 0.5 s.

This is a test defect, not a product defect. A deadline that fires before the release starts leaves the box active with nothing half done, which is correct.

The fix

A stand-in for asyncio, patched into messagefoundry.api.request_timeout only, records the deadline the middleware opens. The release runs under a 30 s deadline, and the held drain reschedules that real deadline to "now", so the middleware's own TimeoutError path answers 503 mid-drain, whatever auth costs. The test then checks:

  • the exact 503 body;
  • inflight == ["DR release"];
  • coord.active is still True;
  • no dr_release_failed row.

It then releases the drain and waits with outliving.drain(timeout=10) == [] (repeated in a finally). Every post is bounded with wait_for. The final assertions are unchanged.

Proof

Condition Before After
serial, 200 runs 1 failed 0 failed
960 runs at -n 96 0 failed (no repro under load) 0 failed
first step-up slowed by 0.5 s 50 of 50 failed --
every step-up slowed by 1.0 s -- 0 of 50 failed

Mutations, each temporary and reverted, all fail fast:

  • outliving.run awaits the task directly;
  • dr_release skips outliving.run;
  • the release never clears _active;
  • the middleware never arms the deadline.

Checks

  • ruff and format: clean.
  • mypy linux 361, win32 342, tests 1389, plus --strict on the changed file: all clean.
  • pytest: the web console suite gives 1096 passed and 3 skipped, at -n auto and at -n 4; the 27 engine files that reference DrCoordinator give 1811 passed and 15 skipped.
  • The Builder's git merge-tree --write-tree origin/main 19ae348fa exited 0 at main cc24a84, with a self-merge control at 0.

Open findings (code review at xhigh, 2 rounds)

  1. PARTIAL: row #3871 covers four tests. Three still race wall time and are untouched: tests/test_dr_outlive_deadline.py, tests/test_reload_cancellation.py, and tests/test_upload_api.py (_settle). _DeadlineCapture, with a reschedule from the held operation, is the reusable shape for a follow-up. The row stays open.
  2. No third review ran on the final rewrite 19ae348fa, because of the two-round cap.
  3. Mutation 3 fails with the same assertion text the flake had. That is correct, because in that mutation the release really did not finish.

Proposed banner

PARTIAL -- test_ui_dr_outlive fires the middleware's real deadline from inside the release's drain, so it no longer races wall time (BACKLOG #3871). Three sibling tests remain.

…he release (BACKLOG #3871)

The console DR release test set a 0.2 s request deadline and asserted the
release finished once outliving_operations.inflight was empty. The deadline
is wall time from the start of the request, so when the auth dependencies
alone outlasted 0.2 s the 503 came before the handler reached
outliving.run: nothing was ever registered, the wait loop exited at once,
and coord.active was still True. The failed CI log shows only the
request-timeout warning, never outlive.run's "caller was cancelled" one.
Reproduced 1 in 200 serial runs locally, and every time with the first
step-up check slowed by 0.5 s.

The drain now blocks on an event until the 503 is in, so it cannot finish
first. An attempt that registered nothing (and never reached the drain)
is retried with twice the deadline, up to 6.4 s, then fails loudly. The
test asserts the release is still running when the deadline answers, then
lets the drain end and waits for the operation itself.

No product change: a deadline that fires before the release begins leaves
the box active with nothing half done, which is correct.

Proposed PR title: test_ui_dr_outlive waits on the coordinator, not wall time (BACKLOG #3871)
… failed-release row (BACKLOG #3871)

Code-review round 1 on the previous commit:
- each post is bounded (deadline + 10 s), so a deadline that never fires
  fails in about 10 s instead of hanging the session on the held drain;
- every 503 must carry the middleware's "timed out" body, so a handler 503
  is not mistaken for a deadline that beat the release;
- no dr_release_failed row on a retried attempt, after the cut-off, or at
  the end, so a cancel that reached the release cannot pass as the flake;
- the drain is released in a finally, so a failure does not park the
  release on the session-scoped loop;
- the final wait polls the outcome (coord inactive) as well as the registry.

Proposed PR title: test_ui_dr_outlive holds the drain and retries a deadline that beat the release (BACKLOG #3871)

Proposed ledger banner for #3871: PARTIAL -- the console test
(test_ui_dr_outlive) is fixed; the mechanism is "before the work is
registered": the deadline fired in the auth dependencies, before the
handler reached outliving.run. tests/test_dr_outlive_deadline.py,
tests/test_reload_cancellation.py and tests/test_upload_api.py carry the
same wall-clock race and are not changed here; the row stays open.
…ain (BACKLOG #3871)

Code-review round 2: the retry ladder only lowered the odds of the race.
The test now swaps asyncio for a capturing stand-in inside the deadline
middleware's module only, opens the release request under a 30 s
deadline, and the held drain reschedules that real deadline to now. The
middleware's own TimeoutError arm answers 503 while the release sits in
its drain, in one attempt, whatever the auth dependencies cost. The 503
body is compared exactly; outliving.drain(timeout=10) replaces the poll
and runs again in the finally; audit reads filter by action in SQL.

Verified: every step-up slowed to 1.0 s passes 50/50; 200 serial and 960
at -n 96 pass; four production mutations (run not outliving, route
bypassing it, release never deactivating, deadline never armed) each fail.

Proposed PR title: test_ui_dr_outlive fires the request deadline inside the drain, not on wall time (BACKLOG #3871)

Proposed ledger banner for #3871: PARTIAL -- the console test
(test_ui_dr_outlive) is fixed; the mechanism is "before the work is
registered": the deadline fired in the auth dependencies, before the
handler reached outliving.run. tests/test_dr_outlive_deadline.py,
tests/test_reload_cancellation.py and tests/test_upload_api.py carry the
same wall-clock race and are not changed here; the row stays open.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

QA for head 19ae348 (the Builder's line, posted by the Manager): QA -- korus roles/BUILDER.md step 11 / Level: xhigh, from the brief. Tag: none printed (JSON findings only, both rounds). / Rounds: 2. Findings: round 1 raised 9, round 2 raised 9, all fixed except 3 open, listed in the PR body (PARTIAL: three sibling tests remain; no third review of the final rewrite; mutation 3's assertion text).

@wshallwshall wshallwshall added the qa Builder QA record posted; not a merge gate label Oct 19, 2026
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Lander: enqueued at 19ae348, pinned, ahead of other work, because this flake evicted engine PRs 2436 and 2435 from the merge queue on 2026-10-19.

  • Review: the QA line names this head (code-review at xhigh, 2 rounds).
  • This PR changes one test file and no production code.
  • Its three open findings sit inside vault #3871's own scope: the three sibling tests that still race wall time, and the review-cap note. #3871 stays open as PARTIAL.
  • All 9 live required contexts are SUCCESS on this head.

@wshallwshall
wshallwshall added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 1b22ec8 Oct 20, 2026
50 checks passed
@wshallwshall
wshallwshall deleted the b3871-dr-outlive-deflake branch October 20, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa Builder QA record posted; not a merge gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant