cadrumo.core.locks module¶
Cross-platform exclusive file locking.
The helper exposes exclusive_file_lock(), a context manager that
acquires an OS-level exclusive lock on a sidecar file alongside a
protected resource, and exclusive_file_lock_async(), its awaitable
twin for callers running on an event loop. The two share one sidecar
path, one OS primitive, one deadline and one refusal; they differ only
in how they wait between attempts. Two operating-system primitives back
the helper:
POSIX (Linux, macOS):
fcntl.flock()withLOCK_EX | LOCK_NB.Windows:
msvcrt.locking()withLK_NBLCKagainst a one-byte region of the lock file.
The wait loop is identical across platforms: try the non-blocking
acquire, sleep for a small backoff interval, retry until the timeout
elapses. On timeout the helper raises LockAcquisitionError
(category LOCKED in the error registry).
The lock file is created adjacent to the protected path by appending
the suffix .lock. The lock fd is held for the duration of the
context. The sidecar is deliberately retained after release. Lock
ownership is managed by the OS against the open descriptor; the mere
existence of the sidecar does not mean that any process owns the lock.
Consumers must not delete it because doing so while another process is
racing to acquire it would create a TOCTOU window.
This primitive is deliberately metadata-free. It does not write a PID, hostname, profile id, timeout stamp, or secure-storage custody state into the sidecar file, and it does not perform stale-lock recovery. Consumers that need recoverable lock records, bucket ownership, or auth-acquisition TTL semantics own those protocols above this OS-lock layer.
- DEFAULT_LOCK_TIMEOUT: Final[_DefaultLockTimeout]¶
Sentinel for
exclusive_file_lock()timeout; resolves at call time.
- exclusive_file_lock(target, *, timeout=DEFAULT_LOCK_TIMEOUT, retry_backoff=DEFAULT_LOCK_TIMEOUT)[source]¶
Acquire an OS-level exclusive lock on a sidecar lock file.
The sidecar lock file is created alongside
targetwith the suffix.lock. The caller is expected to use the lock to coordinate concurrent access totarget. The lock is released when the context manager exits, whether normally or via exception. The lock file itself is left on disk so a racing acquirer never sees a transient missing-file state.On Windows
msvcrt.lockingenforces a mandatory lock against a single byte of the lock file. On POSIXfcntl.flockis advisory — readers that do not also acquire the lock can still observe the protected resource mid-write. Callers MUST treat the lock as advisory across the whole file regardless of the underlying primitive.The sidecar carries no ownership metadata and is not deleted on release. This is a generic local coordination primitive for atomic file updates; higher-level bucket lockfiles, auth acquisition locks, and secure-object sessions provide their own holder records, TTLs, custody checks, and recovery rules.
- Parameters:
target (
Path) – Path to the resource being protected. The lock sidecar is created at<target>.lock. The parent directory must exist before the call.timeout (
float|_DefaultLockTimeout) – Maximum time in seconds to wait for the lock. Defaults toDEFAULT_LOCK_TIMEOUT.0requests a single non- blocking attempt.retry_backoff (
float|_DefaultLockTimeout) – Sleep interval between non-blocking attempts. Must be strictly positive, matching thegt=0bound on theSettingsfield that supplies its default. Tests may shorten this; defaults to0.05.
- Yields:
The
Pathof the acquired lock sidecar.- Raises:
LockAcquisitionError – If the lock cannot be acquired within
timeoutseconds, iftimeoutis negative, or ifretry_backoffis not strictly positive. The error category isLOCKEDandretryableisTrue. The retryable flag means “another acquirer may release shortly and the operation could succeed on retry”; consumers that retry MUST bound the retry budget themselves.- Return type:
Generator[Path]
- exclusive_file_lock_async(target, *, timeout=DEFAULT_LOCK_TIMEOUT, retry_backoff=DEFAULT_LOCK_TIMEOUT)[source]¶
Acquire the same OS-level exclusive lock without blocking the event loop.
This is the awaitable twin of
exclusive_file_lock(), not a replacement for it: identical sidecar path, identical OS primitive, identical deadline and refusal. The only difference is that the wait between non-blocking attempts isasyncio.sleep()rather thantime.sleep(), so a coroutine waiting on a contended lock yields to the loop instead of stalling every other task on it.Use this from a coroutine. Use the synchronous form everywhere else — a synchronous caller has no loop to block and gains nothing here.
Cancellation: the wait is a cancellation point, so a cancelled task stops waiting promptly and the descriptor is closed on the way out. The synchronous form cannot be cancelled at all, which is what makes it unsuitable for a polling UI worker.
An executor hop (
asyncio.to_thread(),run_in_executor) is deliberately NOT the mechanism here. It pays a thread hop per call, and it drops thecontextvarscontext that carries the active run id, so an offloaded read that records an observability event raises instead of recording it.Args and refusals are exactly those of
exclusive_file_lock().- Yields:
The
Pathof the acquired lock sidecar.- Return type:
AsyncGenerator[Path]- Parameters: