Python DynamoDB Lock
Python DynamoDB Lock is a general-purpose distributed locking library built on top of DynamoDB. It supports both coarse-grained and fine-grained locking, heavily inspired by the Java-based AmazonDynamoDBLockClient. The current version is 0.9.1, with the last release in October 2018. While functional, its development appears to be in maintenance mode.
Warnings
- breaking The library's last release was in October 2018. While functional, it may not receive active updates or support for newer Python versions/AWS features.
- gotcha This library does NOT participate in distributed transactions. If an application holding a lock experiences prolonged delays (e.g., GC pauses, errors) preventing heartbeats, another client might assume the lock is abandoned and acquire it. The original client might then commit changes even after its lock has been 'stolen', leading to data inconsistencies.
- gotcha Sudden process termination of a client will leave its acquired locks in DynamoDB until they eventually expire based on their Time-To-Live (TTL) attribute. This expiry is not immediate and can take up to 24 hours.
- gotcha Calling `lock_client.close()` by default does NOT release all locks owned by that client. This design prevents premature lock release while application logic might still be processing under the assumption of holding the lock.
- gotcha The DynamoDB table used for locking must be configured with a primary key (default: 'lock_key' of type String) and it is highly recommended to enable and configure a TTL attribute (default: 'expiry_time') to allow DynamoDB to automatically clean up old/abandoned lock entries.
Install
-
pip install python-dynamodb-lock
Imports
- DynamoDBLockClient
from python_dynamodb_lock import DynamoDBLockClient
Quickstart
import boto3
from python_dynamodb_lock import DynamoDBLockClient
import os
import time
# Configure AWS credentials and region (e.g., via environment variables or ~/.aws/credentials)
# For local testing, you might use 'local' endpoint_url
dynamodb_resource = boto3.resource(
'dynamodb',
region_name=os.environ.get('AWS_REGION', 'us-east-1'),
endpoint_url=os.environ.get('DYNAMODB_ENDPOINT_URL', None)
)
table_name = os.environ.get('DYNAMODB_LOCK_TABLE_NAME', 'MyDistributedLockTable')
# It's recommended to create the table beforehand with a 'lock_key' primary key and TTL enabled
# For demonstration, we'll try to create it if it doesn't exist
try:
table = dynamodb_resource.Table(table_name)
table.load()
except dynamodb_resource.meta.client.exceptions.ResourceNotFoundException:
print(f"Creating DynamoDB table: {table_name}")
dynamodb_resource.create_table(
TableName=table_name,
KeySchema=[{'AttributeName': 'lock_key', 'KeyType': 'HASH'}],
AttributeDefinitions=[{'AttributeName': 'lock_key', 'AttributeType': 'S'}],
BillingMode='PAY_PER_REQUEST'
)
# Wait for the table to be active
dynamodb_resource.meta.client.get_waiter('table_exists').wait(TableName=table_name)
print(f"Table {table_name} created.")
# Instantiate the lock client
# The 'owner_name' helps identify who holds the lock (e.g., hostname + process ID)
lock_client = DynamoDBLockClient(
dynamodb_resource,
table_name=table_name,
owner_name=f"{os.uname()[1]}-{os.getpid()}"
)
lock_key = "my-critical-resource"
try:
print(f"Attempting to acquire lock for {lock_key}...")
# Use the 'lock' method as a context manager for a specific lock
with lock_client.lock(lock_key):
print(f"Successfully acquired lock for {lock_key}. Performing critical section...")
# Simulate work
time.sleep(5)
print(f"Finished critical section for {lock_key}.")
print(f"Lock for {lock_key} automatically released.")
except Exception as e:
print(f"Failed to acquire or process with lock for {lock_key}: {e}")
finally:
# It's important to close the client to stop background heartbeat threads
print("Closing lock client...")
lock_client.close()
print("Lock client closed.")