python_dynamodb_lock package

The package contains a single module - with the same name i.e. python_dynamodb_lock

python_dynamodb_lock module

This is a general purpose distributed locking library built on top of DynamoDB. It is heavily “inspired” by the java-based AmazonDynamoDBLockClient library, and supports both coarse-grained and fine-grained locking.

class DynamoDBLockClient(dynamodb_resource, table_name='DynamoDBLockTable', partition_key_name='lock_key', sort_key_name='sort_key', ttl_attribute_name='expiry_time', owner_name=None, heartbeat_period=datetime.timedelta(0, 5), safe_period=datetime.timedelta(0, 20), lease_duration=datetime.timedelta(0, 30), expiry_period=datetime.timedelta(0, 3600), heartbeat_tps=-1, app_callback_executor=None)[source]

Bases: object

Provides distributed locks using DynamoDB’s support for conditional reads/writes.

Parameters:
  • dynamodb_resource (boto3.ServiceResource) – mandatory argument
  • table_name (str) – defaults to ‘DynamoDBLockTable’
  • partition_key_name (str) – defaults to ‘lock_key’
  • sort_key_name (str) – defaults to ‘sort_key’
  • ttl_attribute_name (str) – defaults to ‘expiry_time’
  • owner_name (str) – defaults to hostname + _uuid
  • heartbeat_period (datetime.timedelta) – How often to update DynamoDB to note that the instance is still running. It is recommended to make this at least 4 times smaller than the leaseDuration. Defaults to 5 seconds.
  • safe_period (datetime.timedelta) – How long is it okay to go without a heartbeat before considering a lock to be in “danger”. Defaults to 20 seconds.
  • lease_duration (datetime.timedelta) – The length of time that the lease for the lock will be granted for. i.e. if there is no heartbeat for this period of time, then the lock will be considered as expired. Defaults to 30 seconds.
  • expiry_period (datetime.timedelta) – The fallback expiry timestamp to allow DynamoDB to cleanup old locks after a server crash. This value should be significantly larger than the _lease_duration to ensure that clock-skew etc. are not an issue. Defaults to 1 hour.
  • heartbeat_tps (int) – The number of heartbeats to execute per second (per node) - this will have direct correlation to DynamoDB provisioned throughput for writes. If set to -1, the client will distribute the heartbeat calls evenly over the _heartbeat_period - which uses lower throughput for smaller number of locks. However, if you want a more deterministic heartbeat-call-rate, then specify an explicit TPS value. Defaults to -1.
  • app_callback_executor (ThreadPoolExecutor) – The executor to be used for invoking the app_callbacks in case of un-expected errors. Defaults to a ThreadPoolExecutor with a maximum of 5 threads.
acquire_lock(partition_key, sort_key='-', retry_period=None, retry_timeout=None, additional_attributes=None, app_callback=None)[source]

Acquires a distributed DynaomDBLock for the given key(s).

If the lock is currently held by a different client, then this client will keep retrying on a periodic basis. In that case, a few different things can happen:

  1. The other client releases the lock - basically deleting it from the database
    Which would allow this client to try and insert its own record instead.
  2. The other client dies, and the lock stops getting updated by the heartbeat thread.
    While waiting for a lock, this client keeps track of the local-time whenever it sees the lock’s record-version-number change. From that point-in-time, it needs to wait for a period of time equal to the lock’s lease duration before concluding that the lock has been abandoned and try to overwrite the database entry with its own lock.
  3. This client goes over the max-retry-timeout-period
    While waiting for the other client to release the lock (or for the lock’s lease to expire), this client may go over the retry_timeout period (as provided by the caller) - in which case, a DynamoDBLockError with code == ACQUIRE_TIMEOUT will be thrown.
  4. Race-condition amongst multiple lock-clients waiting to acquire lock
    Whenever the “old” lock is released (or expires), there may be multiple “new” clients trying to grab the lock - in which case, one of those would succeed, and the rest of them would get a “conditional-update-exception”. This is just logged and swallowed internally - and the client moves on to another sleep-retry cycle.
  5. Any other error/exception
    Would be wrapped inside a DynamoDBLockError and raised to the caller.
Parameters:
  • partition_key (str) – The primary lock identifier
  • sort_key (str) – Forms a “composite identifier” along with the partition_key. Defaults to ‘-‘
  • retry_period (datetime.timedelta) – If the lock is not immediately available, how long should we wait between retries? Defaults to heartbeat_period.
  • retry_timeout (datetime.timedelta) – If the lock is not available for an extended period, how long should we keep trying before giving up and timing out? This value should be set higher than the lease_duration to ensure that other clients can pick up locks abandoned by one client. Defaults to lease_duration + heartbeat_period.
  • additional_attributes (dict) – Arbitrary application metadata to be stored with the lock
  • app_callback (Callable) – Callback function that can be used to notify the app of lock entering the danger period, or an unexpected release
Return type:

DynamoDBLock

Returns:

A distributed lock instance

release_lock(lock, best_effort=True)[source]

Releases the given lock - by deleting it from the database.

It allows the caller app to indicate whether it wishes to be informed of all errors/exceptions, or just have the lock-client swallow all of them. A typical usage pattern would include acquiring the lock, making app changes, and releasing the lock. By the time the app is releasing the lock, it would generally be too late to respond to any errors encountered during the release phase - but, the app may still wish to get informed and log it somewhere of offline re-conciliation/follow-up.

Parameters:
  • lock (DynamoDBLock) – The lock instance that needs to be released
  • best_effort (bool) – If True, any exception when calling DynamoDB will be ignored and the clean up steps will continue, hence the lock item in DynamoDb might not be updated / deleted but will eventually expire. Defaults to True.
close(release_locks=False)[source]

Shuts down the background thread - and releases all locks if so asked.

By default, this method will NOT release all the locks - as releasing the locks while the application is still making changes assuming that it has the lock can be dangerous. As soon as a lock is released by this client, some other client may pick it up, and the associated app may start processing the underlying business entity in parallel.

It is recommended that the application manage its shutdown-lifecycle such that all the worker threads operating under these locks are first terminated (committed or rolled-back), the corresponding locks released (one at a time - by each worker thread), and then the lock_client.close() method is called. Alternatively, consider letting the process die without releasing all the locks - they will be auto-released when their lease runs out after a while.

Parameters:release_locks (bool) – if True, releases all the locks. Defaults to False.
classmethod create_dynamodb_table(dynamodb_client, table_name='DynamoDBLockTable', partition_key_name='lock_key', sort_key_name='sort_key', ttl_attribute_name='expiry_time', read_capacity=5, write_capacity=5)[source]

Helper method to create the DynamoDB table

Parameters:
  • dynamodb_client (boto3.DynamoDB.Client) – mandatory argument
  • table_name (str) – defaults to ‘DynamoDBLockTable’
  • partition_key_name (str) – defaults to ‘lock_key’
  • sort_key_name (str) – defaults to ‘sort_key’
  • ttl_attribute_name (str) – defaults to ‘expiry_time’
  • read_capacity (int) – the max TPS for strongly-consistent reads; defaults to 5
  • write_capacity (int) – the max TPS for write operations; defaults to 5
class BaseDynamoDBLock(partition_key, sort_key, owner_name, lease_duration, record_version_number, expiry_time, additional_attributes)[source]

Bases: object

Represents a distributed lock - as stored in DynamoDB.

Typically used within the code to represent a lock held by some other lock-client.

Parameters:
  • partition_key (str) – The primary lock identifier
  • sort_key (str) – If present, forms a “composite identifier” along with the partition_key
  • owner_name (str) – The owner name - typically from the lock_client
  • lease_duration (float) – The lease duration in seconds - typically from the lock_client
  • record_version_number (str) – A “liveness” indicating GUID - changes with every heartbeat
  • expiry_time (int) – Epoch timestamp in seconds after which DynamoDB will auto-delete the record
  • additional_attributes (dict) – Arbitrary application metadata to be stored with the lock
class DynamoDBLock(partition_key, sort_key, owner_name, lease_duration, record_version_number, expiry_time, additional_attributes, app_callback, lock_client)[source]

Bases: python_dynamodb_lock.python_dynamodb_lock.BaseDynamoDBLock

Represents a lock that is owned by a local DynamoDBLockClient instance.

Parameters:
  • partition_key (str) – The primary lock identifier
  • sort_key (str) – If present, forms a “composite identifier” along with the partition_key
  • owner_name (str) – The owner name - typically from the lock_client
  • lease_duration (float) – The lease duration - typically from the lock_client
  • record_version_number (str) – Changes with every heartbeat - the “liveness” indicator
  • expiry_time (int) – Epoch timestamp in seconds after which DynamoDB will auto-delete the record
  • additional_attributes (dict) – Arbitrary application metadata to be stored with the lock
  • app_callback (Callable) – Callback function that can be used to notify the app of lock entering the danger period, or an unexpected release
  • lock_client (DynamoDBLockClient) – The client that “owns” this lock
PENDING = 'PENDING'
LOCKED = 'LOCKED'
RELEASED = 'RELEASED'
IN_DANGER = 'IN_DANGER'
INVALID = 'INVALID'
release(best_effort=True)[source]

Calls the lock_client.release_lock(self, True) method

Parameters:best_effort (bool) – If True, any exception when calling DynamoDB will be ignored and the clean up steps will continue, hence the lock item in DynamoDb might not be updated / deleted but will eventually expire. Defaults to True.
exception DynamoDBLockError(code='UNKNOWN', message='Unknown error')[source]

Bases: Exception

Wrapper for all kinds of errors that might occur during the acquire and release calls.

CLIENT_SHUTDOWN = 'CLIENT_SHUTDOWN'
ACQUIRE_TIMEOUT = 'ACQUIRE_TIMEOUT'
LOCK_NOT_OWNED = 'LOCK_NOT_OWNED'
LOCK_STOLEN = 'LOCK_STOLEN'
LOCK_IN_DANGER = 'LOCK_IN_DANGER'
UNKNOWN = 'UNKNOWN'