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:
- 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.
- 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.
- Check Lock Status: Be able to check if a resource is currently locked and, if so, by which entity.
Key Requirements:
- The
PessimisticLockManagershould 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 theresource_idis available, it should returnTrueand theentity_idshould be recorded as the owner of the lock. - When
acquire_lock(resource_id, entity_id)is called and theresource_idis already locked byother_entity_id, the calling thread should block untilother_entity_idcallsrelease_lock(resource_id, other_entity_id). Upon release, the lock should be granted toentity_id. - When
release_lock(resource_id, entity_id)is called andentity_idis the current owner of the lock forresource_id, the lock should be freed. - When
release_lock(resource_id, entity_id)is called andentity_idis NOT the owner, aLockError(you'll need to define this custom exception) should be raised. - When
is_locked(resource_id)is called, it should returnTrueif the resource has an active lock, andFalseotherwise. - When
get_lock_owner(resource_id)is called, it should return theentity_idof the current owner if locked, orNoneif 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
PessimisticLockManagershould 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.Lockfor thread safety within yourPessimisticLockManagerclass. - 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_lockwhen a resource is already locked.threading.Conditioncould 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.