Postgresql provides a variety of ways to lock a row, but if you are looking to increase throughput, holding onto a valuable connection (even with a pooler) just for a lock isn’t ideal.
ZooKeeper is a popular distributed locking solution, and is relatively straightforward to run (particularly if you don’t mind risking a single node).
If you want a traditional lock, with a queue, you need a sequence node; but if you’re happy to bail out when the lock is already taken (equiv to SELECT ... FOR UPDATE NOWAIT
) then you only need a single ephemeral lock node.
This client lib mimics the java API, but it’s simple to add a wrapper making it more idiomatic:
const {promisify} = require('util');
const zookeeper = require('node-zookeeper-client');
module.exports = function({ uri }) {
this.connect = function() {
return new Promise((resolve) => {
var client = zookeeper.createClient(uri);
client.once('connected', function () {
resolve({
create: function(path) {
return promisify(client.create).bind(client)(path, null, zookeeper.ACL.OPEN_ACL_UNSAFE,
zookeeper.CreateMode.EPHEMERAL);
},
close: client.close.bind(client),
});
});
client.connect();
});
};
};
And use this to hold a lock, while awaiting a promise:
module.exports = function(zookeeper, errorCodes) {
return async function withLock(id, cb) {
const client = await zookeeper.connect();
try {
const nodeName = `/foo_${id}`;
await client.create(nodeName);
const res = await cb();
return res;
} catch (err) {
if (err.name === "NODE_EXISTS") {
throw ...;
}
throw err;
} finally {
client.close();
}
};
};