Log which channel failed when a test item times out - #77
Merged
Conversation
A test item that had passed 17/17 in 9.8 seconds was reported as having timed out after 3600 seconds, failing a CI leg: https://github.com/JuliaControl/ModelPredictiveControl.jl/actions/runs/32420160289 The test process was healthy throughout. Its stdout kept flowing to the controller — both that item's test summary and the *next* item's are in the run's captured process output — and it disarmed its own hang watchdog normally, meaning `run_testitem` returned and reached the `JSONRPC.send` on the very next line. What went silent was the JSON-RPC socket: every result the process sent after `started(…)` was lost, on a connection whose output pipe was fine, and neither side logged a thing. A test process talks to the controller over two independent channels, and the controller's per-item timer is the sole authority on whether an item is still running. A timeout therefore only ever proves that the *result* never arrived — never that the item is still executing. Which of the two channels went quiet is what separates "the test hung" from "the connection died", and nothing in the log said. The timeout warning now carries that evidence: seconds since the last JSON-RPC message from the process, seconds since its last output, whether the process's own watchdog left a diagnostics dump, and — on a JSONRPC that can report it — how far behind the connection's outbound queue is. On the incident above it would have read seconds_since_last_message = 3600.3 seconds_since_last_output = 5.3 watchdog_dump = false instead of "your test hung". Reconstructing that took the run's artifacts after the fact. No behaviour change: the item is still reported errored exactly as before, and nothing branches on the new timestamps. `outbound_backlog` is reached through `isdefined`, because the compat bound still allows a JSONRPC without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
This CI run failed with a test item reported as a one-hour timeout that had actually passed 17/17 in 9.8 seconds:
The test process was healthy the whole hour. Reconstructed from the run's artifacts:
eaff155creports the previous item passed — its last message the controller ever processedstarted(Luenb. estimator methods), arms the 3600 + 10 s deadlineJSONRPC.send(…, passed, …)Its stdout kept flowing (both test summaries are in the run's captured
process_outputs), and it disarmed its own hang watchdog normally, which meansrun_testitemreturned and execution reached theJSONRPC.sendon the very next line. Nothing was stolen, nothing crashed, nothing restarted. What went silent was the JSON-RPC socket, and neither side logged anything.What this changes
A test process talks to the controller over two independent channels: the JSON-RPC socket carrying every result, and the stdout/stderr pipes carrying captured output. The controller's per-item timer is the sole authority on whether an item is still running, so a timeout only ever proves that the result never arrived — never that the item is still executing. Which of the two channels went quiet is exactly what separates "the test hung" from "the connection died", and nothing in the log said which.
The timeout warning now carries that evidence: seconds since the last JSON-RPC message from the process, seconds since its last output, whether the process's own hang watchdog left a diagnostics dump, and — via
isdefined, since the compat bound still allows a JSONRPC without it — how far behind the connection's outbound queue is (julia-vscode/JSONRPC.jl#111 adds that accessor). On the incident above it would have read:— i.e. the connection died, the test did not. Getting to that conclusion previously meant downloading the run's artifacts and reconstructing the timeline by hand.
No behaviour change. The item is still reported errored exactly as before; nothing branches on the new timestamps. The
_read_diagnosticscall moved above the warning so it can report whether a dump exists, which is the only reordering.Deliberately not in this PR
Making the controller recover — telling "the item hung" apart from "the result was lost" and re-running the item on a healthy process instead of failing it — is what would have turned that run green, but it needs a second source of truth about process progress (a status probe on timeout, a periodic heartbeat, or extending the worker's existing watchdog thread, which already has a file channel that survives both a wedged main thread and a broken connection). That is a separate design question; these diagnostics are what tell us which mechanism to build if it recurs.
Testing
Pkg.test(): 1036 passed, 0 failed, 1 errored, against a baseline onorigin/mainof 1034 passed, 0 failed, 1 errored. The single error is identical in both —ArgumentError: Package Logging not found in current pathattest_worker_lifecycle.jl:235— a pre-existing missing test-target dependency, untouched by this change and worth a separate one-line fix.test_timeout.jlgoes 7 → 17 assertions: the new test item covers the unknown-yet, connection-died and genuine-hang shapes, and that the result splats into the log call the way the handler uses it.One thing worth noting for review:
test_jsonrpc_controller.jlreported 73 assertions on baseline and 65 on this branch. That is not a regression — its count is timing-dependent (it loopsfor n in started … @test, so how many notifications land inside its sleep window varies). Re-running that file alone three times per worktree gave 81/84/68 on this branch and 71/77/75 on baseline, all passing.🤖 Generated with Claude Code