C#: store replace on Windows tries POSIX-semantics rename first (store-windows-cs) #152

Merged
libby merged 4 commits from store-replace/windows-cs into main 2026-11-13 00:38:08 +00:00
Member

What

StoreReplace.Replace(staged, destination) in GameCult.Caching owns how a staged file becomes the store, for the single-file store (WriteSnapshotAtomically) and the directory store (DirectoryMessagePackBackingStore). On Windows it tries a POSIX-semantics rename first (CreateFileW + SetFileInformationByHandle FileRenameInfoEx, REPLACE_IF_EXISTS|POSIX_SEMANTICS), falls back to MoveFileExW(0x9) on Win32 codes 1/50/87 for the rest of the call, retries 25 x 20 ms on 5/32/33, and throws IOException naming the code otherwise. Other OSes keep File.Replace/File.Move. Port of cultcache-ts replaceStoreFile.

Deleted: the File.Exists/Replace/Move branch in WriteSnapshotAtomically and the directory store's own 3-attempt File.Replace loop. ACacheOpeningDuringMeshWritesNeverSeesAnEmptyStore is un-ignored on Windows and tolerates a brief not-found there (still zero empty stores elsewhere). Unresolved kernel32 imports: POSIX call -> same fallback as unsupported; MoveFileExW -> portable File.Replace/Move.

Why

File.Replace on Windows leaves a moment with nothing at the path for a lock-free reader (1,722 empty stores in 17,313 opens). POSIX-first makes that absence rare, not impossible: a stalled rename was measured leaving the path absent 5-47 ms on Starfire (open operator question variants:question:posix-rename-absent-window). Ruling variants:ruling:windows-replace-posix-first.

Verification

  • Linux: GameCult.Caching.Tests 1374 passed / 0 failed / 5 skipped; GameCult.Mesh.Tests 362 passed / 0 failed / 4 skipped; GameCult.Caching.OutsideStoreTests 8 passed / 0 failed.
  • StoreReplaceTests drive the Windows decision table through a fake kernel (red first, commit 5eb2f75c); hand mutations of attempts, the 87 fallback code, fallback stickiness, same-attempt fallback, the posix guard and the unresolved-import handling each fail tests.
  • File\.Replace\( in src/GameCult.Caching and src/GameCult.Caching.MessagePack: only StoreReplace's non-Windows branch.
  • Real-Windows run: see the cut report. IL2CPP/GDK remain unverified (unresolved imports degrade as above, untested on those targets). Known, not fixed: CreateFileW takes the staged path unprefixed, so paths over 260 characters fail without LongPathsEnabled (cross-runtime follow-up).

Spec variants:cut_spec:cut-store-windows-cs.r1, report variants:cut_report:cut-store-windows-cs.h1.

🤖 Generated with Claude Code

## What `StoreReplace.Replace(staged, destination)` in GameCult.Caching owns how a staged file becomes the store, for the single-file store (`WriteSnapshotAtomically`) and the directory store (`DirectoryMessagePackBackingStore`). On Windows it tries a POSIX-semantics rename first (CreateFileW + SetFileInformationByHandle FileRenameInfoEx, REPLACE_IF_EXISTS|POSIX_SEMANTICS), falls back to MoveFileExW(0x9) on Win32 codes 1/50/87 for the rest of the call, retries 25 x 20 ms on 5/32/33, and throws IOException naming the code otherwise. Other OSes keep File.Replace/File.Move. Port of cultcache-ts `replaceStoreFile`. Deleted: the File.Exists/Replace/Move branch in WriteSnapshotAtomically and the directory store's own 3-attempt File.Replace loop. `ACacheOpeningDuringMeshWritesNeverSeesAnEmptyStore` is un-ignored on Windows and tolerates a brief not-found there (still zero empty stores elsewhere). Unresolved kernel32 imports: POSIX call -> same fallback as unsupported; MoveFileExW -> portable File.Replace/Move. ## Why File.Replace on Windows leaves a moment with nothing at the path for a lock-free reader (1,722 empty stores in 17,313 opens). POSIX-first makes that absence rare, not impossible: a stalled rename was measured leaving the path absent 5-47 ms on Starfire (open operator question `variants:question:posix-rename-absent-window`). Ruling `variants:ruling:windows-replace-posix-first`. ## Verification - Linux: GameCult.Caching.Tests 1374 passed / 0 failed / 5 skipped; GameCult.Mesh.Tests 362 passed / 0 failed / 4 skipped; GameCult.Caching.OutsideStoreTests 8 passed / 0 failed. - StoreReplaceTests drive the Windows decision table through a fake kernel (red first, commit 5eb2f75c); hand mutations of attempts, the 87 fallback code, fallback stickiness, same-attempt fallback, the posix guard and the unresolved-import handling each fail tests. - `File\.Replace\(` in src/GameCult.Caching and src/GameCult.Caching.MessagePack: only StoreReplace's non-Windows branch. - Real-Windows run: see the cut report. IL2CPP/GDK remain unverified (unresolved imports degrade as above, untested on those targets). Known, not fixed: CreateFileW takes the staged path unprefixed, so paths over 260 characters fail without LongPathsEnabled (cross-runtime follow-up). Spec `variants:cut_spec:cut-store-windows-cs.r1`, report `variants:cut_report:cut-store-windows-cs.h1`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both store writers call StoreReplace.Replace; the directory store's File.Replace
retry loop and WriteSnapshotAtomically's Exists/Replace/Move block are deleted.
Un-ignores the mesh empty-store race test on Windows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
StoreReplaceTests: the exhaustion message names the last code, not just "Win32"
Some checks failed
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
c51ebcdaf7
Mutation survivor: Contains("32") matched "Win32".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
StoreReplace: unresolved kernel32 imports degrade, claims say rare not never, killing tests
All checks were successful
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Successful in 22m32s
1f6cd2f8c8
ReplaceWindows owns the unresolved-import decision: a POSIX call that does not
resolve (DllNotFound/EntryPointNotFound) is "unsupported"; a MoveFileExW that
does not resolve degrades to the portable File.Replace/Move. Tests for each
exception type, the last-attempt same-attempt fallback and the retried-87 case.
Doc and Mesh test comments no longer claim a reader never finds the path absent;
the Mesh race test tolerates a brief not-found on Windows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
libby merged commit a85c63712b into main 2026-11-13 00:38:08 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/CultLib!152
No description provided.