Skip to main content

yyds-lock

PyPI version License: MIT

中文文档

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-lock binds 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 atexit cleanup hook to close file descriptors cleanly on interpreter shutdown, preventing python ResourceWarning.
  • 💻 Cross-Platform: Seamlessly works on Linux, macOS (using fcntl.flock), and Windows (using msvcrt.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_lock under 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.
  • 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 call sys.exit(1) when the lock is already held.
    • True: Raise AlreadyLockedError when 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

  1. Linux / macOS: Uses fcntl.flock(fd, fcntl.LOCK_EX) for exclusive advisory locking.
  2. Windows: Uses msvcrt.locking(fd, msvcrt.LK_LOCK, 1) to lock the first byte of the file.
  3. 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.
  4. Fork-Safety: Automatically tracks forks and closes open descriptors in child processes post-fork (via os.register_at_fork).
  5. Clean Reclamation: Locks are released when:
    • An explicit release_single call 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

yyds_lock-0.2.3.tar.gz (13.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

yyds_lock-0.2.3-py3-none-any.whl (7.9 kB view details)

Uploaded Python 3

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

Hashes for yyds_lock-0.2.3.tar.gz
Algorithm Hash digest
SHA256 a4b55d4186ec5b814b15b0e50e2e5b1aa8294748e5eaa5a21468c5e1a5b71f3a
MD5 87952d4a38e3dfc6023bed558b08b11b
BLAKE2b-256 e4951e0d2182ab0fe8e9efb670b43ebd1d9bd7ac2447bffeeb3eb10edd9b7144

See more details on using hashes here.

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

Hashes for yyds_lock-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f65fe884860d4a28042ed3036a279c228743ab518c9af9d2813ad5e88e3cc203
MD5 88263c7d7923e9cff2be111f67cc6e23
BLAKE2b-256 d4ae999bcd14c45d9a4a0008295da5828fc001d3d6645869cbfa72de88ef81f0

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

This release

0.2.3 This release

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page