npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@kontsedal/locco

v0.1.0

Published

Distributed locks library

Downloads

4,415

Readme

Buuild and Test Coverage Badge

locco

A small and simple library to deal with race conditions in distributed systems by applying locks on resources. Currently, supports locking via Redis, MongoDB, and in-memory object.

Installation

npm i @kontsedal/locco

Core logic

With locks, user can just say "I'm doing some stuff with this user, please lock him and don't allow anybody to change him" and no one will, till a lock is valid.

The core logic is simple. When we create a lock we generate a unique string identifying a current lock operation. Then, we search for a valid lock with a same key in the storage(Redis, Mongo, js object) and if it doesn't exist we add one and proceed. If a valid lock already exists we retry this operation for some time and then fail.

When we release or extend a lock, we check that lock exists in the storage and has the same unique identifier with a current lock. It makes impossible to release or extend other process lock.

Usage

There are two ways to create a resource lock. In the first one, you should manually lock and unlock a resource. Here is an example with a Redis:

import { Locker, IoRedisAdapter } from "@kontsedal/locco";
import Redis from "ioredis";

const redisAdapter = new IoRedisAdapter({ client: new Redis() });
const locker = new Locker({
  adapter: redisAdapter,
  retrySettings: { retryDelay: 200, retryTimes: 10 },
});

const lock = await locker.lock("user:123", 3000).aquire();
try {
  //do some risky stuff here
  //...
  //
  await lock.extend(2000);
  //do more risky stuff
  //...
} catch (error) {
} finally {
  await lock.release();
}

In the second one, you pass a function in the acquire method and a lock will be released automatically when a function finishes. Here is an example with a mongo:

import { Locker, IoRedisAdapter, MongoAdapter } from "@kontsedal/Locker";
import { MongoClient } from "mongodb";

const mongoAdapter = new MongoAdapter({
  client: new MongoClient(process.env.MONGO_URL),
});
const locker = new Locker({
  adapter: mongoAdapter,
  retrySettings: { retryDelay: 200, retryTimes: 10 },
});

await locker.lock("user:123", 3000).setRetrySettings({retryDelay: 200, retryTimes: 50}).aquire(async (lock) => {
  //do some risky stuff here
  //...
  await lock.extend(2000);
  //do some risky stuff here
  //...
});

API

Locker

The main class is responsible for the creation of new locks and passing them a storage adapter and default retrySettings.

Constructor params:

| parameter | type | isRequired | description | | --------------------------------- | -------------------- | ---------- |---------------------------------------------------------------------------------------------------------| | params.adapter | ILockAdapter | true | Adapter to work with a lock keys storage. Currently Redis, Mongo and in-memory adapters are implemented | | params.retrySettings | object | true | | | params.retrySettings.retryTimes | number(milliseconds) | false | How many times we should retry lock before fail | | params.retrySettings.retryDelay | number(milliseconds) | false | How much time should pass between retries | | params.retrySettings.totalTime | number(milliseconds) | false | How much time should all retries last in total | | params.retrySettings.retryDelayFn | function | false | Function which returns a retryDelay for each attempt. Allows to implement an own delay logic |

Example of a retryDelayFn usage:

const locker = new Locker({
  adapter: new InMemoryAdapter(),
  retrySettings: {
    retryDelayFn: ({
      attemptNumber, // starts from 0
      startedAt, // date of start in milliseconds
      previousDelay,
      settings, // retrySettings
      stop, // function to stop a retries, throws an error
    }) => {
      if (attemptNumber === 4) {
        stop();
      }
      return (attemptNumber + 1) * 50;
    },
  },
});

Provided example will do the same as providing retryTimes = 5, retryDelay = 50

Methods

lock(key: string, ttl: number) => Lock

Creates a Lock instance with provided key and time to live in milliseconds. It won't lock a resource at this point. Need to call an aquire() to do so

Lock.aquire(cb?: (lock: Lock) => void) => Promise<Lock>

Locks a resource if possible. If not, it retries as much as specified in the retrySettings. If callback is provided, lock will be released after a callback execution.

Lock.release({ throwOnFail?: boolean }) => Promise<void>

Unlocks a resource. If a resource is invalid (already taken by other lock or expired) it won't throw an error. To make it throw an error, need to provide {throwOnFail:true}.

Lock.extend(ttl: number) => Promise<void>

Extends a lock for a provided milliseconds from now. Will throw an error if current lock is already invalid

Lock.isLocked() => Promise<boolean>

Checks if a lock is still valid

Lock.setRetrySettings(settings: RetrySettings) => Promise<Lock>

Overrides a default retry settings of the lock.


Redis adapter

Requires only a compatible with ioredis client:

import { IoRedisAdapter } from "@kontsedal/locco";
import Redis from "ioredis";

const redisAdapter = new IoRedisAdapter({ client: new Redis() });

How it works

It relies on a Redis SET command with options NX and PX.

NX - ensures that a record will be removed after provided time

PX - ensures that if a record already exists it won't be replaced with a new one

So, to create a lock we just execute a SET command and if it returns "OK" response means that lock is created, if it returns null - a resource is locked.

To release or extend a lock, firstly, it gets a current key value(which is a unique string for each lock) and compares it with a current one. If it matches we either remove the key or set a new TTL for it.


Mongo adapter

Requires a mongo client and optional database name and lock collection name:

import { MongoAdapter } from "@kontsedal/locco";
import { MongoClient } from "mongodb";

const mongoAdapter = new MongoAdapter({
  client: new MongoClient(process.env.MONGO_URL),
  dbName: "my-db", // optional parameter
  locksCollectionName: "locks", //optional parameter, defaults to "locco-locks"
});

How it works

We create a collection of locks in the database with the next fields:

  • key: string
  • uniqueValue: string
  • expireAt: Date

For this collection we create a special index { key: 1 }, { unique: true }, so mongo will throw an error if we try to create a new record with an existing key.

To create a lock, we use an updateOne method with an upsert = true option:

collection.updateOne(
  {
    key,
    expireAt: { $lt: new Date() },
  },
  { $set: { key, uniqueValue, expireAt: new Date(Date.now() + ttl) } },
  { upsert: true }
);

So, let's imagine that we want to create a lock and there is a valid lock in the DB. If the lock is valid, it won't pass expireAt: { $lt: new Date() } check, because its expireAt will be later than a current date. In this case updateOne will try to create a new record in the collection, because of { upsert: true } option. But it will throw an error because we have a unique index. So this operation can only be successful when there is no valid lock in the DB. If there is an invalid lock in the DB, it will be replaced by a new one.

Release and extend relies on the same logic, but we also compare with a key unique string.