Hone logo
Hone
Problems

Implementing Pessimistic Locking for Resource Management

Imagine a scenario where multiple processes or threads need to access and modify a shared resource, such as a database record or a file. Without proper synchronization, concurrent access can lead to data corruption or inconsistent states. Pessimistic locking is a strategy to prevent these issues by acquiring a lock on the resource before accessing it, ensuring that only one entity can modify it at a time.

This challenge requires you to implement a basic pessimistic locking mechanism in Python that allows multiple entities to safely acquire and release locks on distinct resources.

Problem Description

Your task is to create a Python class, let's call it PessimisticLockManager, that manages locks for various resources. The lock manager should support the following operations:

  1. Acquire Lock: An entity (represented by an identifier) should be able to request a lock for a specific resource (also identified by a string or integer).
    • If the resource is not currently locked, the lock should be granted immediately to the requesting entity.
    • If the resource is already locked by another entity, the requesting entity should wait until the lock is released.
    • A resource can only be locked by one entity at a time.
  2. Release Lock: An entity that currently holds a lock on a resource should be able to release it.
    • Only the entity that acquired the lock can release it. Releasing a lock held by another entity should raise an error.
    • If a resource is not locked, attempting to release its lock should also result in an error.
  3. Check Lock Status: Be able to check if a resource is currently locked and, if so, by which entity.

Key Requirements:

  • The PessimisticLockManager should be thread-safe to handle concurrent access from multiple threads.
  • The locking mechanism should be "pessimistic" – assume contention and block until the lock is acquired.
  • Resources should be identified by unique keys (e.g., strings or integers).
  • Entities requesting locks should also have unique identifiers.

Expected Behavior:

  • When acquire_lock(resource_id, entity_id) is called and the resource_id is available, it should return True and the entity_id should be recorded as the owner of the lock.
  • When acquire_lock(resource_id, entity_id) is called and the resource_id is already locked by other_entity_id, the calling thread should block until other_entity_id calls release_lock(resource_id, other_entity_id). Upon release, the lock should be granted to entity_id.
  • When release_lock(resource_id, entity_id) is called and entity_id is the current owner of the lock for resource_id, the lock should be freed.
  • When release_lock(resource_id, entity_id) is called and entity_id is NOT the owner, a LockError (you'll need to define this custom exception) should be raised.
  • When is_locked(resource_id) is called, it should return True if the resource has an active lock, and False otherwise.
  • When get_lock_owner(resource_id) is called, it should return the entity_id of the current owner if locked, or None if not locked.

Edge Cases to Consider:

  • Multiple entities trying to acquire the same lock simultaneously.
  • An entity trying to release a lock it doesn't own.
  • An entity trying to release a lock that is not held.
  • Resource IDs that are not currently managed by the lock manager.

Examples

Example 1:

from threading import Thread

lock_manager = PessimisticLockManager()

def worker(entity_id, resource_id):
    print(f"Entity {entity_id} trying to acquire lock for {resource_id}")
    if lock_manager.acquire_lock(resource_id, entity_id):
        print(f"Entity {entity_id} acquired lock for {resource_id}")
        # Simulate doing some work
        import time
        time.sleep(1)
        print(f"Entity {entity_id} releasing lock for {resource_id}")
        lock_manager.release_lock(resource_id, entity_id)
        print(f"Entity {entity_id} released lock for {resource_id}")

resource = "document.txt"
thread1 = Thread(target=worker, args=("EntityA", resource))
thread2 = Thread(target=worker, args=("EntityB", resource))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"Is {resource} locked? {lock_manager.is_locked(resource)}")
print(f"Owner of {resource}: {lock_manager.get_lock_owner(resource)}")

Expected Output (order might vary slightly due to thread scheduling, but the locking behavior will be consistent):

Entity EntityA trying to acquire lock for document.txt
Entity EntityA acquired lock for document.txt
Entity EntityB trying to acquire lock for document.txt
Entity EntityA releasing lock for document.txt
Entity EntityA released lock for document.txt
Entity EntityB acquired lock for document.txt
Entity EntityB releasing lock for document.txt
Entity EntityB released lock for document.txt
Is document.txt locked? False
Owner of document.txt: None

Explanation: EntityA successfully acquires the lock first. EntityB attempts to acquire the lock but has to wait. EntityA performs its simulated work and then releases the lock. Once the lock is released, EntityB can then acquire it, perform its work, and release it.

Example 2:

lock_manager = PessimisticLockManager()
resource_id = "printer_queue"

# EntityC attempts to release a lock it doesn't own
try:
    lock_manager.release_lock(resource_id, "EntityC")
except LockError as e:
    print(f"Caught expected error: {e}")

# EntityD acquires the lock
lock_manager.acquire_lock(resource_id, "EntityD")
print(f"Owner of {resource_id}: {lock_manager.get_lock_owner(resource_id)}")

# EntityE attempts to release the lock owned by EntityD
try:
    lock_manager.release_lock(resource_id, "EntityE")
except LockError as e:
    print(f"Caught expected error: {e}")

# EntityD releases its own lock
lock_manager.release_lock(resource_id, "EntityD")
print(f"Owner of {resource_id} after release: {lock_manager.get_lock_owner(resource_id)}")

Expected Output:

Caught expected error: Cannot release lock for 'printer_queue': Entity 'EntityC' does not own the lock.
Owner of printer_queue: EntityD
Caught expected error: Cannot release lock for 'printer_queue': Entity 'EntityE' does not own the lock.
Owner of printer_queue after release: None

Explanation: The first release_lock call fails because the resource is not locked. The second release_lock call fails because "EntityE" is not the owner of the lock. The third release_lock call succeeds as "EntityD" releases its own lock.

Constraints

  • The implementation should use Python's standard threading module.
  • The PessimisticLockManager should be initialized without any arguments.
  • Resource IDs and Entity IDs can be any hashable Python object (strings, integers, etc.).
  • The solution should be efficient in terms of acquiring and releasing locks, aiming for O(1) average time complexity for these operations, excluding waiting time.

Notes

  • Consider using threading.Lock for thread safety within your PessimisticLockManager class.
  • You'll need to manage the state of locks for multiple resources. A dictionary might be a good data structure for this.
  • Think about how to implement the blocking behavior for acquire_lock when a resource is already locked. threading.Condition could be very useful here.
  • Define a custom exception, LockError, to handle specific locking-related errors.
  • The problem statement implies that resources are identified by keys. If a resource is requested for the first time (either to acquire or check status), it should be treated as unlocked.
Loading editor...
python