Skip to content

fix: stop treating a Windows sharing collision as a lost job record - #9

Merged
packetloss404 merged 1 commit into
mainfrom
fix/windows-record-sharing-violations
Sep 5, 2026
Merged

fix: stop treating a Windows sharing collision as a lost job record#9
packetloss404 merged 1 commit into
mainfrom
fix/windows-record-sharing-violations

Conversation

@packetloss404

Copy link
Copy Markdown
Owner

TestResubmit_SpawnsNewJobAndLinksBothWays was flaky on Windows CI. The cause turned out not to be the test.

The bug

Windows opens deny by default, and Go's os.ReadFile asks for FILE_SHARE_READ|FILE_SHARE_WRITE but not FILE_SHARE_DELETE. So:

  • while any reader holds a job record open, the rename that publishes a new version of it fails with ERROR_ACCESS_DENIED (5);
  • while that rename is in flight, a reader fails with ERROR_SHARING_VIOLATION (32).

Both mean "try again in a moment" — which is exactly what POSIX does implicitly by letting the rename proceed under an open handle. Both were treated as permanent.

Measured with one reader and one writer contending on a single path:

attempts failures
os.Rename over an open file 2000 809 (errno 5)
os.ReadFile during a rename 857 (errno 32)

Why it was silent

Neither failure was reported anywhere:

  • every persist site discards its error (_ = m.savePersistedSnapshot...), so a terminal state that failed to write is simply lost;
  • decodeRecordFile turns a failed read into an UnreadableRecord, and loadPersistedJobs drops those from its results.

So a perfectly good job record vanished from a reload and was described as malformed. This is a real durability bug on Windows, not only a test problem.

The fix

atomicfile waits the collision out — ten attempts, 10 ms apart, and only for those two errnos, so a file genuinely held open by something else (an editor, a scanner with a long lease) still fails and says so. isShareViolation is false away from Windows, so those loops run exactly once there: same syscalls, no sleep. atomicfile.ReadFile is the read half; the job record readers use it.

Evidence

The regression test contends a reader and a writer on one path:

  • with the retry: passes 5/5
  • with isShareViolation forced false: fails 3/3, reporting both directions (rename: Access is denied, open: The process cannot access the file because it is being used by another process)

The test's own smaller fault

It waited for Manager.Get to report terminal, then read the record off disk. markTerminalCause flips the in-memory state under the manager lock and persists after releasing it, so that wait did not mean what the read needed it to mean. It now waits on the record itself — via the read-only reader, not the loader that would rewrite what it polls — and asserts nothing came back unreadable. That discarded return is why the failure only ever said "map does not contain ".

Checks

  • go test ./... green; -count=30 -run TestResubmit under 3 concurrent full-suite runs, twice, clean (~105 iterations total).
  • go vet ./... and golangci-lint (--max-issues-per-linter=0 --max-same-issues=0) under GOOS=linux, darwin, windows: 0 issues.

🤖 Generated with Claude Code

TestResubmit_SpawnsNewJobAndLinksBothWays was flaky on Windows CI. The
cause turned out not to be the test.

Windows opens deny by default, and Go's os.ReadFile asks for
FILE_SHARE_READ|FILE_SHARE_WRITE but not FILE_SHARE_DELETE. So while any
reader holds a job record open, the rename that publishes a new version
of it fails with ERROR_ACCESS_DENIED; while that rename is in flight, a
reader fails with ERROR_SHARING_VIOLATION. Both mean "try again in a
moment" -- which is what POSIX does implicitly by letting the rename
proceed under an open handle -- and both were treated as permanent.
Measured with one reader and one writer on a single path: 809 of 2000
renames failed with errno 5 and 857 reads failed with errno 32.

Neither failure was reported. Every persist site discards its error
(`_ = m.savePersistedSnapshot...`), so a terminal state that failed to
write is simply lost; and decodeRecordFile turns a failed read into an
UnreadableRecord, which loadPersistedJobs drops from the results
entirely. A perfectly good job record therefore vanished from a reload
and was described as malformed.

atomicfile now waits the collision out: ten attempts, ten milliseconds
apart, and only for those two errnos, so a file genuinely held open by
something else still fails and says so. isShareViolation is false away
from Windows, where the situation cannot arise, so the loops there run
exactly once -- same syscalls, no sleep. atomicfile.ReadFile is the read
half, and the job record readers use it.

The regression test contends a reader and a writer on one path. With the
retry it passes 5/5; with isShareViolation forced false it fails 3/3,
reporting both directions.

The test had a second, smaller fault of its own: it waited for
Manager.Get to report a terminal state, then read the record off disk.
markTerminalCause flips the in-memory state under the manager lock and
persists after releasing it, so that wait did not mean what the read
needed it to mean -- and reading inside the window found the successor
still running, which sent the loader down its reconcile-and-rewrite path
against a file the manager was writing at that moment. It now waits on
the record, via the read-only reader rather than the loader that would
rewrite what it polls, and asserts nothing came back unreadable. That
discarded return is the reason the failure only ever said "map does not
contain <id>".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T18:38:35.573998Z 21cdcc7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21cdcc7082

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
time.Sleep(shareRetryDelay)
}
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Surface write failures after sharing retries expire

When an editor, antivirus scanner, or another process keeps the destination share-locked beyond this retry window, renameRetrying still returns ERROR_ACCESS_DENIED; however, terminal job persistence discards that result at internal/jobs/manager.go:1339 and other save sites. The temporary file is then removed by Write, so the terminal snapshot is neither retried nor reported, and a restart can reconcile the older running record as abandoned—the same silent durability failure this change intends to fix. Preserve the failed snapshot for a later retry or propagate/report the persistence error instead of relying solely on this short retry window.

Useful? React with 👍 / 👎.

@packetloss404
packetloss404 force-pushed the fix/windows-record-sharing-violations branch from 295701d to 21cdcc7 Compare September 5, 2026 18:42
@packetloss404
packetloss404 merged commit 451fa73 into main Sep 5, 2026
29 of 32 checks passed
@packetloss404
packetloss404 deleted the fix/windows-record-sharing-violations branch September 5, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant