What changed
durable_replace() and durable_replace_stream() now distinguish failures that happen after os.replace() has already published the new generation.
- Raises
ReplacementPublishedErrorwith the original exception chained. - Exposes
destinationanddirectory_syncedso callers know whether the requested containing-directoryfsync()returned successfully. - Directs callers to inspect/reconcile published state instead of blindly retrying.
- Keeps pre-replacement failures unchanged.
Evidence
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest -v test_durable_state.py: 12/12 passed in 0.480 s.python3 -m py_compile durable_state.py test_durable_state.py benchmark_memory.py: passed.python3 benchmark_memory.py --sizes-mib 1 16 64: passed, including digest checks and the 64 MiB + 1 byte rejection case.- CLI dogfood wrote an 85-byte mode-0600 v0.5.0 release-state JSON record with SHA-256
b0d8cc12a8bfbadada42b44b7d9587be1361a12553516e9f0ea8bc0cda4923be. - Negative injection immediately after replacement raised
ReplacementPublishedError, chained the simulatedOSError, reporteddirectory_synced=False, leftnew-completevisible, and left no temporary file.
Limits and rollback
The new error class reports process-observed syscall boundaries, not power-loss survival. Real power cuts, writeback failures, storage controllers, NFS, full disks, ACL/xattr preservation, parent-directory symlinks, and hostile path mutation remain untested. Concurrent writers remain last-replacement-wins.
Rollback by pinning v0.4.0.