cadrumo.adapters.persistence.storage.bucket.lockfile module¶
Per-bucket .lock concurrency primitive for the bucket directory model.
Each bucket carries a single PID-stamped lockfile at <bucket-dir>/.lock
created via os.open with O_CREAT | O_EXCL | O_WRONLY; the
O_EXCL flag is atomic on every POSIX kernel and on Windows NTFS, so a
second-process unlock against a held bucket fails fast with
adapters.persistence.storage.bucket.errors.BucketBusyError per
the substrate locking contract.
The lockfile carries the holder’s PID. A stale lock (PID is no longer a live process) is reclaimed lazily by the acquiring process so an abnormal process exit (SIGKILL, OS crash, container OOM) does not permanently strand the bucket; the lazy reclaim is documented under the plan’s “Lockfile staleness detection” open question.
Every removal goes through unlink_lockfile(), because a
waiting acquirer that opens the lockfile to read its PID blocks the holder’s
unlink on Windows. On the release path that is a wedge rather than a delay –
the record names a live process, so the lock is never reclaimed as stale – so
release retries to a deadline while a stale reclaim stays single-attempt.
- class BucketLockTarget(*args, **kwargs)[source]¶
Bases:
ProtocolWhat the bucket lock actually needs: one directory to place
.lockin.The lock reads two fields – the directory it places
.lockin, and the bucket identity its refusals name. Declaring exactly those, rather than demanding the wholeBucketPathsrecord, is what lets a caller holding a narrowed view of a bucket delegate here without first re-widening it back to the concrete record – a round trip that had to be guarded by a runtime identity check standing in for a boundary this protocol states directly.It is deliberately NOT the pattern for the custody records. Those ports are narrower than the substrate needs on purpose: they hide key material the application has no business reading, so widening a crypto signature onto one would either expose that material or fail late on a field the port omits. The lock is the opposite case – nothing is hidden, because nothing beyond a directory is used.
- property bucket_id: str¶
The bucket’s identity, carried on every lock refusal’s context.
Declared because the lock genuinely reads it, not for symmetry: a busy-or-unlockable refusal names the bucket it could not take, and a protocol that omitted it would either fail late here or silently degrade the operator’s refusal to an unattributed one.
- lock_path(paths)[source]¶
Return the canonical lockfile path for the bucket.
- Return type:
Path- Parameters:
paths (BucketLockTarget)
- acquire_lock(paths, *, wait_seconds=0.0)[source]¶
Acquire the per-bucket lockfile or raise
BucketBusyError.- Parameters:
paths (
BucketLockTarget) – The bucket paths whose.lockto acquire.wait_seconds (
float) – Maximum time to wait for the lock to become free. Defaults to0.0(no wait); callers that want bounded waiting pass a positive value.
- Raises:
BucketBusyError – When the lockfile is held by a live process and the wait window expires.
- Return type:
- release_lock(paths)[source]¶
Release the per-bucket lockfile owned by this process.
Removes the lockfile only when the recorded PID matches this process; a foreign lockfile is left alone so a stale-reclaim race cannot delete another process’s lock.
- Raises:
BucketValidationError – When the lockfile is owned by this process but a peer’s open handle keeps the removal refused past the retry window, so the bucket would otherwise be wedged behind a lock stamped with a live PID.
- Return type:
- Parameters:
paths (BucketLockTarget)