Need to synchronize a bunch of workers so that only 1 does a particular thing? If yes,
then you need a lock file. This package uses a cache store (redis
or keydb
)
to create records atomically that only 1 instance will "own."
This library covers most common use cases for exclusion when working with multiple instances of the same application / container:
- Exclusive initialization: have one of the application instances perform a task on init.
- Periodic checks: Have one of the application instances perform a task periodically.
For long running tasks and rare edge cases, it could be possible that the lock is perceived as acquired by more than one instance. This library does not provide strict guarantees for exclusion for tasks that must be processed exactly once in a non-idempotent manner (e.g. appending an item in order processing). Consider using upserts/cas on your persistent DB in those cases or using a dedicated solution for exclusion like etcd / zookeeper.
npm install @logdna/exclusive-lock --save
const ExclusiveLock = require('@logdna/exclusive-lock')
const cache_connection = require('./my-keydb-connection.js')
const pino = require('pino')
const exclusive_lock = new ExclusiveLock({
name: 'my-distributed-app'
, log: pino()
, cache_manager
})
async function main() {
const acquired = await exclusive_lock.acquire()
if (acquired) {
log.info('You win the race! Lock acquired')
}
exclusive_lock.release()
}
main()
As a feature, exclusive-lock
will start a timer to automatically refresh the TTL on
the lock file, as it's assumed that the lock holder will always want to hold the lock
until explicitly released. This feature is handled by a setInterval
timer and can
encouter errors asynchronously. See the error
event for details on that.
- The timer is not started until a lock is successfully
acquired
. - For clean shutdown, always
release()
an acquired lock so that the timer is canceled. refreshed
is emitted for every successful refresh.
-
options
<Object>
name
<String>
- Required A unique string to identify your applicationcache_connection
<Object>
- Required A connection to the cache, either Redis or Keydblog
<Object>
- A optional logger instance such aspino
. Must support levelsinfo
,warn
,error
,debug
. Default: abstract-logginglock_ttl_ms
<Number>
- Optional. Specify a TTL in milliseconds for the lock. Default: 3000lock_refres_ms
<Number>
- Optional. specify a time in milliseconds for refreshing the lock on an interval. Default: 1000lock_contents
<String>
- Optional. Specify the string contents to put in the lock file, e.g. a server/instance name.
Throws:
<Error>
for validation errors
Attempts to exclusively acquire a lock based on the given name
. If multiple
instances are competing, only 1 will win the lock.
Returns: Promise<Boolean>
if the lock was a success
Emits: acquired
Returns an object containing the remaining TTL (if any) on the lock, and the serialized lock contents. Useful if lock_contents
was used to store
valuable information in the lock, or to analyze the TTL. Any client can inspect
the lock regardless of who acquired it.
If there is no lock, null
is returned
Returns: Promise<Object<lock_ttl_ms: Number, lock_contents: Object|Number|Boolean|String>>
An object containing the remaining TTL (in milliseconds), plus the contents of the lock
Throws: <Error>
for cache store pipeline
errors
Unlock based on name
. Idempotent. Instances that do not have the lock will
be a no-op.
Returns: Promise<undefined>
Emits: released
key
<String>
- The key value of the lock stored in cache
This event is emitted after a successful lock is acquired. Because the acquire()
method is async
, there's no real need to listen for this, but it has been added
to remain consistent with the 'released'
event which can happen asynchronously.
<Object>
- The payload object
This event is emitted after a lock's TTL is successfully refreshed.
key
<String>
- The key value of the lock stored in cache
This event is emitted when a lock is released.
err
<Error>
- A thrown error that occurred
This event is thrown when errors are encountered in the TTL refresh timer. In this case, the error is emitted and the following actions are taken:
release()
will automatically be called. This is so that the lock does not become expired leaving the lock holder thinking that it's still acquired. Users should listen for these events and act accordingly.- If the above
release()
also fails, an'error'
event will be emitted again with those details. At that point, the instance will no longer think the lock is acquired, but its record will remain until the TTL expires. This is because the failure inrelease()
will be because of a failedcache.del()
operation, thus leaving the key.
Copyright © 2022 Mezmo, released under an MIT license. See the LICENSE file and https://opensource.org/licenses/MIT
This project is open-sourced, and accepts PRs from the public for bugs or feature enhancements. These are the guidelines for contributing:
- The project uses Commitlint and enforces Conventional Commit Standard. Please format your commits based on these guidelines.
- An issue must be opened in the repository for any bug, feature, or anything else that will have a PR
- The commit message must reference the issue with an acceptable action tag in the commit footer, e.g.
Fixes: #5
- The commit message must reference the issue with an acceptable action tag in the commit footer, e.g.