Skip to content

About

A lightweight MongoDB-backed advisory lock so a cron job on every instance of a cluster only runs once

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

mongo-job-lock

A tiny, dependency-free advisory lock backed by MongoDB, for cron jobs registered on every instance of a multi-process deploy (PM2 cluster, multiple servers) that must only actually run on one instance at a time.

No new infrastructure — it stores one small document per lock in a MongoDB collection you already have a connection to.

How it works

Acquiring a lock is a single atomic upsert: it succeeds when no lock document exists, or the existing one has expired (steal-on-expiry, so a crashed holder can't deadlock the job forever). If a live, unexpired lock already exists, the upsert hits a duplicate-key error, which is treated as "not acquired" — no exception bubbles up to your job.

Install

npm install mongo-job-lock

Works with either the native MongoDB driver or Mongoose — you just need to hand it a Db instance.

Usage

With Mongoose

const mongoose = require('mongoose');
const { createJobLock } = require('mongo-job-lock');

const withJobLock = createJobLock({ db: mongoose.connection.db });

// In your cron handler, registered on every instance:
cron.schedule('30 2 * * *', async () => {
  const { ran } = await withJobLock('nightly-report-email', 30 * 60 * 1000, async () => {
    await sendNightlyReports();
  });
  if (!ran) console.log('another instance is already running this job');
});

With the native MongoDB driver

const { MongoClient } = require('mongodb');
const { createJobLock } = require('mongo-job-lock');

const client = await MongoClient.connect(uri);
const withJobLock = createJobLock({ db: client.db('myapp') });

API

createJobLock(options)

Option Type Default Description
db Db required A MongoDB Db instance.
collectionName string 'joblocks' Collection used to store lock documents.
onInfo (msg) => void no-op Called when a run is skipped because another instance holds the lock.
onError (msg) => void no-op Called if releasing the lock fails (best-effort; never thrown).

Returns withJobLock(name, ttlMs, fn):

  • name — a unique lock name (e.g. 'nightly-report-email').
  • ttlMs — lock lifetime; should comfortably exceed the job's max runtime, so a slow-but-alive run doesn't get its lock stolen out from under it.
  • fn — the async work to run while holding the lock.

Returns { ran: boolean, result?: any } — ran is false when another instance already holds the lock (fn was never called); otherwise result is whatever fn resolved to. If fn throws, withJobLock rejects with that same error (after still releasing the lock).

Picking a TTL

Pick ttlMs a comfortable margin above how long the job normally takes — long enough that a healthy run never has its lock stolen mid-flight, short enough that a crashed instance doesn't block the job for too long before the next scheduled run steals it back. A job that runs every 24h and normally takes 5 minutes might use a 30-60 minute TTL, for example.

License

MIT

About

A lightweight MongoDB-backed advisory lock so a cron job on every instance of a cluster only runs once

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages