Edited at

タイムアウトした時にちゃんとロックを解除してくれる async-lockを作る

Promiseの排他制御をしてくれるライブラリにasync-lockあります。

で、このライブラリには、ロックしているPromiseの実行が一定時間以上立った場合に Error を投げてくれるTimeoutというオプションがあるのですが、こいつはTimeOutになってもエラーを投げるだけでロックを解除してはくれません。

なので、一定時間経過後に問答無用でロックを解除したい場合は、自分でロックを解除するコードを書かないといけないです。

まあ、処理が長過ぎるときのエラーハンドリングは、内部で実行されているPromise関数が頑張れと言うことなのかもしれませんが、、それってTimeoutの意味なくない?ってなったので自作しました。(といっても本家のライブラリをラップしただけですが・・・)

TypeScriptで書いちゃってますが、型定義部分削ればJavaScriptでも使えると思います。


コード

実行にはasync-lockが必要なので事前にインストールしてください

$ npm install async-lock

$ npm install --save-dev @types/async-lock

作成したラッパークラスは以下です

import AsyncLock from 'async-lock'

export class PromiseLock {

asyncLock: AsyncLock
constructor(option?: AsyncLockOptions) {
this.asyncLock = new AsyncLock(option)
}

acquire(func: Function, options: AquireOptions = {}) {

let { resolveTimeout, key } = options
if (!key) {
key = '__DEFAULT_KEY__'
}

return this.asyncLock.acquire(key, () => {
return new Promise(async resolve => {
let isResolved = false
if (resolveTimeout) {
setTimeout(() => {
if (!isResolved) {
console.error(`${resolveTimeout} ms passed. over resolveTimeout. unlock promiseLock`)
resolve()
}
}, resolveTimeout)
}
await func()
resolve()
isResolved = true
})
})
}

isBusy(key = '__DEFAULT_KEY__') {
return this.asyncLock.isBusy(key)
}
}

interface AsyncLockOptions {
timeout?: number;
maxPending?: number;
domainReentrant?: boolean;
Promise?: any;
}

interface AquireOptions {
resolveTimeout?: number
key?: string | string[]
}


使い方

使い方は以下のような感じです。だいたい本家と一緒ですが、不要ならkeyとか指定しなくても良くなってます。

    const promiseLock = new PromiseLock()

const asyncHandler = () => {
// promiseLock.acquireの第一引数に指定した関数の実行が終わるまでロックする
promiseLock.acquire(async () => {
console.log('start')
await Promise処理()
console.log('finish')
})
}

第2引数にresolveTimeoutを指定することで、指定した時間経過後に強制的にロックを解除することができます。

本家と違ってErrorもスローしません。(console.error()は吐きますが・・・)

        promiseLock.acquire(async () => {

console.log('start')
await Promise処理()
console.log('finish')
}, { resolveTimeout: 2000 }) // 2秒以上処理が終わらない場合は強制的にロックを解除する

一応キーも指定することができます。

        promiseLock.acquire(async () => {

console.log('start')
await Promise処理()
console.log('finish')
},{ key: 'original_key' })

isBusy()も使えます。これは、acquireの中の関数が実行中であればtrueを返す関数です。特にresolveTimeoutと組み合わせると、「処理が終わるまでは次の呼び出しを実行しないようにし、処理に時間がかかりすぎる場合はロックを解除する」みたいな動きができるので、ゲームのスクロール処理とか、「時間経過で処理を行いたいけど、遅延して実行するぐらいなら処理落ちしたほうが良い」みたいな時に結構使えます。

    const handler = () => {

// acquireの処理中は次の処理を受け付けない
if (promiseLock.isBusy()) {
console.log('handler is busy. skip exec function')
return
}

promiseLock.acquire(async () => {
console.log('start')
await Promise処理()
console.log('finish')
}, { resolveTimeout: 2000 }) // 2秒以上処理が遅延する場合には、強制的にロックを解除し、次の処理を実行する
}

本家のoptionを使いたい場合は、newする時に指定することもできます。(ただここで指定できるtimeoutは本家のエラーを吐くだけのtimeoutなので気をつけてください)

    const promiseLock = new PromiseLock({ maxPenging: 100 })

以上