phenotypic.sdk_.promote_store#

phenotypic.sdk_.promote_store(part: Path, final: Path, *, fsync: bool, commit_guard: Callable[[], AbstractContextManager[None]] | None = None) Path[source]#

Atomically promote a fully written .part directory to final.

The caller is responsible for the write order inside part: all arrays and chunks first, then OME/zarr.json, then the root zarr.json last. An interrupted store therefore has no valid root and reads as absent. This function does not write the root zarr.json itself.

The move-aside is mandatory, not an optimization: os.replace onto a non-empty directory raises OSError (ENOTEMPTY) on POSIX, and on Windows MoveFileEx’s MOVEFILE_REPLACE_EXISTING cannot name a directory at all.

The whole exists -> move-aside -> replace sequence sits inside one retry loop and re-evaluates existence on every attempt. That is what makes duplicate execution benign: a uuid .part prevents two writers interleaving chunks, but it does nothing for the promote itself, where a check-then-act done once lets writer B skip the move-aside because A had not yet renamed, then hit ENOTEMPTY on a now-non-empty target.

On failure after a successful move-aside, that attempt’s trash is reconciled before retrying or raising. The previous store is rolled back when final is absent; if a concurrent writer has already published a new final, that winner remains authoritative and only the attempt’s superseded trash is removed. Every retry uses a fresh UUID trash path, so no attempt can collide with its predecessor’s move-aside directory.

Known weakening versus the single-file rename: the two renames are still not one atomic step, so a crash between them (as opposed to a raised error) leaves the image absent plus an orphaned .trash. Both are recoverable – absence reclassifies to the rebuilding stage, and sweep_orphan_parts() clears the leftovers.

Parameters:
Returns:

final.

Return type:

Path