ScanLockManagerLink
File‑Based Locking for Multi‑Process & Multi‑Thread Workloads
1. OverviewLink
ScanLockManager implements a robust, file‑based lock system that works across:
| Level | Scope | Example |
|---|---|---|
| Thread | In‑process synchronization | Multiple threads read the same scan |
| Process | Cross‑process protection | A worker daemon and a web UI both write to the same scan |
Features:
- Shared (read) & exclusive (write) locks - multiple readers may coexist, but writers have exclusive access.
- Timeouts - configurable lock acquisition timeout to avoid deadlocks.
- Automatic cleanup - stale lock files (e.g. from a crash) are purged on manager start.
- Metadata files - optional
.infofiles that record the lock holder, timestamp, PID, etc. Useful for debugging. - Thread‑safe public API - the manager uses an internal
threading.RLockso you can call it from anywhere in the same process.
Why file locks?
They survive across process restarts and even across system reboots (as long as the underlying filesystem supports it). Unlike in‑memory mutexes, file locks are visible to any process that shares the same filesystem.
2. Core ConceptsLink
| Concept | Description |
|---|---|
| Lock file | Each scan (scan_id) has a single lock file (<scan_id>.lock) in a dedicated .locks sub‑directory. |
| Lock type | LockType.SHARED or LockType.EXCLUSIVE. |
| Active lock | A lock that has been acquired and not yet released. |
| Lock info | Optional JSON file (<scan_id>.info) containing metadata about the current holder. |
3. Usage ExampleLink
from plantdb.commons.fsdb.lock import ScanLockManager, LockType
# Initialise manager - the .locks directory will be created automatically
manager = ScanLockManager("/home/alice/scans")
# 1. Read‑only operation (shared lock)
with manager.acquire_lock("scan001", LockType.SHARED, user="reader1"):
data = read_scan("scan001") # user‑defined function
process_data(data)
# 2. Write operation (exclusive lock)
try:
with manager.acquire_lock("scan001", LockType.EXCLUSIVE, user="writer1", timeout=10):
data = modify_scan("scan001")
write_scan("scan001", data)
except LockTimeoutError:
print("Could not obtain write lock - another process is writing")
4. Common Pitfalls & DebuggingLink
| Symptom | Likely Cause | Fix |
|---|---|---|
| Lock never releases after process crash | Stale lock file remains | ScanLockManager cleans them on startup. For manual clean‑up: rm /path/to/.locks/*.lock |
LockError raised unexpectedly |
Another user holds an exclusive lock | Check get_lock_status() or inspect <scan_id>.info |
LockTimeoutError appears after a short wait |
File system has high latency or contention | Increase default_timeout, or move locks to a faster disk |
fcntl.flock raises OSError: [Errno 22] Invalid argument |
Trying to lock a non‑regular file (e.g. a directory) | Ensure lock files are regular (.lock files, not directories) |
Tip: Enable debug logging on the
ScanLockManager(log_level="DEBUG") to see every lock attempt and release.
5. Extending the SystemLink
- Custom lock file location - override
_get_lock_file_path()in a subclass. - Different locking backends - replace
fcntl.flockwith e.g.portalockerfor cross‑platform support. - Metadata hooks - subclass
_write_lock_info()to store additional context (e.g., user roles).
6. Testing StrategyLink
- Unit tests - mock
fcntland file system interactions to test lock acquisition logic. - Integration tests - spawn multiple processes that compete for the same lock; verify that exclusive writers block readers/writers as expected.
- Crash simulation - kill a process holding a lock and ensure the next startup cleans the stale file.
- Timeout checks - configure a very short timeout and confirm that a
LockTimeoutErroris raised when contention is high.
7. ContributingLink
If you’d like to add features (e.g. Windows support, metrics collection, or a higher‑level API), feel free to open a PR.
Please include:
- Unit tests for the new code.
- Documentation updates (this file, if relevant).
- A clear description of the change and its motivation.