Bug 61
CI jobs killed when too many slow IPC tests ran at once
- Status
- Fixed
- Symptom
- Crash; worst effect seen: other
- Where seen
- Bitcoin Core functional tests
- Trigger
- Normal use; no special event is needed
- First reported
- 2026-03-25 (first report)
- Fixed
- 2026-03-26
- Fix
- mp#264
- Shipped to Bitcoin Core
- bitcoin#34804, bitcoin#34952
Summary
The CI jobs in the libmultiprocess repository that build Bitcoin Core with the library change and run its tests were killed with SIGTERM (exit code 143) a few minutes into running Bitcoin Core's IPC functional tests, on every PR and on master, for about a day. They ran up to ten times as many parallel test processes as CPUs, and once mp#263 raised the test timeout factor from 4 to 40, dozens of slow interface_ipc_mining.py runs piled up at once. This was a CI configuration problem and did not affect users. Fixed in mp#264 by lowering the parallelism and run counts.
What happened
- Starting with the CI run of mp#263 itself, the "Run IPC functional tests" step of the ASan+UBSan and TSan jobs died with
Error: Process completed with exit code 143roughly two to six minutes in. @Sjors reported it there (2026-03-25, "Resource exhaustion?"). - @maflcko saw it independently on mp#256, a CI caching change (2026-03-26), with the runner log line "The runner has received a shutdown signal".
- ryanofsky collected ten killed jobs within a day, on the open PRs mp#240 (request cancellation), mp#249 (disconnect race fixes) and mp#256, and on master. The failures blocked CI for every open libmultiprocess PR until the fix, mp#264, merged.
Root cause
.github/workflows/bitcoin-core-ci.yml, added in mp#253 to run some Bitcoin Core CI jobs in the libmultiprocess repository, ran test_runner.py with --jobs set to nproc times nproc_multiplier, where the multiplier was 10 (5 on macOS), and repeated the two IPC tests 200-300 times. interface_ipc.py takes 1-3 s but interface_ipc_mining.py about two minutes, so as the cheap runs drained from the queue the runner ended up running about 40 copies of the expensive test at once. mp#263 raised the timeout factor from 4 (10 on macOS) to 40, so slow runs that had failed quickly now kept running, and the step was killed. What sent the SIGTERM was never pinned down; ryanofsky said "I'm still not sure what would actually be sending the sigterm signals". Resource exhaustion on the GitHub-hosted runner is the working explanation.
The over-subscription dates from mp#253; mp#263 is what made it fail reliably.
Fix
336023382c4c (Sjors) lowers nproc_multiplier to 2 on all jobs and the functional test runs to 20-25 (+6/-6). Sjors reported that it then ran for an hour without failing. ryanofsky proposed running the cheap and expensive tests with separate job counts, or a --repeat-for mode in the test runner, as a follow-up.
Timeline
- 2026-03-25: introduced by 09f10e5a5984 ("ci: bump timeout factor to 40"), merged in mp#263; first kill on that PR's own CI run.
- 2026-03-26: second report on mp#256; fix merged in mp#264.
- 2026-03-30: carried into Bitcoin Core's subtree by bitcoin#34804; 2026-04-01: 31.x backport bitcoin#34952.
Lessons
What would have prevented this bug, found it sooner, or made it easier to debug. The patterns page groups these lessons across all bugs.
Pattern CI, toolchain and platform: A CI job at the edge of its limits
- Prevented by a design rule
- Earliest detector other (existed then)
- Diagnosis moderate
- Fix design easy
- Debugging aids CI artifacts, logging
- Rust n/a
Would Rust have prevented it? Not applicable. CI configuration; no code involved. More on Rust
Could it have been prevented?
Partly. The workflow from
mp#253 sized its
parallelism for the cheap test (--jobs at ten times nproc, hundreds of repeats)
while the queue also held interface_ipc_mining.py, which runs about two minutes and
is memory-heavy; once the short runs drained, some forty copies of the long one ran at
once. A rule "size a runner's job count to the heaviest test in the queue, not the
average" would have set the multiplier at two from the start, which is where
336023382c4c
put it. It was not obvious in advance: with the original timeout factor of 4 the slow
runs failed fast and the queue never filled, so the configuration looked fine for
weeks, and it only tipped when
mp#263 raised the factor
to 40 to chase a different failure.
Could it have been found sooner?
It was found as soon as it existed: the first exit 143 was on mp#263's own CI run, Sjors reported it the same day, and the change merged anyway because the kill looked like unrelated runner trouble. The detector that mattered was the workflow itself; what was missing was treating a new failure mode on a PR's own run as a reason not to merge. A run of the workflow at the new factor before merging, watched for resource use rather than pass/fail, would have shown the pile-up.
What made it hard to debug?
The failure was a bare Process completed with exit code 143 two to six minutes into
the step, with no message from whatever sent the signal; maflcko's
sighting
added the runner log line "The runner has received a shutdown signal", which says the
host stopped the job but not why. ryanofsky
collected ten jobs
across four branches within a day and worked out from their timing and the test
durations that the expensive test was accumulating, but wrote that he was "still not
sure what would actually be sending the sigterm signals"; resource exhaustion was the
working theory and Sjors's hour-long run of the reduced configuration was the
evidence it was right. A memory or load graph from the runner, or a test-runner
option that caps the number of concurrent copies of one test, would have turned a
day of cross-PR correlation into a glance. The failures the higher factor was meant
to fix were a test race that no factor could fix (see
mp#253), so the change
that caused the kills bought nothing.
Lesson
When a CI job repeats a mix of cheap and expensive tests, the steady state is all expensive ones running at once; set concurrency for that case, and when a PR's own run dies in a new way, treat it as the PR's problem until shown otherwise.
Notes
- mp#263 raised the timeout factor to prevent failures where
interface_ipc.pywaited for a "canceled while executing" log line. Those failures were a race in the test, where the line is never written, so a longer timeout could not have fixed them; with factor 40 each such failure waited longer, which may have added to the load.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | crash |
| Worst effect observed | other |
| Seen in | functional-test |
| Scope | ci-infra |
| Trigger | Normal use (ordinary-path) |
| Phase | any |
| Side | n/a |
| Component | ci |
| Root cause kind | assumption-violated |
| Fix kind | config |
| Detection | ci-deterministic |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | unknown |
| Confidence | medium |