Repository navigation
fix(shell): the job store could crash a supervisor on Windows with WinError 5 (2.118.1) - #157
Merged
Merged
Conversation
…nError 5 (2.118.1) `_write_json_atomic` is the publish step for every c3_shell_job status update. It used one shared temp name and attempted os.replace exactly once, so a supervisor could die before running anything and report "failed before running" for a command that was fine. Visible as an intermittent windows-latest CI failure; the invisible half is that the same race can lose a job on a real Windows box. Three defects: - A shared temp name. `<name>.tmp` is identical for every writer, so the parent creating the job record and the supervisor immediately saving status raced on the temp itself. The suffix is now a pid AND a random token — a pid alone is not enough, because this store is written from a threaded server and threads share a pid. The concurrency test caught that second-order case against the first version of this fix. - No retry. Another process holding a read handle on the target makes os.replace raise transiently; one attempt turned that into a lost write. Four attempts with backoff, and a persistent error still raises. - Orphaned temp files, now cleaned up on every failure path. Same construction as cli/_hook_utils._atomic_write_json, which already had it. The duplication is deliberate (a PreToolUse subprocess imports nothing from services) and is now noted in both. Four of the five new tests fail against the old implementation. Claude-Session: https://claude.ai/code/session_01JjAQC4Uv3aBnv9oQLPMBMy
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.
Follow-up to the intermittent
windows-latestfailure seen on the 2.118.0 merge.What broke
services/shell_output._write_json_atomicis the publish step for everyc3_shell_jobstatus update. On CI:The supervisor died before running anything, and the caller got
failed before runningfor a command that was perfectly fine. The CI flake isthe visible half — the invisible half is that the same race can lose a job on a
real Windows box.
Three defects
A shared temp name.
<name>.tmpis the same string for every writer, sothe parent creating the job record and the supervisor immediately saving
status raced on the temp itself — one mid-write while the other tried to
publish it.
The suffix is now a pid and a random token. A pid alone is not enough:
this store is written from a threaded server, and threads share a pid.
My first version of this fix used
.tmp<pid>and the new concurrency testfailed on it — six threads,
PermissionError×3 andFileNotFoundError×3.Worth stating plainly because
cli/_hook_utilshas the pid-only form and isonly safe there because hooks are separate processes.
No retry. Another process holding a read handle on the target — AV
scanner, Search indexer, a concurrent reader — makes
os.replaceraisetransiently. One attempt turned that into a lost write. Now four attempts
with exponential backoff; a genuinely persistent error still raises rather
than being swallowed.
Orphaned temp files. A failed replace abandoned its temp in the store
forever. Cleaned up on every failure path.
Not a new invention
This is the construction
cli/_hook_utils._atomic_write_jsonhas had sincev2.66 — including a docstring that names "transient Windows sharing violations"
explicitly.
shell_outputsimply never got it. The duplication is deliberate(a PreToolUse hook subprocess imports nothing from
services) and is now notedin both files.
fsyncis deliberately not added here: this store is ephemeral spill state,not durable config, and an fsync per status update is not worth paying.
Verification
Five new tests in
TestAtomicWrite. Reverted the implementation and re-ran toprove they bite:
(The fifth — a persistent
PermissionErrorstill raising — passes either wayby construction; it is there so a future retry loop cannot start swallowing
real failures.)
Full suite on this branch:
ruffclean, 3660 passed, 6 skipped, 0 failed.Known related, deliberately out of scope
Other fixed-temp-name
os.replacesites exist (services/agent_locks.py,services/access_guard._write_scope_config,cli/hub_server,mobile_api).None has a demonstrated failure, and
agent_locksis the one most worth asecond look since it is genuinely concurrent. Left for a separate change rather
than widened into this one.
https://claude.ai/code/session_01JjAQC4Uv3aBnv9oQLPMBMy