Using ZooKeeper for locking

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();
        }
    };
};