yyds-lock
yyds-lock is an industrial-grade, ultra-lightweight, zero-dependency Python library that guarantees single-instance execution of scripts, processes, or threads using operating system level advisory file locks. It is ideal for cron jobs, automation scripts, schedulers, and background daemons.
Key Features
- 🛡️ Immunity to Crashes / Force Kills: Unlike PID files or stale lock files that cause permanent lockups if a process is terminated forcefully (
kill -9, crash, or power loss),yyds-lockbinds the lock to the process file descriptor. The OS automatically and instantly releases the lock as soon as the process ends. - 🪶 Zero Dependencies: 100% pure Python standard library. Package size is less than 5KB and does not pollute your runtime environment.
- 🎛️ Dual Modes: Supports both "Instant Exit" (non-blocking, terminates immediately if another instance is running) and "Queue / Wait" (blocking, waits for the existing instance to finish).
- 🧵 Thread-Safety & Isolation: Safe to use in multi-threaded programs. Different threads running under the same process are isolated and will block or raise conflicts on the same lock.
- 🔱 Fork-Safety: Automatically handles Unix process forks (
multiprocessing, Celery, Gunicorn, etc.) by closing inherited locks in child processes without unlocking the parent. - 📁 Inaccessible Directory Fallback: If the home directory is read-only or does not exist (e.g., in headless Docker containers), the library automatically and safely falls back to the system temporary directory.
- 🧹 Automatic Cleanup: Registers an
atexitcleanup hook to close file descriptors cleanly on interpreter shutdown, preventing pythonResourceWarning. - 💻 Cross-Platform: Seamlessly works on Linux, macOS (using
fcntl.flock), and Windows (usingmsvcrt.locking).
Installation
pip install -U yyds-lock
Usage
You can protect your script using any of the following approaches:
Pattern A: Direct Call (Best for straightforward scripts / entrypoints)
Place this call at the very top of your entrypoint script. If another instance of the script is already running, the new instance will immediately print an error and exit with status code 1.
import time
import yyds_lock
# Force single-instance execution.
yyds_lock.force_single(lock_name="my_automation.lock", block=False)
print("Running heavy automation task...")
time.sleep(300)
Pattern B: Decorator with Dynamic Lock Names
Decorate your functions to enforce mutual exclusion. The lock_name parameter can also be a callable (e.g. lambda function) that dynamically generates the lock name based on function arguments.
import yyds_lock
# 1. Static lock name
@yyds_lock.single_decorator(lock_name="my_task.lock", block=False)
def main():
print("Executing single instance task safely...")
# 2. Dynamic lock name based on arguments
@yyds_lock.single_decorator(lock_name=lambda job_id: f"job_{job_id}.lock", block=False)
def process_job(job_id):
print(f"Processing job {job_id} exclusively...")
if __name__ == "__main__":
main()
process_job(42)
Pattern C: Handle Lock Conflict (Exception Raising)
If you prefer to handle the locking failure programmatically (e.g., to perform custom cleanups, log warnings, or run fallback logic) instead of immediately terminating the process, set raise_on_conflict=True to raise AlreadyLockedError:
import yyds_lock
from yyds_lock import AlreadyLockedError
try:
yyds_lock.force_single(lock_name="my_automation.lock", block=False, raise_on_conflict=True)
except AlreadyLockedError:
print("Failed to acquire lock. Running fallback script instead...")
# Add custom fallback actions here
Configuration / Arguments
Both force_single and single_decorator accept the following arguments:
lock_name(str or callable): The filename/path of the lock, or a callable returning a string when using the decorator.- If a simple filename is given (e.g.
"my_job.lock"), it is automatically created in a hidden directory.yyds_lockunder the user's home directory (~/.yyds_lock). - If an absolute or relative path is given (e.g.,
"/var/run/my_job.lock"), it is created at that specific path. The parent directories will be created automatically if they do not exist.
- If a simple filename is given (e.g.
block(bool):False(default): Exit immediately (or raise) if the lock cannot be acquired.True: Block and queue, waiting for the active process/thread to finish and release the lock.
raise_on_conflict(bool):False(default): Immediately log an error and callsys.exit(1)when the lock is already held.True: RaiseAlreadyLockedErrorwhen the lock is already held, allowing the caller to catch it.
base_dir(str, optional): Overrides the default folder directory (~/.yyds_lock) where simple filenames are saved.
Logging
yyds-lock uses Python's standard logging library. All lock conflicts and warning outputs are logged using:
import logging
logger = logging.getLogger("yyds_lock")
By default, if you have not configured any handlers for your logging system, yyds-lock will automatically print user-friendly colored error messages to sys.stderr to maintain simplicity for basic scripts.
How It Works Under the Hood
- Linux / macOS: Uses
fcntl.flock(fd, fcntl.LOCK_EX)for exclusive advisory locking. - Windows: Uses
msvcrt.locking(fd, msvcrt.LK_LOCK, 1)to lock the first byte of the file. - Thread Safety: Uses a thread-safe global registry with reentrancy checks mapped to
threading.get_ident(). File descriptor locking calls are executed outside the global lock, preventing deadlocks when threads block and wait. - Fork-Safety: Automatically tracks forks and closes open descriptors in child processes post-fork (via
os.register_at_fork). - Clean Reclamation: Locks are released when:
- An explicit
release_singlecall is executed. - The decorated function finishes execution.
- Python exit handlers run (
atexit). - The process terminates or is killed, prompting the operating system to reclaim all file descriptors and release the locks.
- An explicit
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file yyds_lock-0.2.3.tar.gz.
File metadata
- Download URL: yyds_lock-0.2.3.tar.gz
- Upload date:
- Size: 13.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4b55d4186ec5b814b15b0e50e2e5b1aa8294748e5eaa5a21468c5e1a5b71f3a
|
|
| MD5 |
87952d4a38e3dfc6023bed558b08b11b
|
|
| BLAKE2b-256 |
e4951e0d2182ab0fe8e9efb670b43ebd1d9bd7ac2447bffeeb3eb10edd9b7144
|
File details
Details for the file yyds_lock-0.2.3-py3-none-any.whl.
File metadata
- Download URL: yyds_lock-0.2.3-py3-none-any.whl
- Upload date:
- Size: 7.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f65fe884860d4a28042ed3036a279c228743ab518c9af9d2813ad5e88e3cc203
|
|
| MD5 |
88263c7d7923e9cff2be111f67cc6e23
|
|
| BLAKE2b-256 |
d4ae999bcd14c45d9a4a0008295da5828fc001d3d6645869cbfa72de88ef81f0
|