How to lock on object which shared by multiple async method in nodejs?
Asked Answered
M

3

22

I have one object with different properties in nodejs, there are different async function which access and modify that object with some complex execution. A single async function may have internal callbacks (or async functions), that may take some time to execute and then that function will modify that object. I want to lock that object until I'm done with all modification, only after that any other async function will access it.

Example:

var machineList = {};

function operation1() {
    waitForMachineList();
    //complex logic
    //modification of machineList, some closure and callbacks functions and again modification of machineList
    leaveFromMachineList();
}
function operation2() {
    waitForMachineList();
    //complex logic
    //modification of machineList, some closure and callbacks functions and again modification of machineList
    leaveFromMachineList();
}
function operation3() {
    waitForMachineList();
    //complex logic
    //modification of machineList, some closure and callbacks functions and again modification of machineList
    leaveFromMachineList();
}
function operation4() {
    waitForMachineList();
    //complex logic
    //modification of machineList, some closure and callbacks functions and again modification of machineList
    leaveFromMachineList();
}

Suppose machineList is one complex object, and there are different operations are done on this object by different async methods (operation1(), operation2(), ...) to modify it. Those operation are called in any sequence and any number of time as per request comes from client. Each request will execute single operation.

There are some internal closure functions and callbacks (or async functions) in each operation function, it may take some time. But I want to lock machineList object till any single operation is done.

On start of any operation, I want to lock object like waitForMachineList() and will release lock after leaveFromMachineList().

So finally I want to implement locking mechanism in nodejs. Just like Critical Session in C++ and lock in C#.

So please some will help to implement it in nodejs? or suggest me any node module which I can use for this.

Mathildamathilde answered 6/8, 2016 at 9:57 Comment(0)
M
37

I have done Locking using async-lock node module. Now I can achieve the goal which is mention in question.

Example:

var AsyncLock = require('async-lock');
var lock = new AsyncLock();

function operation1() {
    console.log("Execute operation1");
    lock.acquire("key1", function(done) {
        console.log("lock1 enter")
        setTimeout(function() {
            console.log("lock1 Done")
            done();
        }, 3000)
    }, function(err, ret) {
        console.log("lock1 release")
    }, {});
}

function operation2() {
    console.log("Execute operation2");
    lock.acquire("key1", function(done) {
        console.log("lock2 enter")
        setTimeout(function() {
            console.log("lock2 Done")
            done();
        }, 1000)
    }, function(err, ret) {
        console.log("lock2 release")
    }, {});
}

function operation3() {
    console.log("Execute operation3");
    lock.acquire("key1", function(done) {
        console.log("lock3 enter")
        setTimeout(function() {
            console.log("lock3 Done")
            done();
        }, 1)
    }, function(err, ret) {
        console.log("lock3 release")
    }, {});
}operation1(); operation2(); operation3();

Output:

Execute operation1

lock1 enter

Execute operation2

Execute operation3

lock1 Done

lock1 release

lock2 enter

lock2 Done

lock2 release

lock3 enter

lock3 Done

lock3 release

Mathildamathilde answered 6/8, 2016 at 11:40 Comment(0)
E
4

I created a simplified version of async-lock that I wanted to share here, for those who want to understand how something like this can be technically implemented or don't want to add a new package.

const createLock = () => {
  const queue = [];
  let active = false;
  return (fn) => {
    let deferredResolve;
    let deferredReject;
    const deferred = new Promise((resolve, reject) => {
      deferredResolve = resolve;
      deferredReject = reject;
    });
    const exec = async () => {
      await fn().then(deferredResolve, deferredReject);
      if (queue.length > 0) {
        queue.shift()();
      } else {
        active = false;
      }
    };
    if (active) {
      queue.push(exec);
    } else {
      active = true;
      exec();
    }
    return deferred;
  };
};

You can than use this function like this:

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const lock = createLock();

function test(id, ms) {
  lock(async () => {
    console.log(id, "start");
    await sleep(ms);
    console.log(id, "end");
  });
}

test(1, 400);
test(2, 300);
test(3, 200);
test(4, 100);

// Output:
// 1 start
// 1 end
// 2 start
// 2 end
// 3 start
// 3 end
// 4 start
// 4 end

But note that the function passed has to be a Promise.

Evincive answered 22/11, 2022 at 19:45 Comment(0)
I
0

I wrote this code to solve similar problem.

I think it is kind of classic DB transaction model. It implements only lock mechanism, not rollbacks. It works in following way:

  1. you adding bunch of sync methods in order in which they should be called
  2. these methods will be called in N milliseconds
  3. there will be only one get request to get DB data (or any other data that you need to modify) and one request to save DB data
  4. every method will receive reference to "up to date data", i.e. handler № 2 will receive data object that was changed by handler № 1. Order depends on order of № 1

It works in browsers. It should work in Node.js, but I didn't test it.

Note that because every method will receive reference to object, you should modify actual reference, and if you want to read it and return some value, then copy it, don't return that reference, because values of that reference may be changed in future by future handlers

TRANSACTION will wait for N ms (defaults to 250) before execution. That defines which methods will be grouped into single transaction. You can also make instant calls.

Here is the code:

let TIMER_ID = 0;
let LOCK = Promise.resolve();
let RESOLVE_LOCK = undefined;
let FUNC_BUFFER = [];
let SUCCESS_BUFFER = [];
let FAIL_BUFFER = [];


/**
 * Gets DB data. 
 */
async function get() {
    return {
        key1: "value1",
        key2: {
            key3: "value2"
        }
    };
}

/**
 * Sets new data in DB.
 */
async function set(value) {
    return;
}


/**
 * Classic database transaction implementation.
 *
 * You adding bunch of methods, every method will be
 * executed with valid instance of data at call time,
 * after all functions end the transaction will end.
 * If any method failed, then entire transaction will fail
 * and no changes will be written. If current transaction is
 * active, new one will be not started until end of previous one.
 *
 * In short, this transaction have ACID properties.
 *
 * Operations will be auto grouped into separate transactions
 * based on start timeout, which is recreated every time on
 * operation call.
 *
 * @example
 * ```
 * // Without transaction:
 * create("1", 123)
 * update("1", 321)
 * read("1") => 123 // because `update` is async
 *
 * // With transaction:
 * create("1", 123)
 * update("1", 321)
 * read("1") => 321
 *
 * // Without transaction:
 * create("v", true)
 * update("v", false) // throws internal error,
 *                    // "v" will be created
 * read("v") => true // because `update` is async
 * update("v", false) // will update because `update` is async
 *
 * // With transaction:
 * create("v", true)
 * update("v", false) // throws internal error,
 *                    // entire transaction will throw error,
 *                   // "v" will be not created
 * read("v") => true // entire transaction will throw error
 * update("v", false) // entire transaction will throw error
 * ```
 *
 * @example
 * ```
 * // Transaction start
 * create()
 * update()
 * update()
 * remove()
 * // Transaction end
 *
 * // Transaction start
 * create()
 * update()
 * sleep(1000)
 * // Transaction end
 *
 * // Transaction start
 * update()
 * remove()
 * // Transaction end
 * ```
 */
const TRANSACTION = {
    /**
     * Adds function in transaction.
     *
     * NOTE:
     * you shouldn't await this function, because
     * it only represents transcation lock, not
     * entire transaction end.
     *
     * @param f
     * Every function should only modify passed state, don't
     * reassign passed state and not save changes manually!
     * @param onSuccess
     * Will be called on entire transaction success.
     * @param onFail
     * Will be called on entire transaction fail.
     * @param startTimeout
     * Passed `f` will be added in current transaction,
     * and current transaction will be called after
     * `startTimeout` ms if there will be no more `f` passed.
     * Default value is recommended.
     */
    add: async function(
        f,
        onSuccess,
        onFail,
        startTimeout
    ) {
        await LOCK;

        window.clearTimeout(TIMER_ID);
        FUNC_BUFFER.push(f);

        if (onSuccess) {
            SUCCESS_BUFFER.push(onSuccess);
        }

        if (onFail) {
            FAIL_BUFFER.push(onFail);
        }

        if (startTimeout == null) {
            startTimeout = 250;
        }

        TIMER_ID = window.setTimeout(() => {
            TRANSACTION.start();
        }, startTimeout);

        console.debug("Added in transaction");
    },
    start: async function() {
        LOCK = new Promise((resolve) => {
            RESOLVE_LOCK = resolve;
        });
        console.debug("Transaction locked");

        let success = true;

        try {
            await TRANSACTION.run();
        } catch (error) {
            success = false;
            console.error(error);
            console.warn("Transaction failed");
        }

        if (success) {
            for (const onSuccess of SUCCESS_BUFFER) {
                try {
                    onSuccess();
                } catch (error) {
                    console.error(error);
                }
            }
        } else {
            for (const onFail of FAIL_BUFFER) {
                try {
                    onFail();
                } catch (error) {
                    console.error(error);
                }
            }
        }

        FUNC_BUFFER = [];
        SUCCESS_BUFFER = [];
        FAIL_BUFFER = [];
        RESOLVE_LOCK();

        console.debug("Transaction unlocked");
    },
    run: async function() {
        const data = await get();
        const state = {
            value1: data.key1,
            value2: data.key2
        };

        for (const f of FUNC_BUFFER) {
            console.debug("Transaction function started");
            f(state);
            console.debug("Transaction function ended");
        }

        await set({
            key1: state.value1,
            key2: state.value2
        });
    }
}

Example № 1:

/**
 * Gets DB data. 
 */
async function get() {
    return {
        key1: "value1",
        key2: {
            key3: "value2"
        }
    };
}

/**
 * Sets new data in DB.
 */
async function set(value) {
    console.debug("Will be set:", value);

    return;
}


new Promise(
    (resolve) => {
        TRANSACTION.add(
            (data) => {
                data.value2.key3 = "test1";
            },
            () => console.debug("success № 1")
        );
        TRANSACTION.add(
            (data) => {
                const copy = {
                    ...data.value2
                };
                
                resolve(copy);
            },
            () => console.debug("success № 2")
        );
        TRANSACTION.add(
            (data) => {
                data.value1 = "test10";
                data.value2.key3 = "test2";
            },
            () => console.debug("success № 3")
        );
    }
)
.then((value) => {
    console.debug("result:", value);
});

/* Output:

Added in transaction
Added in transaction
Added in transaction
Transaction locked
Transaction function started
Transaction function ended
Transaction function started
Transaction function ended
Transaction function started
Transaction function ended
Will be set: {key1: 'test10', key2: {key3: 'test2'}}
result: {key3: 'test1'}
success № 1
success № 2
success № 3
Transaction unlocked 

*/

Example № 2:

TRANSACTION.add(
    () => {
        console.log(1);
    }
);
TRANSACTION.add(
    () => {
        console.log(2);
    },
    undefined,
    undefined,
    0 // instant call
);


/* Output:

16:15:34.715 Added in transaction
16:15:34.715 Added in transaction
16:15:34.717 Transaction locked
16:15:34.717 Transaction function started
16:15:34.718 1
16:15:34.718 Transaction function ended
16:15:34.718 Transaction function started
16:15:34.718 2
16:15:34.718 Transaction function ended
16:15:34.719 Transaction unlocked

*/

Example № 3:

TRANSACTION.add(
    () => {
        console.log(1);
    }
);
TRANSACTION.add(
    () => {
        console.log(2);
    },
    undefined,
    undefined,
    0 // instant call
);

await new Promise((resolve) => {
    window.setTimeout(() => {
        resolve();
    }, 1000);
});

TRANSACTION.add(
    () => {
        console.log(3);
    }
);


/* Output:

16:19:56.840 Added in transaction
16:19:56.840 Added in transaction
16:19:56.841 Transaction locked
16:19:56.841 Transaction function started
16:19:56.842 1
16:19:56.842 Transaction function ended
16:19:56.842 Transaction function started
16:19:56.842 2
16:19:56.842 Transaction function ended
16:19:56.842 Transaction unlocked
16:19:57.840 Added in transaction
16:19:58.090 Transaction locked
16:19:58.090 Transaction function started
16:19:58.090 3
16:19:58.091 Transaction function ended
16:19:58.091 Transaction unlocked

*/
Isahella answered 20/9, 2021 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.