Skip to content

Instantly share code, notes, and snippets.

@Munawwar
Last active November 22, 2023 19:11
Show Gist options
  • Save Munawwar/85e204b579f9b8b13a496f72437ba928 to your computer and use it in GitHub Desktop.
Save Munawwar/85e204b579f9b8b13a496f72437ba928 to your computer and use it in GitHub Desktop.
Distributed Lock with Dynamodb & node.js
// MIT License 2021 - https://opensource.org/licenses/MIT
const delay = require('delay');
/**
* @param {AWS.DynamoDB.DocumentClient} dynamodbDocumentClient
* @param {string} tableName
* @param {string} ownerId a lock's gotta have an owner!
* @param {Object} [options]
* @param {string} [options.hashKeyProperty='key'] default value is 'key'
* @param {string} [options.hashKeyValue='lock'] default value is 'lock'
* @param {string} [options.ownerIdProperty='owner'] default value is 'owner'
* @param {number} [options.retryDelay=200] default value is 200ms
* @returns {Promise<() => Promise<boolean>} returns an 'unlock' function
*/
async function dynamoLock(
dynamodbDocumentClient,
tableName,
ownerId,
{
retryDelay = 200,
hashKeyProperty = 'key',
hashKeyValue = 'lock',
ownerIdProperty = 'owner',
} = {},
) {
if (!ownerId) throw new Error('dynamoLock call needs an owner id to reserve a lock.');
let unlock;
while (!unlock) {
try {
await dynamodbDocumentClient.put({
TableName: tableName,
Item: {
[hashKeyProperty]: hashKeyValue,
[ownerIdProperty]: ownerId,
},
ConditionExpression: '#ownerIdProperty = :empty OR attribute_not_exists(#ownerIdProperty)',
ExpressionAttributeNames: {
"#ownerIdProperty": ownerIdProperty
},
ExpressionAttributeValues: {
':empty': '',
},
}).promise();
unlock = async () => {
try {
await dynamodbDocumentClient.put({
TableName: tableName,
Item: {
[hashKeyProperty]: hashKeyValue,
[ownerIdProperty]: '',
},
ConditionExpression: `#ownerIdProperty = :ownerId`,
ExpressionAttributeNames: {
"#ownerIdProperty": ownerIdProperty
},
ExpressionAttributeValues: {
':ownerId': ownerId,
},
}).promise();
return true;
} catch (err) {
// could not unlock? deadlock? you will need to delete the lock manually from db
return false;
}
};
} catch (err) {
console.log(err);
await delay(retryDelay);
}
}
return unlock;
}
module.exports = dynamoLock;
const AWS = require('aws-sdk');
const dynamodbClient = new AWS.DynamoDB.DocumentClient();
const dynamoLock = require('./dynamo-lock');
(async () => {
// process 1
const process1Id = 'lambda1';
const p1 = dynamoLock(dynamodbClient, 'Test', process1Id)
.then(async (unlock) => {
console.log('process 1 has got the lock. now unlocking...');
await unlock();
console.log('unlocked');
});
// process 2
const process2Id = 'lambda2';
const p2 = dynamoLock(dynamodbClient, 'Test', process2Id)
.then(async (unlock) => {
console.log('process 2 has got the lock. now unlocking...');
await unlock();
console.log('unlocked');
});
await Promise.all([p1, p2]);
})();
@Munawwar
Copy link
Author

Munawwar commented Jul 13, 2021

A day comes when your lock doesn't get released (because of server crashing or whatever reason). So this solution is not good enough, without a TTL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment