So at the end I wrote my own locker functions. The idea is to have a second dynamodb table which only contains one entry. Using the conditionExpression it will be only set a new entry if it doesn't exist any, or was already expired the same one (the id of the table is lock_name, which you should always send the same one from every lambda, not lock_id! lock_id controls within the same lambda to free the lock)
Then, you free the item by removing it by its id. You can try acquiring the lock with some while sleep for several times until it fails.
def acquire_lock(lock_name: str, lock_id: str) -> bool:
"""
Try to acquire a distributed lock.
Args:
lock_name (str): Name of the lock (e.g., 'step-function-scheduler').
lock_id (str): Unique identifier for this lock acquisition attempt.
Returns:
bool: True if lock was successfully acquired, False otherwise.
"""
expires_at = datetime.utcnow() + timedelta(seconds=LOCK_TIMEOUT_SECONDS)
try:
lock_table.put_item(
Item={
'lock_name': lock_name,
'lock_id': lock_id,
'expires_at': int(expires_at.timestamp()), # Unix timestamp for TTL
'created_at': datetime.utcnow().isoformat()
},
ConditionExpression='attribute_not_exists(lock_name) OR expires_at < :now',
ExpressionAttributeValues={
':now': int(datetime.utcnow().timestamp())
}
)
logger.info(f"Successfully acquired lock: {lock_name} with id: {lock_id}")
return True
except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
logger.info(f"Failed to acquire lock: {lock_name} (already held by another instance)")
return False
except Exception as e:
logger.error(f"Error acquiring lock: {e}")
return False
def release_lock(lock_name: str, lock_id: str) -> bool:
"""
Release a distributed lock.
Args:
lock_name (str): Name of the lock to release.
lock_id (str): The lock_id that was used to acquire the lock.
Returns:
bool: True if lock was successfully released, False otherwise.
"""
try:
lock_table.delete_item(
Key={'lock_name': lock_name},
ConditionExpression='lock_id = :lock_id',
ExpressionAttributeValues={':lock_id': lock_id}
)
logger.info(f"Successfully released lock: {lock_name}")
return True
except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
logger.warning(f"Lock {lock_name} was not held by this instance (lock_id: {lock_id})")
return False
except Exception as e:
logger.error(f"Error releasing lock: {e}")
return False