GithubHelp home page GithubHelp logo

tristanls / dynamodb-lock-client Goto Github PK

View Code? Open in Web Editor NEW
50.0 3.0 22.0 77 KB

A general purpose distributed locking library built for AWS DynamoDB.

License: MIT License

JavaScript 100.00%
dynamodb distributed-lock nodejs fencing

dynamodb-lock-client's Introduction

dynamodb-lock-client

Stability: 1 - Experimental

NPM version

A general purpose distributed locking library with fencing tokens built for AWS DynamoDB.

For AWS SDK v3 version go to: https://github.com/trilogy-group/dynamodb-lock-client-v3

Contributors

@tristanls, @Jacob-Lynch, @simlu, Lukas Siemon, @tomyam1, @deathgrindfreak, @jepetko, @fpronto

Contents

Installation

npm install dynamodb-lock-client

Usage

To run the below example, run:

npm run readme
"use strict";

const AWS = require("aws-sdk");
const DynamoDBLockClient = require("../index.js");

const dynamodb = new AWS.DynamoDB.DocumentClient(
    {
        region: "us-east-1"
    }
);

// "fail closed": if process crashes and lock is not released, lock will
//                never be released (requires human intervention)
const failClosedClient = new DynamoDBLockClient.FailClosed(
    {
        dynamodb,
        lockTable: "my-lock-table-name",
        partitionKey: "mylocks",
        acquirePeriodMs: 1e4
    }
);

failClosedClient.acquireLock("my-fail-closed-lock", (error, lock) =>
    {
        if (error)
        {
            return console.error(error)
        }
        console.log("acquired fail closed lock");
        // do stuff
        lock.release(error => error ? console.error(error) : console.log("released fail closed lock"));
    }
);

// "fail open": if process crashes and lock is not released, lock will
//              eventually expire after leaseDurationMs from last heartbeat
//              sent
const failOpenClient = new DynamoDBLockClient.FailOpen(
    {
        dynamodb,
        lockTable: "my-lock-table-name",
        partitionKey: "mylocks",
        heartbeatPeriodMs: 3e3,
        leaseDurationMs: 1e4
    }
);

failOpenClient.acquireLock("my-fail-open-lock", (error, lock) =>
    {
        if (error)
        {
            return console.error(error)
        }
        console.log(`acquired fail open lock with fencing token ${lock.fencingToken}`);
        lock.on("error", error => console.error("failed to heartbeat!"));
        // do stuff
        lock.release(error => error ? console.error(error) : console.log("released fail open lock"));
    }
);

Tests

At this time, test are implemented for FailOpen lock acquisition and release.

    npm test

Documentation

Setting up the lock table in DynamoDB

Recommended

The DynamoDB lock table needs to be created independently. The following is an example CloudFormation template that would create such a lock table:

AWSTemplateFormatVersion: "2010-09-09"

Resources:

  DistributedLocksStore:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      TableName: "distributed-locks-store"
      BillingMode: PAY_PER_REQUEST

Outputs:

  DistributedLocksStore:
    Value: !GetAtt DistributedLocksStore.Arn

The template above would make your config.partitionKey == "id" and your config.lockTable == "distributed-locks-store".

You can choose to call your config.partitionKey any valid string except fencingToken, leaseDurationMs, lockAcquiredTimeUnixMs, owner, or guid (these attribute names are reserved for use by DynamoDBLockClient library). Your config.partitionKey has to correspond to the partition key (HASH) of the Primary Key of your DynamoDB table.

Using sort key

In some cases, you may be constrained to use a DynamoDB table that requires to specify a sort key. The following is an example CloudFormation template that would create such a lock table:

AWSTemplateFormatVersion: "2010-09-09"

Resources:

  DistributedLocksStore:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
        - AttributeName: sortID
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
        - AttributeName: sortID
          KeyType: RANGE
      TableName: "distributed-locks-store"
      BillingMode: PAY_PER_REQUEST

Outputs:

  DistributedLocksStore:
    Value: !GetAtt DistributedLocksStore.Arn

The template above would make your config.partitionKey == "id", config.sortKey = "sortID", and your config.lockTable == "distributed-locks-store".

You can choose to call your config.partitionKey and config.sortKey any valid string except fencingToken, leaseDurationMs, lockAcquiredTimeUnixMs, owner, or guid (these attribute names are reserved for use by DynamoDBLockClient library). Your config.partitionKey has to correspond to the partition key (HASH) of the Primary Key of your DynamoDB table. Your config.sortKey has to correspond to the sort key (RANGE) of the Primary Key of your DynamoDB table.

DynamoDBLockClient

Public API

new DynamoDBLockClient.FailClosed(config)

  • config: Object
    • dynamodb: AWS.DynamoDB.DocumentClient Instance of AWS DynamoDB DocumentClient.
    • lockTable: String Name of lock table to use.
    • partitionKey: String Name of table partition key (hash key) to use.
    • sortKey: String (Default: undefined) Optional name of table sort key (range key) to use. If specified, all lock ids will be required to contain a sortKey.
    • acquirePeriodMs: Number How long to wait for the lock before giving up. Whatever operation this lock is protecting should take less time than acquirePeriodMs.
    • owner: String Customize owner name for lock (optional).
    • retryCount: Number (Default: 1) Number of times to retry lock acquisition after initial failure. No retries will occur if set to 0.
  • Return: Object Fail closed client.

Creates a "fail closed" client that acquires "fail closed" locks. If process crashes and lock is not released, lock will never be released. This means that some sort of intervention will be required to put the system back into operational state if lock is held and a process crashes while holding the lock.

new DynamoDBLockClient.FailOpen(config)

  • config: Object
    • dynamodb: AWS.DynamoDB.DocumentClient Instance of AWS DynamoDB DocumentClient.
    • lockTable: String Name of lock table to use.
    • partitionKey: String Name of table partition key (hash key) to use.
    • sortKey: String (Default: undefined) Optional name of table sort key (range key) to use. If specified, all lock ids will be required to contain a sortKey.
    • heartbeatPeriodMs: Number (Default: undefined) Optional period at which to send heartbeats in order to keep the lock locked. Providing this option will cause heartbeats to be sent.
    • leaseDurationMs: Number The length of lock lease duration. If the lock is not renewed via a heartbeat within leaseDurationMs it will be automatically released.
    • owner: String Customize owner name for lock (optional).
    • retryCount: Number (Default: 1) Number of times to retry lock acquisition after initial failure. No retries will occur if set to 0.
    • trustLocalTime: Boolean (Default: false) If set to true, when the client retrieves an existing lock, it will use local time to determine if leaseDurationMs has elapsed (and shorten its wait time accordingly) instead of always waiting the full leaseDurationMs milliseconds before making an acquisition attempt.
  • Return: Object Fail open client.

Creates a "fail open" client that acquires "fail open" locks. If process crashes and lock is not released, lock will eventually expire after leaseDurationMs from last heartbeat sent (if any). This means that if process acquires a lock, goes to sleep for more than leaseDurationMs, and then wakes up assuming it still has a lock, then it can perform an operation ignoring other processes that may assume they have a lock on the operation.

client.acquireLock(id, callback)

  • id: String|Buffer|Number|Object Unique identifier for the lock. If the type of id is String|Buffer|Number the type must correspond to lock table's partition key type. If the type of id is Object, it is expected to have the following format:
    {
      [config.partitionKey]: String|Buffer|Number,
      [config.sortKey]: String|Buffer|Number
    }
    
    For example, if config.partitionKey = "myPartitionKey" and config.sortKey = "mySortKey" and partition key value is id1234 and sort key value is abcd, then the Object would be:
    {
      myPartitionKey: "id1234",
      mySortKey: "abcd"
    }
    
    Sort key part of id is only required if lock is configured with a sort key. The types of partition key and sort key must correspond to lock table's partition key and sort key types.
  • callback: Function (error, lock) => {}
    • error: Error Error, if any.
    • lock: DynamoDBLockClient.Lock Successfully acquired lock object. Lock object is an instance of EventEmitter. If the lock is acquired via a fail open client configured to heartbeat, then the returned lock may emit an error event if a heartbeat operation fails.
      • fencingToken: Integer fail open locks only Integer monotonically incremented with every "fail open" lock acquisition to be used for fencing. Heartbeats do not increment fencingToken.

Attempts to acquire a lock. If lock acquisition fails, callback will be called with an error and lock will be falsy. If lock acquisition succeeds, callback will be called with lock, and error will be falsy.

Fail closed client will attempt to acquire a lock. On failure, client will retry after acquirePeriodMs up to retryCount times. After retryCount failures, client will fail lock acquisition. On successful acquisition, lock will be locked until lock.release() is called successfuly.

