async-lockを使った排他制御

async-lockを使ってサーバサイドで排他制御させる方法です。排他制御を使えば同時アクセスよるデータの不整合を防げます。本記事で扱うモジュールのバージョンは次のとおりです。

  • Node.js - 14.16.0
  • express - 4.17.1
  • async-lock - 1.3.0

インストール

次のコマンドを実行します。

npm install async-lock

以降は使い方になります。

execute function を使う場合

コールバック関数を抜けてからロックが解放されます。execute functionでのエラーをコールバック関数で判定する必要があります。

/**
* async-lock sample.
* use execute function.
*/
"use strict";
//const fs = require('fs');
const path = require('path');
//const https = require('https');
const express = require('express');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
// let webServer = https.createServer({
// key: fs.readFileSync('server-key.pem'),
// cert: fs.readFileSync('server-crt.pem'),
// maxVersion: 'TLSv1.3',
// minVersion: 'TLSv1',
// }, app)
// .listen(8443, function() {
// console.log('start. (exit: Ctrl + C)');
// }
// );
let webServer = app.listen(8080, () => {
console.log('start. (exit: Ctrl + C)');
});
const AsyncLock = require('async-lock');
const lock = new AsyncLock({ timeout: 60000 });
const lockKey = 'key';
let count = 0;
// sleep function.
const sleep = (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
};
// execute function.
async function execute(done) {
console.log(Date.now() + ' execute');
await sleep(5000);
console.log(Date.now() + ' sleep end');
count++;
done(undefined, 'execute end. count=' + count);
}
app.get('/start-lock', (request, response) => {
lock.acquire(lockKey, execute, (err, result) => {
console.log(Date.now() + ' start-lock execute end.');
if (err) {
console.error(err.message);
response.send('error.');
// lock released
return;
}
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
// lock released
});
});
app.get('/start-lock2', (request, response) => {
lock.acquire(lockKey, execute, (err, result) => {
console.log(Date.now() + ' start-lock2 execute end.');
if (err) {
console.error(err.message);
response.send('error.');
// lock released
return;
}
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
// lock released
});
});

実行結果

node async-lock-sample-execute.jsを実行し、ブラウザから/start-lock > /start-lock2の順に実行した場合の実行結果(ログ)です。

1620694694502 execute
1620694699510 sleep end
1620694699511 start-lock execute end.
1620694699511 response send start.
1620694699513 response send end.
1620694699514 execute
1620694704513 sleep end
1620694704514 start-lock2 execute end.
1620694704514 response send start.
1620694704515 response send end.

エラーを発生させる

execute functionでエラーを発生させたい場合は次のようにdone()を実行します。

done(new Error('test exception.'));

エラー発生時の実行結果

execute functionでエラーを発生させた場合の実行結果です。

1620694953006 execute
1620694958010 sleep end
1620694958012 start-lock execute end.
test exception.
1620694958023 execute
1620694963027 sleep end
1620694963029 start-lock2 execute end.
test exception.

Promise を使う場合

then()またはcatch()がコールされるタイミングでロックは解除されます。

/**
* async-lock sample.
* use promise.
*/
"use strict";
//const fs = require('fs');
const path = require('path');
//const https = require('https');
const express = require('express');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
// let webServer = https.createServer({
// key: fs.readFileSync('server-key.pem'),
// cert: fs.readFileSync('server-crt.pem'),
// maxVersion: 'TLSv1.3',
// minVersion: 'TLSv1',
// }, app)
// .listen(8443, function() {
// console.log('start. (exit: Ctrl + C)');
// }
// );
let webServer = app.listen(8080, () => {
console.log('start. (exit: Ctrl + C)');
});
const AsyncLock = require('async-lock');
const lock = new AsyncLock({ timeout: 60000 });
const lockKey = 'key';
let count = 0;
// sleep function.
const sleep = (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
};
// execute function.
async function execute(done) {
console.log(Date.now() + ' execute');
await sleep(5000);
console.log(Date.now() + ' sleep end');
count++;
done(undefined, 'execute end. count=' + count);
}
app.get('/start-lock', (request, response) => {
lock.acquire(lockKey, execute)
.then(result => {
// lock released.
console.log(Date.now() + ' start-lock execute end.');
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
}).catch(err => {
// lock released.
console.error(err.message);
response.send('error.');
});
});
app.get('/start-lock2', (request, response) => {
lock.acquire(lockKey, execute)
.then(result => {
// lock released.
console.log(Date.now() + ' start-lock2 execute end.');
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
}).catch(err => {
// lock released.
console.error(err.message);
response.send('error.');
});
});

