はじめに
自身の業務上の課題を解決するために、Node.jsのスクリプトを作りました。
そのメモです。
背景
ある会社にて、社員向けの業務システムがあります。
そのシステムでは、年に一度、社員情報の初期化を行う必要があります。
社員情報を初期化するには、社員情報更新APIを使用します。
1回のレスポンスタイムは1~2秒かかります。
そのAPIでは、引数に社員リストを指定できません。
1社員あたりにつき、1回APIを呼ぶ必要があります。
また、そのシステムで管理している社員は10,000人とします。
さて、10,000人分の社員情報初期化を考えると…
- 10,000回のAPI呼び出しを同時実行 → サーバー側で処理を捌ききれない
- 10,000回のAPI呼び出しを順次実行 → 時間がかかりすぎる(1回あたり2秒とすると、20,000秒 = 5時間)
つらい。
サーバーへの過剰負荷を避けつつも、効率的に全ての処理を完遂したい。
方針
APIの呼び出し方をクライアント側で工夫します。
10,000回のAPI呼び出しを同時に実行するのでもなく、順次に実行するのでもなく、中間をとります。
例えば、2,000回ずつのAPI呼び出し処理に分けて、2秒間隔で繰り返し実行する(10,000回に到達するまで)、というイメージ。
つまり、並列実行処理を、一定の間隔で繰り返します。
これによって、サーバー側への過剰負荷を避けながら、効率的に処理を完遂することを目指します。
加えて、API実行エラーがあったら、その社員に対する処理はリトライしたいので、エラーが発生したAPI呼び出しは結果として出力できるようにします。
※1回のAPIでエラーが発生しても後続の処理に影響はないものとします。
サンプルプログラム(Node.js)
目的を実現するためのNode.jsスクリプトを準備しました。
Promise・await/async・resolveあたりを駆使して何とかできそうです。
尚、このスクリプトは並列実行処理を一定間隔で繰り返すためのサンプルプログラムであり、実際にはAPI呼び出しは行っていません。
最小単位の処理として、1秒待機するだけの処理をdoUnitTask
として定義しています。
さらに、1%の確率でエラーが発生する処理として仮定しています。
// 最小単位のタスク(次の状況を想定したダミーの処理)を定義する。
// - 1秒の遅延が発生する
// - 1%の確率でエラーが発生する
function doUnitTask(num) {
return new Promise(function (resolve, reject) {
setTimeout(function(){
if (Math.random() <= 0.01) {
// ※Promise.all().catch()は最初のrejectのみキャッチされる。
// 複数のエラーハンドリングに対応できるように、rejectは使わずにresolveで例外を発生させる。
resolve(new Error(`Error occured! [id: ${num}]`));
// reject();
} else {
resolve();
}
}, 1000);
});
}
async function main() {
// 全タスク数
let TASK_NUM = 1000;
// いくつのタスクに分割するか
let DEVIDE_LENGTH = 100;
// タスク群を実行する間隔[ms]
let DURATION = 2000;
// 全てのタスクを実行するための配列を用意
let allTasks = [];
for (let i = 0; i < TASK_NUM; i++){
allTasks.push(i)
}
// 指定した数毎にタスクを分割する
let dividedAllTasks = []
for(let i = 0; i < allTasks.length; i += DEVIDE_LENGTH){
dividedAllTasks.push(allTasks.slice(i, i + DEVIDE_LENGTH));
}
// 「複数タスク」を「定期的」に「並列実行」する
let successedTaskNum = 0
let erroredTaskNum = 0
for (let i = 0; i < dividedAllTasks.length; i++){
let currentTasks = dividedAllTasks[i]
const promises = [];
for (let j = 0; j < currentTasks.length; j++){
let unittask = currentTasks[j];
promises.push(doUnitTask(unittask));
}
await new Promise(function(resolve, reject) {
setTimeout(function() {
Promise.all(promises)
.then(function(values) {
values.filter(function(item){
// エラーハンドリング
if (item instanceof Error) {
console.log(item.message)
erroredTaskNum++;
} else {
successedTaskNum++;
}
})
// 進捗率を表示
console.log(`Proccessing...(${Math.round((successedTaskNum + erroredTaskNum) / TASK_NUM * 100)}%)`)
resolve()
})
// ※Promise.all().catch()は最初のrejectのみキャッチする。
// 複数のrejectに対応できないため使用しない。
// .catch(function(reason){
// console.log(`${reason}`)
// })
}, DURATION)
});
}
console.log(`all done.`)
console.log(`SUCCESS TASK NUM : ${successedTaskNum}`)
console.log(`ERROR TASK NUM : ${erroredTaskNum}`)
}
main()
実行例
$ node example.js
start.
Error occured! [id: 168]
Error occured! [id: 183]
Error occured! [id: 319]
Error occured! [id: 551]
Error occured! [id: 580]
Error occured! [id: 838]
Error occured! [id: 943]
Proccessing...(10%)
...(略)...
Proccessing...(90%)
Error occured! [id: 9152]
Error occured! [id: 9281]
Error occured! [id: 9814]
Error occured! [id: 9860]
Error occured! [id: 9972]
Proccessing...(100%)
all done.
SUCCESS TASK NUM : 9899
ERROR TASK NUM : 101
さいごに
はやく業務で使ってみたい。