Skip to content

ndustrialio/node-distributed-lock

Repository files navigation

Node Distributed Lock

Build Package on npm semantic-release

Node Distributed Lock (NDL) is a client- and dialect-agnostic distributed locking tool for NodeJS applications. It can be used to orchestrate database migrations in a distributed setting or as a general distributed locking capability for any use.

Unlike other popular distributed locking tools that enforce a specific migration library or provide same-transaction locking, NDL can be used to lock across distributed nodes for the duration of your arbitriary action, whether it be another database write, a cleanup job, or even triggering a lambda function.

Note: A lock table will be created to store the locks in the database.

Currently, the following database clients/ORMs are supported. Please open an issue or contribute with your own PR to help grow the list:

Database Client Supported
Sequelize X
KnexJs X
node-postgres - Pool X

Currently, the following dialects are supported. Please open an issue or contribute with your own PR to help grow the list:

Database Dialect Supported
PostgreSQL X
MySQL
MariaDB
SQLite
Microsoft SQL Server

Installation

npm install @ndustrial/node-distributed-lock

Highlights

  • Works with the popular ORM Sequelize out of the box
  • Supports other query interfaces/database clients
  • Does not enforce same-transaction locking
  • Supports independent locks (locks are named)

Simple Example

Sequelize

// index.js
const Sequelize = require('sequelize');
const DistributedLock = require('@ndustrialio/node-distributed-lock');

const sequelize = new Sequelize({...});

const importantLogic = async () => {
  // will never run at the same time
}

const importantLock = new DistributedLock('my-important-logic-lock', {
  queryInterface: sequelize.queryInterface
});

importantLock
  .lock(importantLogic) // Ensures importantLogic is run by a single node at a time
  .then(() => {
    console.log('Done!');
  });

KnexJS

// index.js
const KnexJs = require('knex');
const DistributedLock = require('@ndustrialio/node-distributed-lock');

const knex = new KnexJs({...});

const importantLogic = async () => {
  // will never run at the same time
}

const importantLock = new DistributedLock('my-important-logic-lock', {
  queryInterface: knex
});

importantLock
  .lock(importantLogic) // Ensures importantLogic is run by a single node at a time
  .then(() => {
    console.log('Done!');
  });

PG Pool

// index.js
const { Pool } = require('pg');
const DistributedLock = require('@ndustrialio/node-distributed-lock');

const pool = new Pool({...});

const importantLogic = async () => {
  // will never run at the same time
}

const importantLock = new DistributedLock('my-important-logic-lock', {
  queryInterface: pool
});

importantLock
  .lock(importantLogic) // Ensures importantLogic is run by a single node at a time
  .then(() => {
    console.log('Done!');
  });

If the above code was scaled to >1 nodes, each node will create its own instance of the distributed lock and call the .lock() method; however, only one node will run the method importantLock at the same time. Once the logic is completed on one node, the next node will be able to obtain the lock and call the method.

It is important to note that, with current functionality, the importantLock() method will be called by every node, just never at the same time. In the world of database migrations, this is expected functionality, as the migration table will be up-to-date after the first execution.

Configuration

The DistributedLock(lockName, params) constructure allows some configuration to alter the behavior of the locking mechanism.

First and foremost, the lockName is used to ensure all distributed locks are locking on the same mutex. This enables you to create multiple locks in the same deployment and manage them independently.

The following values are passed in via the params argument.

Configuration Name Description Required Default
queryInterface The query interface database client X
queryInterfaceName The name of the query database client (i.e. sequelize, knex)
lockTableName The name of the table that will hold locks distributed_lock
lockTTLSeconds The time (seconds) after which locks should expire 1200
skipIfObtained Whether subsequent lock calls to an obtained lock should exit early false

NOTE: If you omit the queryInterfaceName, NDL will attempt to determine it based on the interface object.

NOTE: You can specify a database schema for the lock table by passing it as a prefix to the lockTableName (i.e. public.table). This schema will be created if it does not exist.

Single Execution

An option called skipIfObtained can be set to true in order to indicate that simultaneous lock calls on the same mutext should exit early if the lock mutex has already been obtained. This can be used to ensure that a single caller gains access to running the logic at any given time and subsequent callers will not run it immediately after.

Callers that have exited early due to this flag will resolve a result equal to the symbol DistributedLock.EXECUTION_SKIPPED.

License

See the LICENSE.