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
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 theasync-mutex
package. Instead ofstatic
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\$func.code
) instead of the name, but it might slower the processing. \$\endgroup\$