Repository navigation
Stop killing processes by bare PID — in the Windows tests, and in Stop-Slot - #124
Conversation
The PowerShell Tests job runs one suite at a time on a fresh VM, which is the one condition under which issue #123's bug cannot show: the suite tracks the processes it spawns as bare PIDs and force-kills all of them on the way out, including PIDs whose process exited long before. Windows hands a freed PID to a new process quickly, so a finishing run kills a *concurrent* run's live slot processes -- and only a concurrent run's. That is why it failed on a developer's machine, with pwsh and 5.1 side by side, and passed in CI every time. Invoke-OverlappingPoolTests.ps1 starts six copies of the suite 20s apart, alternating the two shells -- what reproduced #123 by hand -- and fails if any of them fails. It also fails if the runs never actually overlapped, so a suite that gets fast enough to finish before the next one starts cannot quietly turn this into a check that runs six times and tests nothing. This commit deliberately leaves the suite's PID tracking as it is: the job's first run is the control, standing the bug up in CI before the fix lands on top of it. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaThe diff does not contain the actual fix to
None of these appear in the diff. The diff only adds:
The harness is a good regression-detection mechanism, but it exercises the unfixed test. On If the diff was truncated and the 2. Bugs and logic errorsIn
3. CLI and API safety
4. Test coverageThe new harness is itself the test coverage for the concurrency bug, which is the right idea. However:
5. Suggested follow-up issues
VerdictThe PR adds a valuable concurrency harness but does not contain the fix described in the issue's "Fix (test-only)" section. The acceptance criteria are unmet:
Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
Cleanup force-killed every PID the run had ever recorded, including processes that exited long before (the stand-ins Stop-Slot already killed, and the runner-loop.ps1 slots that exit at once on the empty pat.secret). Windows reuses PIDs, so with two runs of windows_pools_test.ps1 overlapping -- pwsh and 5.1 side by side, or several sessions on one box -- a finishing run killed another run's live processes. Logged while 6 runs overlapped, one Cleanup killed 3 other runs' stand-in slots, 3 unrelated conhost.exe and an unrelated sleep.exe. That is both flakes in #123: killing another run's slot-1 stand-in fails "scale down removes the highest slots and keeps the rest running", and killing its tree-test parent before Stop-Slot gets there makes Stop-Slot see "pid not running", skip taskkill /T, and leave the child alive. Every spawned process is now tracked as a Process object with its handle held open: Windows will not reuse a PID while a handle to it is open, and Stop-TrackedProcesses kills only tracked processes that have not exited. For a PID read back from a .pid file, only a process that started before that file was written is tracked -- a file is all a caller has there, and a process that merely inherited the number cannot have started before it. The stand-ins wait for the test process to exit rather than sleeping a fixed 120s, so a slow run cannot outlast them and a killed run does not orphan them, and both "was it killed" checks use WaitForExit on the held handle rather than Get-Process -Id, which a reused PID also satisfies. Reading a .pid file waits for digits, not just for the file to exist: Set-Content creates it before writing, and an empty read is PID 0, the System Idle Process, which is always running. The rules live in tests\ProcessTracking.ps1 rather than inside the suite that needed them first, so tests\process_tracking_test.ps1 can check them directly. The suite cannot check them itself: the condition that broke it is two runs sharing a machine and a PID handed from one to the other, and no single run can arrange that on demand -- six overlapping runs on a windows-latest VM did not reproduce it either. Also from the overlapping-runs job, and unrelated to PIDs: a check's expected phrase is now matched with whitespace collapsed on both sides. Both shells word-wrap a child's stderr at the host's width, and that width depends on how the suite itself was launched, so redirecting its output to a file split "can't confirm it is idle" across two lines and failed three checks under 5.1 that pass when the same suite is run straight. Fixes #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth ACs are addressed:
The latent problems called out in the issue (empty 2. Bugs and logic errors
if ($proc.StartTime -gt (Get-Item $PidFile).LastWriteTime) { return $null }The comment claims "a process that survives both checks cannot have been replaced between them" — but the handle is opened before the start-time check, and the start-time check reads
function Get-StandInCommand {
param([int]$ProcessId = $PID)
The function is documented as "safe to call more than once," and it is — but the list grows unboundedly across a run. For a test that spawns dozens of processes this is fine; for a long-lived consumer it's a leak. More importantly,
The overlap loop runs after the wait loop, so all
The deadline is shared across all runs, and each 3. CLI and API safetyNo Python, CLI, or GitHub API changes. The new CI job uses One CI concern: the new 4. Test coverage
Missing: a test that The overlapping-runs CI job is the right shape for reproducing #123, but it's inherently probabilistic: 6 runs at 20s stagger may not reproduce on a fast runner. The 5. Suggested follow-up issues
APPROVE — the fix correctly addresses both ACs and the latent bugs; the concerns above are non-blocking (race direction is safe, timeout accounting only bites on pathological runs, and the rest are documentation/robustness). Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
Two things from the review of the previous commit. Invoke-OverlappingPoolTests.ps1 falls back to "now" for a run's end time when the process's own ExitTime cannot be read. "Now" is an upper bound -- always later than the run really ended -- so a pair of runs judged on it can look like it overlapped when it did not, which is the one thing the "nothing was tested concurrently" guard exists to catch. Overlap now has to be shown from an exact exit time. Stop-TrackedProcesses never empties the tracked list, where a comment said it drained it. The code is right and the comment was not: a process it killed still belongs to the run, and a failure on the way out of a run that has already cleaned up has to find the same list, not a shorter one. Also spells out which way Register-SlotPidFile's read of the .pid file's timestamp can race: toward refusing a process, never toward killing one that isn't ours. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
DeepSeek AI Code Review1. Acceptance criteriaBoth ACs are addressed:
The latent problems called out in the issue (empty 2. Bugs and logic errors
3. CLI and API safetyNo CLI or Python API changes. No new dependencies. No GitHub API calls added. The new CI job uses One note: the new job has 4. Test coverage
Missing cases worth noting (non-blocking):
5. Suggested follow-up issues
APPROVE — the fix correctly addresses both ACs and the latent issues; the Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
Every step of it reads something another process is free to take away first: the process it just found may exit, and the .pid file may be gone by the time Get-Item looks at it. Either way there is nothing of ours to clean up, so the answer is the same $null the rest of the function returns -- and a caller tidying up after a failed run is the last place an exception helps anyone. The handle open and the start-time comparison now sit in one try. Also says, where the shared deadline is set, that it is one wait for all the runs rather than a budget each -- what the job's own timeout-minutes has to cover is the staggering plus this, not $Runs times this. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth AC items are addressed:
The latent problems called out in the issue are also addressed: the 2. Bugs and logic errors
3. CLI and API safetyNo Python CLI or public API changes. No new dependencies. No GitHub API calls added. The new CI job uses 4. Test coverage
Gap: there is no test that The overlapping-runs CI job is the right shape for the original bug. The 5. Suggested follow-up issues
APPROVE — the fix correctly addresses both AC items and the latent hazards; the remaining items are non-blocking (comment accuracy, CI action version verification, overlap-detection robustness). Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
The control run, as promised in the description
Word-wrapped mid-phrase, because the suite's own stdout was a file rather than a pipe and the host width differs. Deterministic, nothing to do with PIDs, and it means No PID-reuse symptom appeared at all, in any of the six runs. A fresh CI VM does not churn PIDs the way a working machine does. So this job is a regression guard — it earned its keep by catching the wrap bug — but it is not proof the PID fix works. What checks that directly is On the DeepSeek reviewTaken, in
Not taken:
Generated by Claude Code |
The previous commit said a .pid file rewritten between the two reads could only make Register-SlotPidFile stricter. That was backwards. Reading the contents first and the timestamp second hands back a timestamp from a *later* write than the one that put the PID we are holding there, and that widened window is exactly where a stranger that took the PID sits: it would pass the start-time check and be tracked, and a tracked process is one this run kills. Reading the timestamp first can only bind the PID to an earlier write than its own, and an early timestamp only ever refuses -- which is the direction this guard exists to fail in. Found in review of the previous commit. Also splits the overlapping-runs job's "nothing was tested concurrently" message in two, because the two ways of reaching it call for different things: the runs really did not overlap and the stagger wants lowering, or no run would say when it ended and the question cannot be answered -- still not a pass, but not the same advice. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth AC items from the linked issue are addressed:
The latent problems called out in the issue are also handled: the 2. Bugs and logic errors
3. CLI and API safetyNo CLI changes. No new Python dependencies. No GitHub API calls added. The new CI job uses The new workflow job runs 4. Test coverage
The One gap: 5. Suggested follow-up issues
APPROVE — the fix correctly addresses both AC items, the latent bugs called out in the issue are handled, the new unit test directly exercises the rules, and the CI job is a reasonable regression guard. The remaining items are non-blocking. Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
The new test's "a .pid file naming a process that has since exited is refused" check failed once under 5.1 and passed everywhere else, which is the flake shape. It is the assertion that was wrong, not the rule. The check kills a stand-in and waits for it, but the run still holds an open handle to it -- which is exactly what keeps the PID taken -- so the process object outlives the process, Get-Process can still hand back the corpse, and it did start before the .pid file was written, so Register-SlotPidFile tracked it. Nothing is wrong with tracking a corpse (Stop-TrackedProcesses skips what has exited), but there is no reason to, and saying so makes the rule say what it means: track what there is still something to clean up about. The check is deterministic either way now, and its comment explains that "exited" and "gone" are different things while anyone holds a handle -- the same fact the tracking rules are built on. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth ACs from the issue are addressed:
The latent problems called out in the issue are also covered: the The new 2. Bugs and logic errors
(Get-Item $reusedPidFile).LastWriteTime = $stranger.StartTime.AddSeconds(-5)This back-dates the file's mtime to before the stranger started, which is exactly the condition that should cause refusal — and it does. Fine. But note the real-world race the comment describes ("a .pid rewritten between the two reads") is not actually exercised: the test only simulates the post-hoc state, not the interleaving. That's acceptable given the file's own
But: the parent script itself runs
3. CLI and API safetyNo Python CLI changes. No new dependencies. No GitHub API calls. The new CI job uses One thing: the new job has 4. Test coverage
The one gap: the actual PID-reuse scenario (a PID handed from a dead process to a live one) is not exercised — the test simulates it by back-dating the file's mtime. The file's The 5. Suggested follow-up issues
APPROVE — the fix correctly addresses both ACs, the helper extraction is well-scoped, and the new CI job reproduces the original failure mode. The non-blocking items above belong in the backlog. Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
Same bug as #123, in the fleet rather than the tests, and it is the one that can cost somebody something: a slot's .pid file outlives the process it names -- a crash, a reboot, a runner-loop that gave up -- and Windows gives that number to whatever starts next. Stop-Slot read the bare number, found *a* process, and ran taskkill /PID <n> /T /F on it: the stranger and its children. Get-Process by itself cannot tell the difference, so "the PID in the file is running" was being read as "this slot is running". The overlapping-runs job caught it: in one run, windows-pools.ps1 died mid-shrink with exit -1 -- killed, between signalling one slot and the next -- which is what this looks like from the inside when the number it was about to kill had come round to something of its own. Get-SlotProcess is now the only way to that number. A slot's process is its own only if it started before the .pid file naming it was written, which a process that merely inherited the number cannot have done, and the handle it opens is held from that check through the wait to the kill, so the PID cannot be recycled underneath any of it. Re-querying Get-Process at each step was what left a slot free to exit mid-wait and hand its number on in time to be killed in its place. Test-SlotRunning, Start-Slot's already-running check and `list`'s running count all go through it, so nothing reads a bare PID any more, and a stale .pid is dropped with a line saying which of the two things happened -- gone, or taken over -- because "not running" about a number something else is running under reads like a bug in the tool. The .pid file's own timestamp is read with -Force: a name beginning with a dot is hidden to Get-Item on some hosts, which silently made every slot look stopped. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth ACs are addressed:
The latent problems called out in the issue (PID 0 from empty read, 2. Bugs and logic errors
function Get-StandInCommand {
param([int]$ProcessId = $PID)
return "[System.Diagnostics.Process]::GetProcessById($ProcessId).WaitForExit()"
}
3. CLI and API safety
No new Python dependencies, no CLI signature changes, no GitHub API calls touched. 4. Test coverage
The overlapping-runs CI job is the right shape for reproducing #123, and the script correctly fails when runs don't actually overlap rather than passing vacuously. Missing: no test that Missing: no test for the 5. Suggested follow-up issues
APPROVE — the fix is correct and well-tested; the items above are non-blocking. Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
Stop-Slot read the .pid file again after Get-SlotProcess had already read it, for the pid in its log lines. In a PR about not trusting bare PIDs that is the wrong instinct twice over: the file can be rewritten between the two reads, so the number in "signalled ... (pid N)" could name something other than the process actually being waited on and killed. Every line about a live process now names $proc.Id. The raw contents are read in one place only -- the branch where there is no process to name and what the file says is the only thing to report. Also says, in the tree test, that both processes in the generated script wait on the test process rather than on each other: the child has to outlive its parent or the check cannot tell Stop-Slot killing the tree from the child simply following its parent down. Refs #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC
|
On the latest review round. Taken, in Not taken: the claim that reading
A rewrite under this function can only bind a PID to a write older than its own, and an older timestamp only ever refuses. Worth noting this is the reverse of what the review two rounds ago asked for, which is what moved the code to this order in the first place ( Also not taken, both previously answered: Generated by Claude Code |
GPT AI Code ReviewSkipped: API key not configured The To enable GPT reviews, add
Once the secret is configured, re-run this workflow or push a new commit to trigger a fresh review. REVIEW SKIPPED - API KEY NOT CONFIGURED Reviewed by GPT via gpt-pr-review.yml. Advisory only. |
DeepSeek AI Code Review1. Acceptance criteriaBoth AC items are addressed:
The latent problems called out in the issue (empty 2. Bugs and logic errors
3. CLI and API safety
No new dependencies. The PR is PowerShell-only; no No GitHub API calls. The changes are local process management. Correct.
4. Test coverage
Missing case: Missing case: Missing case: The overlap test itself is well-designed: it fails if runs don't actually overlap, and it distinguishes "did not overlap" from "could not establish overlap". This is the right way to make a concurrency test meaningful. 5. Suggested follow-up issues
APPROVE — The fix correctly addresses both AC items and the latent bugs. The Reviewed by DeepSeek via deepseek-pr-review.yml. Advisory only. |
Fixes #123.
A PID is not an identity. A process dies, the file naming it does not, and Windows hands the number to whatever starts next — so "the PID I recorded is running" and "the thing I recorded is running" stop being the same claim. This PR removes every place that confused the two, in the tests (which is what #123 reports) and in
Stop-Slot(which the new CI job caught on the way).The test fix (#123 as filed)
Cleanupforce-killed every PID the run had ever recorded, most of whose processes had long exited — the stand-insStop-Slotalready killed, and therunner-loop.ps1slots that exit at once on the emptypat.secret. With two runs overlapping, a finishing one killed a live run's slot processes, and that run then failed checks about its own slots.Every spawned process is now tracked as a
Processobject with its handle held open — Windows will not reuse a PID while a handle to it is open — and cleanup kills only tracked processes that have not exited. A PID read back from a.pidfile is trusted only if the process it names started before that file was written. The stand-ins wait for the test process to exit rather than sleeping a fixed 120s, and the "was it killed" checks useWaitForExiton the held handle.The rules live in
tests/ProcessTracking.ps1so thattests/process_tracking_test.ps1can check them directly — 12 deterministic checks, under both shells. The suite itself cannot check them: the condition that broke it is two runs sharing a machine and a PID passing between them, which no single run can arrange on demand.The
Stop-Slotfix (found by the new job)Same bug, in the fleet. A slot's
.pidoutlives its process, andStop-Slotread the bare number, found a process, and rantaskkill /PID <n> /T /F— a stranger and its children.Get-SlotProcessis now the only route to that number: a slot's process counts as its own only if it started before the.pidnaming it was written, and the handle is held from that check through the wait to the kill, so nothing can be recycled underneath any of it.Test-SlotRunning,Start-Slot's already-running check andlist's running count all go through it.The CI job, and what it is worth
Overlapping runsstarts six copies of the suite 20s apart, alternating shells — the condition the existing job cannot create, one suite per fresh VM. It fails if any run fails, and if the runs never actually overlapped.Being straight about what it proved: it did not reproduce #123's PID-reuse symptom. Run against the unfixed suite it failed 3 of 6 — all three from a different, deterministic bug it exposed,
Check's expected phrase being word-wrapped when the suite's stdout is a file rather than a pipe (sopowershell -File tests\windows_pools_test.ps1 > out.txtfails onmaintoday; expected phrases are now matched with whitespace collapsed). Later it caught theStop-Slotkill above, and a flake in the new test file. It is a regression guard that earns its keep, not the proof —process_tracking_test.ps1is the proof.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sz2Nx4sPZaFYv7gaCmMmAC