Fail open client will attempt to acquire a lock. On failure, if trustLocalTime is false (the default), client will retry after leaseDurationMs. If trustLocalTime is true, the client will retry after Math.max(0, leaseDurationMs - (localTimeMs - lockAcquiredTimeMs)) where localTimeMs is "now" and lockAcquiredTimeMs is the lock acquisition time recorded in the retrieved lock. Lock acquisition will be retried up to retryCount times. After retryCount failures, client will fail lock acquisition. On successful acquisition, if heartbeatPeriodMs option is not specified (heartbeats off), lock will expire after leaseDurartionMs. If heartbeatPeriodMs option is specified, lock will be renewed at heartbeatPeriodMs intervals until lock.release() is called successfuly. Additionally, if heartbeatPeriodMs option is specified, lock may emit an error event if it fails a heartbeat operation.

lock.release(callback)

  • callback: Function error => {}
    • error: Error Error, if any. No error implies successful lock release.

Releases previously acquired lock.

Fail closed lock is deleted, so that it can be acquired again.

Fail open lock heartbeats stop, and its leaseDurationMs is set to 1 millisecond so that it expires "immediately". The datastructure is left in the datastore in order to provide continuity of fencingToken monotonicity guarantee.

Releases

We follow semantic versioning policy (see: semver.org):

Given a version number MAJOR.MINOR.PATCH, increment the:

MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards-compatible manner, and
PATCH version when you make backwards-compatible bug fixes.

caveat: Major version zero is a special case indicating development version that may make incompatible API changes without incrementing MAJOR version.

dynamodb-lock-client's People

Contributors

deathgrindfreak avatar fpronto avatar tomyam1 avatar tristanls avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

dynamodb-lock-client's Issues

Documentation Incorrect?

We're using FailOpen and the documentation says:

if process crashes and lock is not released, lock will eventually expire after leaseDurationMs from last heartbeat sent

This is not really true. Rather the it should say

For an unreleased lock, the next request to lock will wait leaseDurationMs to acquire the lock and only acquire it if in that period a heartbeat hasn't altered it.

Very different behavior.

It is very unfortunate since now we can't use this in the way that we wanted to. We might have to implement our own locking library after all :(

Edit: It also seems that the entry is "stuck" in the table for-ever in this case (with a lease duration of 1). Which seems to be a bug I guess? It should be cleaned up?

Browser Support

Before starting with my problem i just want to thank you for this code

There is a way to use this as a browser package?

Right now i solved the "problem" with dynamodb.DocumentClient dependecy by injecting a middleware created by me.

But still I can't use this in a browser because of Joi depedency.

Someone had this problem before, or know a way to work arround this?

Not working for DynamoDB tables with sortKey

Not working for DynamoDB tables with sortKey. There is no option to provide sort_key which is resulting into mismatch of schema while acquiring lock.

ERROR	ValidationException: The provided key element does not match the schema
    at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:51:27)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14)
    at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18

Add optional rangeKey support

add optional rangeKey support so that the locking can be used like this:

            failOpenClient.acquireLock([LOCK_ID, SUB_LOCK_ID], (error, lock) => {
                if (error) {
                    reject(error);
                } else {
                    console.log(`acquired open lock with fencing token ${lock.fencingToken}`);
                    lock.on('error', () => console.error('failed to heartbeat!'));
                    resolve(lock);
                }
            });

where

  • LOCK_ID is the value of the configured partitionKey
  • SUB_LOCK_ID is the value of the configured rangeKey

Heartbeat continues after fail open lock is released

Hey, I appreciate you creating this library! It helped me quickly get started with dynamodb locks.

Anyway, there is a bug that occasionally occurs where the following steps happen in this order:

  • A heartbeat refreshLock function call happens and a dynamodb put call is made to refresh the lock.
  • While waiting for the response from dynamodb a lock.release() function call is made.
  • clearTimeout is called on the setTimeout's return value for the last heartbeat.

At this point the lock is released and the heartbeat is stopped. But then...

  • The dynamodb put callback fires and calls setTimeout to refresh the lock again in the future, causing the heartbeat to start back up.

Now the heartbeat continues indefinitely and keeps the process alive. I run this inside of an AWS lambda and it keeps the lambda alive the full 300 seconds.

I tried a simple fix of adding:

self._released = true;

to Lock.prototype.release and then inside the refreshLock function right before calling setTimeout:

if(!self._released) {
  self._heartbeatTimeout = setTimeout(refreshLock, self._heartbeatPeriodMs);
}

This has prevented the heartbeat from turning back on after the lock is released.

Thanks again!

FailOpen lock probably won't work if heartbeatPeriodMs is unspecified.

Looking at this if statement :

...
    if (self._heartbeatTimeout)
    {
        clearTimeout(self._heartbeatTimeout);
        return self._releaseFailOpen(callback);
    }
    else
    {
        return self._releaseFailClosed(callback);
    }
