Repository navigation
test(integration-tests): stop Python reaper flake on a slow removal - #521
Merged
Merged
Conversation
…y port The Python SIGKILL test failed once on hosted CI with a container still listed 45 seconds after its process died. The reaper test in every port had the same bound. Removal can be correct and still take longer than that. dockerd cannot report a container's exit until its overlay unmount has synced the whole underlying filesystem, so the container stays listed until the host's dirty pages reach the disk. Sampled kernel stacks show dockerd waiting in ovl_sync_fs -> sync_filesystem -> folio_wait_writeback. Measured on a capped daemon with a 256 MiB dirty backlog: 34-38s at 12 MB/s and 67-76s at 6 MB/s, the same on Ryuk 0.8.1 and 0.14.0, with no container left behind. With no backlog at 6 MB/s it took under 11s. On Ryuk 0.14.0 the unmodified test fails at 45s with the hosted assertion and Docker status `Up`, and passes at 120s. Every port's bound is now 120s, and every failure message carries Docker's status, because the hosted failure gave none. Python also moves off the testcontainers package to a private Ryuk 0.14.0 that waits for its ACK, as the TypeScript helper does. The package's Ryuk 0.8.1 tries each removal once and discards the error: a fault proxy that refused one removal left the container running for good while Ryuk logged that it had removed it. That defect is proven by fault injection only. No real load produced it, so it is a separate fix, not the explanation of the hosted failure. The hosted failure itself was not reproduced on hosted hardware. The real suite's dirty backlog peaked at 153 MiB in the lab, below what the lab needed to reach 45s, so the hosted cause remains inferred. The new failure messages will show it if it recurs.
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.
What Changed
server/python/tests/integration/reaper.py) now starts its own Ryuk0.14.0and waits for Ryuk's ACK, as the TypeScript helper does. Thetestcontainersdependency leaves theintegrationextra (pyproject.toml,uv.lock).test_reaper_kill.pyadds a refused-removal test, in which a Docker API proxy fails the first container removal, and two socket tests for the ACK. Its failure message also includes Ryuk's log.Cause
Verdict: a test problem in all five ports, plus a separate reaper gap in Python only.
dockerdcannot report a container's exit until its overlay unmount has synced the whole underlying filesystem. Sampled kernel stacks showeddockerdinovl_sync_fs -> sync_filesystem -> folio_wait_writebackin 24 of 30 samples between the kill and the container's exit.Upwhile removal is still in progress. Removal then succeeds.The hosted cause is inferred, not reproduced on hosted hardware. The hosted failure recorded no Docker status and no Ryuk log. The real suite's dirty backlog peaked at 153 MiB in the lab, below the 256 MiB the lab used to pass 45s, and hosted disk speed and dirty settings are unknown. If it recurs, the new messages will show it: a status of
ExitedorDead, or a Ryuk log with no removal attempt, would point away from slow exit.Evidence
Time from SIGKILL to removal on a capped Docker daemon with a 256 MiB dirty cap and a continuous writer:
Up, and passes at 120s in 90s.The Python gap is separate. The
testcontainerspackage runs Ryuk 0.8.1, which tries each removal once, discards the error and never has its ACK read. A proxy that refused one removal left the container running for good while Ryuk logged it as removed; Ryuk 0.14.0 retried and removed it. This is proven by fault injection only and never appeared under real load, so it is not offered as the explanation of the hosted failure. The other ports already ran Ryuk 0.14.0 with an ACK.Slow removal has no regression test, because reproducing it needs a throttled daemon and a sustained backlog that CI cannot host. The bound's comments carry the measurements instead.
Gate
This took one no-mistakes run. Review found one issue, now fixed: the held-open test asserted only that a connection was returned. The Test step passed with an approved exception, because 2 of 4 live scenarios were driven against Docker. The two ACK tests are deliberately Docker-free unit tests, and real Ryuk's ACK is exercised by the live SIGKILL and retry tests, because the reaper waits for it before any container starts. A mutant check confirmed the held-open test can fail.
Validation
Risk Assessment
✅ Low: Test-only and test-harness changes; the reaper replacement, ACK wait, widened bounds, status-bearing messages and the fixed held-open test all behave correctly on trace, with the inferred-cause caveat already recorded.
Testing
Drove two reaper scenarios live against the local Docker daemon: a SIGKILLed child's container is removed by Ryuk, and a removal that fails once is retried until removed. Both pass. The two Ryuk registration scenarios ran against an in-process fake socket server, not Docker. They pass, and a mutant that closes the connection before returning is caught by the peer-side check, but they are not live product results. The full Python suite passes with xdist (4456 passed, 11 skipped), excluding reaper_kill, which ran separately. The flake root cause from the intent (why the container outlived its process on hosted CI) was not reproduced or confirmed here.
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
server/python/tests/integration/test_reaper_kill.py:215-test_a_connection_ryuk_acknowledged_is_held_openclaims the connection is held open but asserts onlyconn is not None. The session lifetime depends on that connection staying open. An implementation that closed the socket before returning it (for examplewith socket.create_connection(...) as conn:in_register) still passes, although Ryuk would then prune the session 10s later. Public behaviour:_registerreturns a live connection after ACK. Oracle: Ryuk's protocol, where closing the last connection starts the prune. Fix: assert the socket is still open, such asconn.fileno() != -1, or have_one_shot_serverconfirm that it sees no EOF.server/python/tests/integration/test_reaper_kill.py:44- The hosted failure's cause stays inferred, and the commit message says so: not reproduced on hosted hardware, and the lab's real-suite backlog peak of 153 MiB was below what reached 45s. I traced the candidate paths. Slow removal is covered by the 120s bound in all five ports. A single failed removal is covered by Python's move to Ryuk 0.14.0 with the fault-injection test. An unregistered session is covered by the ACK wait. Java (Testcontainers 2.0.5) and C# (4.16.0) already run Ryuk 0.14.0, so no sibling port keeps the one-try gap. A removal slower than 120s can still fail, and the new messages then show Docker's status. The PR summary should carry the 'inferred' caveat.🔧 Fix applied.
1 info still open:
server/python/tests/integration/test_reaper_kill.py:44- The hosted failure's cause stays inferred, and the commit message says so: not reproduced on hosted hardware, and the lab's real-suite backlog peak of 153 MiB was below what reached 45s. I traced the candidate paths. Slow removal is covered by the 120s bound in all five ports. A single failed removal is covered by Python's move to Ryuk 0.14.0 with the fault-injection test. An unregistered session is covered by the ACK wait. Java (Testcontainers 2.0.5) and C# (4.16.0) already run Ryuk 0.14.0, so no sibling port keeps the one-try gap. A removal slower than 120s can still fail, and the new messages then show Docker's status. The PR summary should carry the 'inferred' caveat.scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainscd server/python && .venv/bin/python -m pytest tests/integration/test_reaper_kill.py -k "ack or never" -q (2 passed, no Docker)Mutant check: _register replaced by a version that closes the socket before returning; test_a_connection_ryuk_acknowledged_is_held_open fails with AssertionError, and the peer-side state is "eof"uv sync --all-extras --all-groups in server/python (repo-local .venv, to install fastapi and other extras that were missing).venv/bin/python -m pytest tests -q -n 4 -k 'not reaper_kill' (4456 passed, 11 skipped).venv/bin/python -m pytest tests/integration/test_reaper_kill.py (4 passed, live Docker, Ryuk reaper)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.