Mutex

This project implements mutual exclusion logic that allows a resource to be locked per-user. This feature is used to block more than one user from editing the same document draft at the same time. Note that since the lock is per user, it won’t stop performing the same action in two separate browser tabs while being logged in to the same account. Single user can also hold multiple locks (to multiple documents).

Resources to lock are typically identified by their UID (unique identified). The uid field is present in all models extending BaseModel. The lock information is stored in Redis database determined by REDIS_DB_MUTEX.

Locks have a timeout. The lock needs to be renewed by acquiring it again to maintain the resource lock. When the user has finished with the object, the lock can be explicitly released, or it will be release automatically after the timeout period.

Note

Note that the locking an object does not block it from being read or written. It signals that the object is in use to other users who attempt to acquire a lock.

Python code

mutex = Mutex(object.uid, timedelta(minutes=1), user)

try:
    mutex.acquire()
    print("You have a lock on the object for the next minute. Please Re-acquire the lock if you need to extend it, or release when no longer needed.")
except ResourceLocked as e:
    print(f"Resource already in use by {e.user}. Please try again in 1 minute")

...
# lock can be released whether it was acquired or not
mutex.release()
exception utils.mutex.MutexError
exception utils.mutex.RedisNotSetUp

Exception for when redis connection is not set-up (host string is not set). Depending on the context, handlers of this error can show the user an error message, and default to the same behaviour as if the resource was already locked, or let the user proceed with caution.

In practice this error is only expected in development, when redis is not set-up.

exception utils.mutex.ResourceLocked(resource_id: str, value: str)

Exception raised when a resource is already locked by someone else

property user_id: int

ID of the user holding the lock

profile_id

Profile ID (Auditor) of the user holding the lock

class utils.mutex.Mutex(resource_uid: Any, timeout: timedelta, user: User)

Implements mutual exclusion (Semaphore) for arbitrary resources by using Redis as its backend.

Wraps Redis in commands to acquire and release lock methods that raise ResourceLocked if the resource is already locked.

Parameters:
  • resource_uid – UID of the object to lock

  • timeout – timeout after which the lock will expire automatically

  • user – the user who acquires the lock

check_lock() bool

Checks the status of the resource lock, whether it’s locked by current user, someone else, or not locked.

Returns:

True if resource is locked by current user, or False if it’s not locked at all.

Raises:
acquire()

Acquires or refreshes the lock.

Raises:
  • ResourceLocked – if resource is already locked by someone else

  • RedisNotSetUp – if redis connection required by Mutex is not set-up

release()

Releases the lock if acquired by the current user. Does not raise any exceptions if resource is locked by someone else. Fails silently if Redis is not setup.

REST API

Lock can be acquired and released by making a POST (acquire) or DELETE (release) request to the API endpoint: /api/lock/{resource-uid}/

POST

Acquires lock and responds with 200 status code if successful, or 423 if resource is already locked by another user (the response will contain the lock details).

If the resource is already locked by the same user, the lock is renewed and 200 status code returned.

Response json if lock acquisition has failed.
{
    "message": "Resource is already in use by another user",
    "user_id": 1
}
DELETE

Releases the lock if the current user has it. Always responds with a successful 204 status.