Releases: ikorfale/durable-state-write
Release list
v0.5.0: classify post-replacement failures
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.
v0.4.0: fail closed on non-regular destinations
What changed
- Existing symlinks, dangling symlinks, FIFOs, directories, and other non-regular destinations are rejected instead of silently replacing the directory entry.
- Existing regular-file mode preservation now uses
lstat(), so the leaf is not followed while selecting permissions. - Corrected stale benchmark wording: peak RSS comes from Python
resource.getrusage, not GNU time.
Evidence
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest -v test_durable_state.py: 9/9 passed in 0.482 s on Python 3.12.3/Linux.python3 -m py_compile durable_state.py test_durable_state.py benchmark_memory.py: passed.git diff --check: passed.python3 benchmark_memory.py --sizes-mib 1 16 64: passed; all SHA-256 checks matched, and 64 MiB + 1 byte preserved the old file with zero temp residue.- CLI dogfood wrote an 83-byte mode-0600 JSON state file. A symlink destination then exited 2, preserved the link and target digest, and left zero matching temporary files.
Limits and rollback
This remains a tested prototype for trusted local containing directories. It does not establish power-loss guarantees, NFS/full-disk behavior, parent-directory symlink safety, or safety under hostile concurrent path mutation. Concurrent writers remain last-replacement-wins. Roll back by pinning v0.3.0.
v0.3.0: reproducible peak-RSS evidence
Adds benchmark_memory.py, a dependency-free Linux benchmark that compares the working-tree CLI with v0.1.1 using sparse 1, 16, and 64 MiB inputs. It verifies output SHA-256 values and exercises the 64 MiB + 1 negative case.\n\nObserved on Linux/Python 3.12.3 in the clean release commit: at 64 MiB, v0.1.1 peaked at 84,352 KiB RSS and v0.3.0 at 20,688 KiB. For 64 MiB + 1, both exited 2, preserved the old destination, and left no temporary files; peaks were 84,352 KiB and 20,560 KiB. Unit suite: 8/8 passed in 0.466 s.\n\nThe first benchmark draft assumed GNU time, which was absent in the dogfood environment. The released benchmark uses Python resource.getrusage in an isolated worker instead. Measurements are same-host observations, not fixed guarantees or throughput results. Power-loss, NFS, full-disk, ACL/xattr, locking, idempotency, and consensus guarantees remain explicitly out of scope.
v0.2.0: bounded-memory streamed input
The CLI now copies stdin in chunks of at most 1 MiB instead of buffering the full permitted input. Oversized input still fails before replacement, preserving an existing destination and cleaning the temporary file.
Evidence on Linux/Python 3.12:
python3 -m py_compile durable_state.py test_durable_state.pypassed.python3 -m unittest -v test_durable_state.pypassed 8 tests in 0.474s.- Real piped JSON dogfood produced a valid 50-byte mode-0600 file.
- A 64 MiB+1 pipe exited 2, preserved the old destination, and left no temporary file.
Limitations remain explicit: no physical power-cut testing, NFS/full-disk/ACL/xattr coverage, locking, or lost-update prevention. Roll back by pinning v0.1.1 or removing the copied helper.
v0.1.1: explicit concurrent-writer contract
This bounded increment turns the previously untested concurrent-writer limitation into an explicit contract and regression test.
Evidence on Linux/Python 3.12:
python3 -m unittest -v test_durable_state.py: 6 tests passed in 0.481s.- Eight synchronized writer processes each submitted a distinct 128 KiB generation; all exited 0, the final file exactly matched one submitted payload, and no completed-process temp files remained.
- Real CLI dogfood wrote a 50-byte JSON state file with mode 0600.
- Negative case: 64 MiB + 1 byte stdin exited 2 and created no destination.
Concurrent writers are deliberately last-replacement-wins with unspecified ordering. This is not read-modify-write protection and does not prevent lost updates. Power-loss durability, NFS, full disks, ACLs/xattrs, and symlink policy remain untested/unsupported boundaries.
v0.1.0 — bounded tested prototype
First useful release.
- Atomic same-directory replacement with file and directory fsync barriers
- Permission preservation and stdin CLI
- Five dependency-free regression tests
Limitations: process-crash tests are not power-cut tests; no guarantee is claimed for NFS, full disks, concurrent writers, ACLs/xattrs, symlinks, or storage-controller failures.