Repository navigation
Description
Bug Description
test_run_timeout_during_metrics_drain_interrupts fails in about 1 in 6 CI test jobs with AssertionError: aggregator must still write a final snapshot. The docstring describes a fast session, after which the 2.5 s run_timeout_s watchdog fires while the aggregator drains a large tokenization backlog. In every CI failure, the watchdog instead fires before the services are ready. The metrics aggregator gets SIGTERM during its own startup and exits without writing final_snapshot.json.
| Test expects | All 30 CI failures show | |
|---|---|---|
| When the watchdog fires | During the metrics drain | During service launch, 2.32–2.40 s after the aggregator is spawned. All 2 services ready is never logged. |
final_snapshot.json |
Written with state == "interrupted" |
Not written. The aggregator dies on SIGTERM before its handler is installed. |
| Test outcome | Pass | pytest.raises(ExecutionError, match="Run timeout") passes, then the assertion at L66 (in _read_final_snapshot, called from L169) fails |
Why it fails
-
confirmed: The 2.5 s budget includes setup and service startup.
run_benchmarksets the deadline beforesetup_benchmarkruns (execute.py#L1294-L1300).- A comment in the sibling test says the budget "must comfortably exceed service + worker startup" (test_run_timeout.py#L120-L124).
- This test sets 2.5 s (L162).
-
confirmed (CI logs): Aggregator startup takes up almost all of the budget. The watchdog fires 2.32–2.40 s after the aggregator is spawned. Two slow steps run before the aggregator installs its SIGTERM handler (
__main__.py#L331-L339) and sends its ready signal (L353-L356):Step Code On the runner Load the tokenizer token_metrics.py#L274 Done 1.21–1.40 s after spawn (all 30 failures) Spawn and warm up a spawn-context shard processtoken_metrics.py#L321-L398 Not finished when the watchdog fired in 29/30. In the 30th it logged setup 1.2sand finished 2.40 s after spawn. -
confirmed: This is the abort path when the watchdog fires before a session exists.
RunWatchdog._firecancels the orchestration task (watchdog.py#L155-L162).launch()'sexcept Exceptiondoes not catch theCancelledError(launcher.py#L127-L151).MetricsPipeline.startcatches it asBaseExceptionand runsterminate_all(pipeline.py#L371-L377, launcher.py#L153-L180), which sends SIGTERM to both services.- The event logger already has a handler and logs the signal. The aggregator has not installed its handler yet, so the default SIGTERM action kills it. In 29/30 failures it was still in shard setup at that point. In all 30, fixture teardown starts 0.21–0.28 s after the watchdog fires, well before the 5 s SIGKILL escalation.
_run_benchmark_asyncthen raisesExecutionError("Run timeout … aborted before a report could be produced")(execute.py#L1045-L1049). That message satisfiespytest.raises, so the test continues to the snapshot assertion and fails there.
-
confirmed (CI logs):
- All 30 failures lack
All 2 services readyandmetrics aggregator received SIGTERM. - 29 of 30 also lack
BatchTokenizer: N shardsand the aggregator'sev_pubsubscription. - In the 30th failure, shard setup finished at
51,115, the aggregator subscribed at51,116, and the watchdog fired at51,117. - The session never started, so the 100 large prompts (test_helpers.py#L37-L43) were loaded but never sent.
- All 30 failures lack
-
confirmed (code): The scenario in the docstring cannot pass on main.
final_snapshot.jsongetsstate == "interrupted"in only two ways. One is the session'sINTERRUPTEDmarker beforeENDED, whichsession.runpublishes in itsfinallyonly ifstop()already ran (session.py#L486-L492). The other is the aggregator's SIGTERM handler (__main__.py#L50-L111).- If the watchdog fires after a normal
ENDED, neither happens. The parent waitsinterrupted_teardown_grace_s(30 s) and then sends SIGKILL (pipeline.py#L409-L425), so the snapshot is eithercompleteor missing. - A pass therefore needs the abort to land after the aggregator installs its SIGTERM handler and before a normal
ENDED.
-
suspected: Whether the test passes depends on runner load, which the test does not control.
- Passing runs capture no output, so they have no timeline.
- Time between consecutive result lines is 3.21–3.32 s for passes and 2.82–2.96 s for failures.
- The failure rate rose from September to October. The cause is unknown.
-
confirmed (local macOS only): After the aggregator is killed during shard setup, its shard worker and the
multiprocessingresource tracker stay alive, reparented to PID 1.
Steps to Reproduce
-
In CI, run the
testjob (uv run pytest -xv -m "not slow and not performance" --cov=src …). Failing examples:Date Trigger Run / job 2026-10-26 push to main (4b385eb) 40344725100 2026-10-27 PR feat(provenance): resolve source git SHA via a validated multi-channel chain #491 40433891117 2026-10-28 PRs Add validation module to serve as backend for submission checker and policies enforcement #544, fix: reject calls without declared tools #547, fix: save datasets selected by run mode #548, fix: decode SGLang streaming responses #550 40705033572, 40721937559, 40721994833, 40722576277 2026-09-29 push to main (0543413) 36644804123 (shard setup finished 2 ms before the watchdog fired) Green runs for comparison: 40722319572, 40344804818.
-
On Linux, run
uv run pytest -xvs "tests/integration/commands/test_run_timeout.py::test_run_timeout_during_metrics_drain_interrupts". Given the mechanism above, it should fail whenever the aggregator has not installed its SIGTERM handler by the time the watchdog fires. This has not been tried on Linux outside CI. -
On macOS the test cannot run unmodified, because
pin_loadgen() requires Linux. Withenable_cpu_affinity=Falseadded to_make_config'sBenchmarkConfig, it failed 3 of 3 times locally with the same signature.
Environment
| Runner | GitHub-hosted, 4 vCPU / 2 physical cores / 1 NUMA node (from the run's CPU-affinity log) |
| Image | ubuntu-24.04 for 24 failures (September to Oct 22). ubuntu-26.04 20261012.163.1 for 6 failures (Oct 26–28). |
| Python | CPython 3.12.11 (uv) |
| Commit | main 4b385eb5 and PR merge refs on it (Oct 26–28). Earlier failures ran on earlier main commits. The test, launcher, pipeline, watchdog, execute, session and metrics-aggregator files are unchanged from 05434130 (Sep 29) to 4b385eb5. |
Relevant Logs
Excerpt from run 40344725100. Runner timestamps are removed and long lines are trimmed.
15:03:32,101 - ...cpu_affinity - INFO - CPU affinity: 2 physical cores across 1 NUMA nodes, requesting 5 for loadgen, 1 workers
15:03:32,271 - ...benchmark.execute - INFO - Loaded 100 samples
15:03:32,275 - ...services.launcher - INFO - Launching service: ...metrics_aggregator (id=0)
15:03:32,275 - ...services.launcher - INFO - Launching service: ...event_logger (id=1)
15:03:32,609 - ...zmq.pubsub - INFO - Subscriber connected to ipc:///dev/shm/zmq_0ocxk4jx/ev_pub_a037df9b
15:03:33,524 - ...metrics_aggregator.token_metrics - ERROR - Failed to compute chat-template baseline for .../tokenizers/char; ...
15:03:34,602 - ...benchmark.watchdog - ERROR - Run timeout reached; aborting run
15:03:34,603 - __main__ - WARNING - event logger received SIGTERM — flushing writers
.../tokenizers/char (Streaming: False): 0%| | 0/100 [00:02<?, ?it/s]
E AssertionError: aggregator must still write a final snapshot
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
==== 1 failed, 33 passed, 157 deselected, 16 warnings in 278.93s (0:04:38) =====
# job 109665184403 (main push, 2026-09-29)
23:26:48,716 - ...services.launcher - INFO - Launching service: ...metrics_aggregator (id=0)
23:26:51,115 - ...token_metrics - INFO - BatchTokenizer: 1 shards across 2 CPUs (setup 1.2s)
23:26:51,116 - ...zmq.pubsub - INFO - Subscriber connected to ipc:///dev/shm/zmq_so7uvv_n/ev_pub_f9c7d40f
23:26:51,117 - ...benchmark.watchdog - ERROR - Run timeout reached; aborting run
The chat-template ERROR is expected, because the char tokenizer has no chat template. Here it only marks the end of tokenizer load. All 2 services ready (launcher.py#L128) is missing from every failure.
Impact
-
Unit tests are skipped. CI runs with
-xand collectstests/integrationbeforetests/unit. This one failure ends the job early: the main-push run stopped after 279 s with1 failed, 33 passed. A green job reports2684 passed, 8 skipped. About 2,650 tests never run until someone re-runs the job, and about 2,570 of them are unit tests. -
Frequency, 2026-09-01 to 10-30: this test failed in 30 of the 191
testjobs that reached it (16%).Period Failed / reached September 18 / 145 October 12 / 46 Pushes to main 7 / 26 -
Recent failures:
- PRs feat: block hardcoded secrets with gitleaks at commit, push, and CI #541 (Oct 21), feat(provenance): resolve source git SHA via a validated multi-channel chain #491 (Oct 27), and Add validation module to serve as backend for submission checker and policies enforcement #544, fix: reject calls without declared tools #547, fix: save datasets selected by run mode #548, fix: decode SGLang streaming responses #550 (Oct 28)
- Pushes to main on Oct 22 and Oct 26
Possible fixes
- Re-budget the test (test-only change).
- Set
run_timeout_swell above service and worker startup, astest_run_timeout_produces_interrupted_reportdoes (6.0 s with a ~120 s workload, test_run_timeout.py#L119-L126). - Make the session long enough that the watchdog always fires mid-session.
- Re-document the test as "abort mid-session with a large tokenization backlog", because point 5 shows the drain case cannot pass.
- Only test_run_timeout.py#L152-L163 changes.
- Set
- Make the documented drain scenario real, if that is the intended contract.
- When an abort lands during the drain, have
drain_and_build_reportsend SIGTERM (terminate_all) before it falls back tokill_all(pipeline.py#L415-L425). The aggregator's handler would then write an INTERRUPTED snapshot (__main__.py#L50-L111). - The test would also need a deterministic trigger, such as aborting when the aggregator reaches DRAINING, instead of a wall-clock deadline that includes startup.
- When an abort lands during the drain, have
- Close the aggregator's pre-handler SIGTERM window.
- Install a SIGTERM handler before tokenizer and shard setup (
__main__.py#L253-L270, token_metrics.py#L360-L382). A SIGTERM during startup would then exit cleanly and terminate the shard workers, which would also avoid the orphaned worker in point 7. - This only makes this test pass if the handler also writes an INTERRUPTED snapshot. That conflicts with the sibling test's comment that a fire before the session exists leaves no mid-run artifacts (test_run_timeout.py#L120-L124), so it needs a design decision.
- Install a SIGTERM handler before tokenizer and shard setup (
Activity