...

self._heartbeatTimeout is used as an indirect way to determine whether the lock is fail open or closed.

Since heartbeatPeriodMs is optional, it can be left undefined. If that's the case, then self._heartbeatTimeout will not be set because this if statement is bypassed when heartbeatPeriodMs is falsy and the code will erronously try to release a fail open lock using fail closed mechanism.

Problem: No recordings to visualize requests

Distributed locks are non-trivial. Recordings help increase confidence that the correct thing is happening.

@simlu:

I'd much prefer mocha as we could use node-tdd and we could easily use recordings and hence visualize the requests that are actually being made.

Does not seem to work on lambda

If I try to use this library on lambda I get this error, although if I load it locally in node 6.10.3 it seems to be fine.

Unable to import module 'main_index': Error
at Function.Module._resolveFilename (module.js:469:15)
at Function.Module._load (module.js:417:25)
at Module.require (module.js:497:17)
at require (internal/module.js:20:19)
at Object. (/var/task/node_modules/joi/lib/index.js:6:13)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)

Thank you!

Just wanted to give a big thank you for creating this library!

We've finally moved on to our own locking logic, which is purpose build for our (much simpler) needs, but this library has gotten us here and we're very grateful for it!

It's an awesome library, especially since it handles various locking needs and has a good amount of features.

Thank you again, and so long, and thanks for all the fish code. I'm sure I'll see you around @tristanls =)

Feature Request: Support for Promises

Hello,
This is a very nice lock client and I was looking to make use of it in a project that I was working on. However, because the APIs provided here are callback-based, integration has not been as clean as I would have loved it to be.

The Javascript Promise API is pretty mature enough especially for asynchronous programming and it enables developers to write cleaner, more readable code.

Would it be possible to provide a Promise based API for client.acquireLock and lock.release

So that rather than doing something like as demonstrated in the README:

client.acquireLock("lock-key", (error, lock) =>
    {
        if (error)
        {
            return console.error(error)
        }
        console.log(`acquired fail open lock with fencing token ${lock.fencingToken}`);
        lock.on("error", error => console.error("failed to heartbeat!"));
        // do stuff.
        lock.release(error => error ? console.error(error) : console.log("released fail open lock"));
    }
);

developers can do something like:

try {
    const lock = await client.acquireLock("lock-key");
    console.log(`acquired fail open lock with fencing token ${lock.fencingToken}`);
    lock.on("error", error => console.error("failed to heartbeat!"));
    // do stuff.
    await lock.release();
);
} catch (error) {
    return console.error(error)
}

For my personal project, I have written a wrapper around the client to implement this behavior but I thought it'd be super useful to others as well.

Thank you.

Release conflict

Hi again, having troubles again
I'm using the FailOpen feature
if i try to do a release at same time of an heartbeat, it crashes
i think this happens because the lock try to release with a diferent guid value

  • heartbeat reach the database
  • release reach the database before heartbeat return and change guid value in the lock

i was able to reproduce this error because my middleware have a some delay
we are doing a workarround by checking the time
if it's close to an heartbeat wait a bit to release

Readme: Lock Table

No documentation on what needs to happen with the lock table. Does it need to be set up manually?

Tests Question

@tristanls First of all really great library and appreciate the time/effort you've put into it. Source is fairly easy to follow and seems to accomplish exactly what's needed for locking (for my scenario at least). Have two quick questions for when you get a moment:

(1) Is the reason for the lack of tests because of the experimental state that the module is currently in, or a lack of time to implement?
(2) If it's a lack of time, do you have a preferred JS testing setup?

Asking as I'm thinking about using this module for a distributed system I'm working on currently and would be willing to write tests. Having some CI&D/testing to ensure no breakage between releases is fairly important to me.

Not reproducible test recordings

We are mocking all our tests outgoing requests (using lambda-tdd). However since the owner is different per environment we can't really use mocking (i.e. ci will have a different owner than local especially since we use docker for everything)

Looking in particular at the line

owner: `${pkg.name}@${pkg.version}_${os.userInfo().username}@${os.hostname()}

https://github.com/tristanls/dynamodb-lock-client/blob/master/index.js#L50

Any reason why we need the os... information here? My preference would be to make this customization at the very least or even just change this to ${pkg.name}@${pkg.version}. Thoughts? I will create a pr if you are ok with changing this.

Edit: Create a pr here https://github.com/tristanls/dynamodb-lock-client/pull/7/files

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.