実行結果

node async-lock-sample-promise.jsを実行し、ブラウザから/start-lock > /start-lock2の順に実行した場合の実行結果(ログ)です。

1620696446739 execute
1620696451755 sleep end
1620696451757 execute
1620696451758 start-lock execute end.
1620696451758 response send start.
1620696451765 response send end.
1620696456764 sleep end
1620696456765 start-lock2 execute end.
1620696456765 response send start.
1620696456766 response send end.

コールバック関数のみの場合

ロック解除のタイミングはPromiseを使う場合と変わりません。

/**
* async-lock sample.
* only callback.
*/
"use strict";
//const fs = require('fs');
const path = require('path');
//const https = require('https');
const express = require('express');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
// let webServer = https.createServer({
// key: fs.readFileSync('server-key.pem'),
// cert: fs.readFileSync('server-crt.pem'),
// maxVersion: 'TLSv1.3',
// minVersion: 'TLSv1',
// }, app)
// .listen(8443, function() {
// console.log('start. (exit: Ctrl + C)');
// }
// );
let webServer = app.listen(8080, () => {
console.log('start. (exit: Ctrl + C)');
});
const AsyncLock = require('async-lock');
const lock = new AsyncLock({ timeout: 60000 });
const lockKey = 'key';
let count = 0;
const sleep = (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
};
app.get('/start-lock', (request, response) => {
lock.acquire(lockKey, async (done) => {
console.log(Date.now() + ' execute');
await sleep(5000);
console.log(Date.now() + ' sleep end');
count++;
done(undefined, 'execute end. count=' + count);
}).then(result => {
// lock released.
console.log(Date.now() + ' start-lock execute end.');
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
}).catch(err => {
// lock released.
console.error(err.message);
response.send('error.');
});
});
app.get('/start-lock2', (request, response) => {
lock.acquire(lockKey, async (done) => {
console.log(Date.now() + ' execute');
await sleep(5000);
console.log(Date.now() + ' sleep end');
count++;
done(undefined, 'execute end. count=' + count);
}).then(result => {
// lock released.
console.log(Date.now() + ' start-lock2 execute end.');
console.log(Date.now() + ' response send start.');
response.send(result);
console.log(Date.now() + ' response send end.');
}).catch(err => {
// lock released.
console.error(err.message);
response.send('error.');
});
});

実行結果

node async-lock-sample-only-callback.jsを実行し、ブラウザから/start-lock > /start-lock2の順に実行した場合の実行結果(ログ)です。

1620698092689 execute
1620698097698 sleep end
1620698097700 execute
1620698097701 start-lock execute end.
1620698097702 response send start.
1620698097721 response send end.
1620698102703 sleep end
1620698102704 start-lock2 execute end.
1620698102704 response send start.
1620698102705 response send end.

ロック処理内でのエラーについて

execute functionを使う場合、Promiseを使う場合、コールバック関数のみの場合問わず、ロック処理内でエラーを発生させたい場合は次のようにdone()を実行します。

done(new Error('test exception.'));

次のようにthrowを使うとすると実行エラーになるので注意してください。

throw new Error('test exception.');
にほんブログ村 IT技術ブログへ

コメント

メールアドレスが公開されることはありません。 が付いている欄は必須項目です