Node.jsで「並列実行処理を一定間隔で繰り返す」

# はじめに

自身の業務上の課題を解決するために、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回に到達するまで）、というイメージ。

つまり、<b>並列実行処理を、一定の間隔で繰り返します</b>。

これによって、サーバー側への過剰負荷を避けながら、効率的に処理を完遂することを目指します。

加えて、API実行エラーがあったら、その社員に対する処理はリトライしたいので、エラーが発生したAPI呼び出しは結果として出力できるようにします。

※1回のAPIでエラーが発生しても後続の処理に影響はないものとします。

# サンプルプログラム(Node.js)

目的を実現するためのNode.jsスクリプトを準備しました。
Promise・await/async・resolveあたりを駆使して何とかできそうです。

尚、このスクリプトは<b>並列実行処理を一定間隔で繰り返す</b>ためのサンプルプログラムであり、実際にはAPI呼び出しは行っていません。
最小単位の処理として、1秒待機するだけの処理を`doUnitTask`として定義しています。
さらに、1%の確率でエラーが発生する処理として仮定しています。

```example.js
// 最小単位のタスク（次の状況を想定したダミーの処理）を定義する。
// - 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
```

# さいごに
はやく業務で使ってみたい。