0
\$\begingroup\$

I needed a way to ensure that a function called was done only one at a time. Because of how Javascript is done and with Asynchronous functions, this wasn't the case, and there could be concurrency calls that was causing troubles on my code.

So I ended up with the following code. In a nutshell, you call Lock.wait by passing the name, the callback, the current scope and any parameters.

The Lock system will either run that method directly if no other is running, then call any pending one that might have arrived in between.

Every pending methods handle Promise and will return the appropriate result back to the original call (const result = Lock.wait(...)).

My tests seems to show that it works, but I'm curious to have your feedback on it, and how it can be improved.

At least one area where it can be improved is on the name of the method. I tried to find a way to not pass the name, but use some kind of function identification instead, but since it's an anonymous function, I can't use .name on that function, and I couldn't find any "hash id" for a give function in javascript. I'm eager to have a better way here.

Here's the locking mechanism:


class Lock {
    static pendings = {}
    static running = {}

    static isRunning (name) {
        return (name in this.running && this.running[name] === true)
    }

    static async run (name) {
        this.running[name] = true

        while (this.pendings[name].length) {
            const current = this.pendings[name].shift()
            try {
                const result = await current.callback.apply(current.scope, current.params)
                current.resolve(result)
            } catch (e) {
                current.reject(e)
            }
        }
        this.running[name] = false
    }

    static async wait (name, callback, scope, ...params) {
        const fut = new Promise((resolve, reject) => {
            if (!(name in this.pendings)) this.pendings[name] = []
            this.pendings[name].push({
                callback, scope, params, resolve, reject
            })
        })

        if (!this.isRunning(name)) {
            // Not running, so we do
            this.run(name)
        }

        return await fut
    }
}

And you call it like this:

(async function test () {
    console.time('Test called')
    const pendings = []
    for (let i = 1; i < 6; i++) {
        console.log('Adding', i)
        pendings.push(Lock.wait(
            'long_process',
            async (i) => new Promise((resolve, reject) => {
                console.log('Waiting for', i)
                console.time('For' + i)
                setTimeout(() => {
                    console.log(i, 'finished, returning i * 2')
                    resolve(i * 2)
                    console.timeEnd('For' + i)
                }, i * 1000)
            }),
            this,
            i
        ))
    }

    console.log('Waiting on ending')
    const result = await Promise.all(pendings)
    console.log('result is', result)

    console.timeEnd('Test called')
})()

The expected duration is about 15 seconds (incremental wait from 1 to 5). The result is:

Adding 1
Waiting for 1
Adding 2
Adding 3
Adding 4
Adding 5
Waiting on ending
Promise {<pending>} // Returned result from calling test()
1 'finished, returning i * 2'
For1: 1000.5537109375 ms
Waiting for 2
2 'finished, returning i * 2'
For2: 2000.3701171875 ms
Waiting for 3
3 'finished, returning i * 2'
For3: 3000.2177734375 ms
Waiting for 4
4 'finished, returning i * 2'
For4: 4000.22607421875 ms
Waiting for 5
5 'finished, returning i * 2'
For5: 5000.503173828125 ms
'result is' (5) [2, 4, 6, 8, 10]
Test called: 15006.42626953125 ms
\$\endgroup\$
5
  • \$\begingroup\$ Based on the second sentence, I've edited with the assumption that the language is JavaScript. \$\endgroup\$ Jan 11, 2022 at 13:06
  • \$\begingroup\$ That is correct, it is Javascript, sorry I forgot to mention that! \$\endgroup\$
    – Cyril N.
    Jan 11, 2022 at 13:36
  • \$\begingroup\$ I have used a locking npm package with redis named redlock, which also need to input the shared name to identify the resource to be locked, so I think it is fine. However, there is also another way like in the async-mutex package. Instead of static methods, it need to create an instance and each instance handle a single list, so that you can create one instance for each resource, without inputing the name as string, which may lead to typo mistake. You may have a look to those packages. \$\endgroup\$ Jan 12, 2022 at 7:20
  • \$\begingroup\$ I thought about creating an instance per lock, but the issue is that you now need to keep track of that instance to ensure you don't create it twice. That's why I went with the "naming" system. I thought about creating a hash from the function code (func.code) instead of the name, but it might slower the processing. \$\endgroup\$
    – Cyril N.
    Jan 12, 2022 at 8:50
  • \$\begingroup\$ I don't have time to do a proper review so just throwing this out there as something to think about... I created some functionality for the same purpose, but I implemented it with events. On the first lock attempt, status becomes locked. Any other invocation ends up awaiting a promise that only resolves when the event is raised. When the event is raised the status is also set to unlocked. One of the previously blocked invocations gets the lock and the rest wait again, and so on. When an invocation gets the lock, its callback is executed. \$\endgroup\$
    – 404
    Jan 12, 2022 at 19:21

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.