{"id":8503,"library":"pymysqllock","title":"PyMySQLLock","description":"PyMySQLLock is a Python library that provides a MySQL-backed distributed locking primitive. It enables multiple application instances to coordinate and ensure that only one instance holds a specific lock at a time, performing tasks that require exclusive access. As of version 0.2.0, it aims to be a lightweight solution when MySQL is the primary dependency for application uptime and health, rather than relying on external systems like ZooKeeper or etcd. The project appears stable with a moderate release cadence.","status":"active","version":"0.2.0","language":"en","source_language":"en","source_url":"https://github.com/sanketplus/PyMySQLLock","tags":["mysql","locking","distributed lock","concurrency","database"],"install":[{"cmd":"pip install PyMySQLLock","lang":"bash","label":"Install PyMySQLLock"}],"dependencies":[{"reason":"Requires an underlying Python MySQL driver for connection. PyMySQL is a common choice.","package":"PyMySQL","optional":true},{"reason":"Requires an underlying Python MySQL driver for connection.","package":"mysql-connector-python","optional":true},{"reason":"Requires an underlying Python MySQL driver for connection.","package":"mysqlclient","optional":true}],"imports":[{"note":"The top-level package name for import uses camel case `PyMySQLLock` as per the project's source and documentation, not `pymysqllock`.","wrong":"from pymysqllock import Locker","symbol":"Locker","correct":"from PyMySQLLock import Locker"}],"quickstart":{"code":"import os\nfrom PyMySQLLock import Locker\n\n# --- Configuration (use environment variables for security) ---\nMYSQL_HOST = os.environ.get('MYSQL_HOST', 'localhost')\nMYSQL_USER = os.environ.get('MYSQL_USER', 'root')\nMYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'password')\nMYSQL_DB = os.environ.get('MYSQL_DB', 'test_db')\n\nLOCK_NAME = 'my_critical_task_lock'\nACQUIRE_TIMEOUT = 10 # seconds\n\ndef run_task_with_lock():\n    locker = None\n    try:\n        # Locker uses connection parameters compatible with common MySQL drivers (e.g., PyMySQL)\n        locker = Locker(\n            host=MYSQL_HOST,\n            user=MYSQL_USER,\n            password=MYSQL_PASSWORD,\n            database=MYSQL_DB\n        )\n        lock = locker.lock(LOCK_NAME)\n\n        print(f\"Attempting to acquire lock '{LOCK_NAME}' with a {ACQUIRE_TIMEOUT}s timeout...\")\n        # Try to acquire the lock. Default timeout is -1 (infinite wait).\n        # Setting refresh_interval_secs keeps the connection alive for long-held locks.\n        if lock.acquire(timeout=ACQUIRE_TIMEOUT, refresh_interval_secs=5):\n            print(f\"Successfully acquired lock '{LOCK_NAME}'. Performing critical task...\")\n            # Simulate work\n            import time\n            time.sleep(5)\n            print(\"Critical task completed.\")\n        else:\n            print(f\"Failed to acquire lock '{LOCK_NAME}' within {ACQUIRE_TIMEOUT} seconds. Another instance might hold it.\")\n    except Exception as e:\n        print(f\"An error occurred: {e}\")\n    finally:\n        if 'lock' in locals() and lock.is_acquired():\n            print(f\"Releasing lock '{LOCK_NAME}'.\")\n            lock.release()\n        if locker:\n            locker.close_connection()\n            print(\"MySQL connection closed.\")\n\nif __name__ == \"__main__\":\n    print(\"Ensure MySQL server is running and database exists.\")\n    print(\"Set MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB environment variables if not using defaults.\")\n    run_task_with_lock()","lang":"python","description":"This quickstart demonstrates how to acquire and release a named MySQL-backed lock. It configures the MySQL connection using environment variables for security, attempts to acquire a lock with a timeout, performs a simulated task if successful, and ensures the lock is released and the connection closed."},"warnings":[{"fix":"Always specify a sensible `timeout` argument (e.g., `lock.acquire(timeout=60)`) to prevent indefinite waits and allow the application to handle lock contention gracefully. The `refresh_interval_secs` argument should also be set if locks are held for longer periods to keep the MySQL connection alive.","message":"The default `acquire()` method for a lock has a `timeout` of -1, meaning it will wait indefinitely for the lock to become available. This can lead to application processes hanging if the lock is held for a prolonged period or never released.","severity":"gotcha","affected_versions":"0.2.0 and earlier"},{"fix":"Implement robust error handling and ensure that `lock.release()` is called in a `finally` block. For critical operations, combine with application-level idempotency or retry mechanisms. Consider increasing `wait_timeout` on the MySQL server if connections are frequently idle and being dropped, or use `refresh_interval_secs` in `acquire()` to keep the connection active.","message":"PyMySQLLock uses MySQL's `GET_LOCK()` and `RELEASE_LOCK()` functions, which are tied to the specific MySQL connection that acquired them. If the application process holding a lock crashes or the underlying database connection is lost, the lock will be implicitly released by MySQL after its connection-level timeout, potentially leading to brief windows of unprotected access.","severity":"gotcha","affected_versions":"0.2.0 and earlier"},{"fix":"Each thread should establish its own separate MySQL connection (and thus its own `Locker` instance). Alternatively, use a connection pool that manages connections on a per-thread basis, ensuring a connection is used by only one thread at a time.","message":"Sharing a single MySQL database connection object across multiple threads within the same application process can lead to deadlocks, race conditions, or unexpected behavior, as most Python MySQL drivers are not thread-safe at the connection level.","severity":"gotcha","affected_versions":"All versions"}],"env_vars":null,"last_verified":"2026-04-16T00:00:00.000Z","next_check":"2026-07-15T00:00:00.000Z","problems":[{"fix":"This specific error often indicates an underlying transaction lock contention. While `PyMySQLLock` uses named locks, ensuring your application transactions are short and efficient can help. For `PyMySQLLock` itself, ensure the `timeout` parameter in `lock.acquire()` is set appropriately and consider increasing `innodb_lock_wait_timeout` on the MySQL server if application logic genuinely requires longer waits.","cause":"The MySQL server's `innodb_lock_wait_timeout` has been exceeded while attempting to acquire a lock or perform a database operation that requires a lock.","error":"sqlalchemy.exc.InternalError: (PyMySQL.err.InternalError) (1205, 'Lock wait timeout exceeded; try restarting transaction')"},{"fix":"This indicates expected contention. The application should implement a retry strategy with exponential backoff, log the failure, or inform the user that the resource is currently busy. Do not simply busy-wait, as this consumes CPU unnecessarily.","cause":"The `lock.acquire(timeout=X)` method returned `False` because another process or thread successfully held the named lock for the entire specified `X` duration.","error":"RuntimeError: Could not obtain named lock my_critical_task_lock within 10 seconds"},{"fix":"Ensure your MySQL connection is set to an appropriate isolation level (e.g., `READ COMMITTED`). For `PyMySQLLock`, if you are reusing the underlying connection for other database operations, you might need to commit (even if no writes) or explicitly close and re-open the connection to force it to see the latest state. `Locker` manages its own connection, but application code interacting with the same DB often faces this.","cause":"This is a common issue with long-lived database connections, where the connection's transaction isolation level or internal caching prevents it from seeing committed changes made by other connections without explicit refresh or re-connection.","error":"MySQL connection not reflecting external changes (e.g., `SELECT` queries returning stale data)"}]}