cadrumo.core.atomic_write module¶
Shared two-tier atomic-write helper.
Every durable on-disk write in this codebase must never leave a torn or
partially-written file behind, whether the process crashes, is killed, or
raises mid-write. Before this module, four independent dialects of the same
“write a sibling tempfile, then os.replace() it over the target”
pattern had accreted across the storage substrate: a standard fsync+replace
variant (adapters.persistence.storage.envelope), a hidden-file variant
with no fsync, plain-write variants with no fsync at all, and a
collision-hardened O_EXCL + mode 0o600 variant reserved for the
master-key store. This module collapses all of that onto four named tiers
so a new writer picks one deliberately instead of inventing a fifth dialect:
Standard tier (
atomic_write_bytes(),atomic_write_text()): atempfile.NamedTemporaryFile()sibling in the target’s own parent directory ({stem}.prefix,.tmpsuffix), write, flush,fsync,os.replace(), then a best-effort parent-directoryfsyncviafsync_parent_dir(). Suitable for ordinary durable application data with a single writer.Hardened tier (
atomic_write_hardened_bytes(),atomic_write_hardened_text()): the master-key store’s pattern –O_EXCLplusO_NOINHERIT/O_CLOEXECwhere the platform defines them, file mode0o600, and a collision-hardened{name}.{pid}.{token_hex}.tmptemp name – before the same fsync/replace/parent-fsync sequence. Use this tier for secret-bearing targets or any target more than one process/thread could plausibly write concurrently; theO_EXCLopen refuses to silently reuse or truncate an unexpected pre-existing tempfile. A memoryview-and-offset loop completes shortos.write()calls and refuses a write that makes no progress.Deferred-publish tier (
hardened_staged_publication()): the hardened tier’s staging and publication with the write itself left to the caller, for a producer that cannot hand over abytespayload – it needs a real file on disk to build incrementally or to read back before the result becomes operator-visible, or it must complete unrelated work between the write and the publication. The staging sibling carries the same unguessable{name}.{pid}.{token_hex}.tmpname, the sameO_EXCLreservation and the same0o600mode; publication is the same fsync/replace/parent-fsync sequence, requested explicitly so the caller can translate a publication failure into its own domain error. It exists because callers needing that shape were open-coding a predictable{name}.tmpsibling and a bareos.replace()next to the operator’s chosen destination, which is a guessable name, an unsynced publication, and – with a narrowexcept– a staged file that survives an interrupt.Best-effort tier (
atomic_write_best_effort_bytes(),atomic_write_best_effort_text()): the same tempfile-sibling-plus-os.replace()mechanics as the standard tier, but deliberately WITHOUT anyfsynccall. Reserved for rebuildable derived caches (a compiled- registry pickle, a corpus-text extraction cache, a validation-verdict stamp) where a torn write on crash only costs a recompute on the next process, never data loss – the fsync cost is not worth paying on a cache write. This tier never logs and never swallows: it raises the underlying exception unwrapped, same as the other two tiers, and leaves the try/except/log/swallow policy entirely to the caller, because every existing best-effort caller already wraps its write in its own catch with its own log level and message and duplicating that here would double-log or contradict it.
Every tier guarantees the tempfile is unlinked on ANY failure – a bare
try/finally around the whole sequence, not a narrow except
OSError – so a KeyboardInterrupt or any other BaseException
mid-write cannot leave an orphan tempfile next to the target. No tier wraps
or translates the underlying exception: callers see the raw
OSError (or whatever the platform raises) so each call site can
apply its own domain-specific error class, matching the existing
call-site-owns-its-error-type convention this module’s callers already use.
Payload content is never logged; only the target path and the exception
type are (the best-effort tier logs nothing at all; see above).
The helpers are freestanding primitives at the core layer with no
dependency beyond fsync_parent_dir().
- atomic_write_bytes(path, data)[source]¶
Atomically write
datatopath(standard tier).Stages a
tempfile.NamedTemporaryFile()sibling inpath’s parent directory (created if absent), writes and fsyncs it, then replacespathwithos.replace()and best-effort fsyncs the parent directory. The tempfile is unlinked on any failure, including aBaseExceptionraised mid-write.
- atomic_write_text(path, text, *, encoding='utf-8')[source]¶
Atomically write
texttopath(standard tier, text variant).Encodes
textand delegates toatomic_write_bytes(); see its docstring for the write sequence and failure semantics.
- atomic_write_best_effort_bytes(path, data)[source]¶
Atomically write
datatopath(best-effort tier, no fsync).Stages a
tempfile.NamedTemporaryFile()sibling inpath’s parent directory (created if absent), writes it, then replacespathwithos.replace(). Unlike the standard and hardened tiers, this tier never callsfsyncon the tempfile or the parent directory: it exists for rebuildable derived caches where a torn write on crash only costs a recompute, not data loss, so the fsync cost is not worth paying on the write. The tempfile is unlinked on any failure, including aBaseExceptionraised mid-write. This tier never logs; the caller owns its own catch/log/swallow policy (see the module docstring).
- atomic_write_best_effort_text(path, text, *, encoding='utf-8')[source]¶
Atomically write
texttopath(best-effort tier, text variant).Encodes
textand delegates toatomic_write_best_effort_bytes(); see its docstring for the write sequence and failure semantics.
- class DurableWriteBatch[source]¶
Bases:
objectDefer the per-file durability sync across many writes, flushing once.
A hardened write costs three syncs — the staged fd, then the parent directory after
os.replace(). Measured on this project’s target platform that is ~3.5 ms/file against ~0.6 ms for the same write without them, so a bulk ingest of 20,000 evidence records spends about a minute on durability alone. This batch collapses that to one directory sync per touched directory at commit.Only for content-addressed or re-derivable payloads. The trade is real: a crash mid-batch can leave a file whose name is present but whose bytes never reached the platter. That is recoverable exactly when the reader can detect it and the caller can reproduce it — a blob named by the SHA-256 of its own contents fails its digest check on read and its source document is still on disk to re-import. It is NOT acceptable for key material, pointers, or session records: a torn key is neither detectable nor re-derivable, and those writers must keep the unbatched path.
Explicit by construction. The batch is passed as an argument rather than bound to ambient context, because a durability policy that travels invisibly would silently weaken whichever unrelated write happened to run inside the scope.
- durable_write_batch()[source]¶
Yield a
DurableWriteBatchand commit it on exit.Commits from a
finally, so an exception mid-batch still syncs whatever already landed rather than leaving the completed writes less durable than an unbatched run would have.- Return type:
- atomic_write_hardened_bytes(path, data, *, mode=384, batch=None)[source]¶
Atomically write
datatopath(hardened tier).Opens a collision-hardened
{name}.{pid}.{token_hex}.tmpsibling withO_EXCL(refusing to reuse or truncate an unexpected pre-existing tempfile) plusO_NOINHERIT/O_CLOEXECwhere the platform defines them, at file modemode. A memoryview-and-offset loop completes shortos.write()calls and treats a non-positive write as an error. The staged bytes are fsynced beforeos.replace(), followed by a best-effort parent-directory fsync. The tempfile is unlinked on any failure, including aBaseExceptionraised mid-write. Use this tier for secret-bearing targets or targets more than one process could plausibly write concurrently.- Parameters:
path (
Path) – Destination file. Parent directory is created if absent.data (
bytes) – Full file contents to write.mode (
int) – POSIX file mode for the staged tempfile (and, transitively, the replaced target). Defaults to0o600.batch (
DurableWriteBatch|None) – OptionalDurableWriteBatch. When supplied, the write stays atomic but its two fsyncs are deferred to the batch commit. Pass one ONLY for content-addressed or otherwise re-derivable payloads; seeDurableWriteBatchfor why key material, pointers and session records must not use it.
- Raises:
OSError – When staging, writing, or replacing the file fails (including
FileExistsErrorfrom anO_EXCLcollision). The original exception propagates unwrapped; the tempfile is cleaned up first.- Return type:
- atomic_write_hardened_text(path, text, *, encoding='utf-8', mode=384)[source]¶
Atomically write
texttopath(hardened tier, text variant).Encodes
textand delegates toatomic_write_hardened_bytes(); see its docstring for the write sequence and failure semantics.
- class StagedPublication(*, staging_path, target_path)[source]¶
Bases:
objectA reserved hardened staging file awaiting an explicit publication.
Yielded by
hardened_staged_publication(). The caller writes the payload atpathby whatever means its producer requires, then callspublish()to move those bytes atomically onto the target. Leaving the context without publishing discards the staged file, so an interrupt, a refusal raised between the write and the publication, or an early return can never leave the payload stranded next to the target under a name the caller never told anyone about.Publication is a method rather than an automatic action on clean exit because the callers that need this tier write sensitive artefacts and must translate a publication
OSErrorinto their own typed refusal. A context manager that published on exit would raise from thewithstatement itself, where the caller can no longer distinguish a publication failure from a failure in its own body.- Parameters:
staging_path (Path)
target_path (Path)
- publish()[source]¶
Atomically move the staged bytes onto the target and sync its directory.
- Raises:
OSError – When the replace or the directory sync fails. The staged file is left in place for the enclosing context manager to discard, so a failed publication never strands the payload.
RuntimeError – When called a second time. One staging file publishes once; a second call would replace the target with a path the first call already consumed.
- Return type:
- hardened_staged_publication(target_path, *, mode=384)[source]¶
Reserve a hardened staging sibling of
target_pathfor a caller-driven write.The deferred-publish tier. Reserves an unguessable
{name}.{pid}.{token_hex}.tmpsibling with the hardened tier’sO_EXCLopen at file modemode, yields it, and discards it on any exit that did not publish – including aBaseExceptionsuch as the operator interrupting the work, which is precisely the case a narrowexcept OSErroraround a hand-rolled staging file misses.Use this tier when the payload cannot be handed over as
bytes: the producer builds the file incrementally, reads it back to verify it before it becomes operator-visible, or must complete unrelated work between the write and the publication. When the payload is in hand, useatomic_write_hardened_bytes()instead – it is the same guarantees in one call.The reservation matters as much as the name. Opening with
O_EXCLrefuses to adopt anything already sitting at the staging path, so the name handed to the caller is one this call claimed rather than one it inherited. A caller that writes in place keeps that inode; a caller that stages through its own replace supersedes it, and in both cases the name was never available to anyone else in between.- Parameters:
target_path (
Path) – Destination the staged bytes are published onto. Its parent directory is created if absent; the target itself is not touched untilStagedPublication.publish()runs.mode (
int) – POSIX file mode for the reserved staging file (and, transitively, the published target). Defaults to0o600.
- Yields:
A
StagedPublicationbound to the reserved staging file.- Raises:
OSError – When the parent directory or the staging reservation cannot be created (including
FileExistsErrorfrom anO_EXCLcollision).- Return type: