実行時間制限を抜け出そう！ GoogleAppsScript（GAS）の並列処理化のススメ　その２

## 既知の問題（再掲）
前回の投稿

https://qiita.com/Cheap-Engineer/items/1428546e17f1348b933a

にて、以下２つの問題がありました。
今回は、この２つを解決しようと思います。

#### 問題１
`UrlFetchApp.fetchAll`メソッドに引き渡すリクエスト数が多いと、以下のようなエラーが起こります。
![2023-12-21_09h33_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89689/70581806-cbcb-7293-5a5f-ef767216a1a6.png)
#### 問題２
検証の結果、同時実行数が多いと、レスポンスの一部に以下のようなHTMLが返ってきます。
![2023-12-21_15h26_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89689/e1db75aa-93d4-c426-654f-958f50d4e6c2.png)

## 問題１の解決策
この問題の解決策としては、リクエストを１００個ずつに分割して、`fetchAll
()`メソッドに渡す方法が良いでしょう。
と、いうことで、`chunkRequest()`関数を作ることにします。
この関数は、リクエスト配列を指定された数で分割し、分割後のリクエスト配列を要素とした配列を返します。

```js
/**
 * リクエスト配列を指定された数で分割し、分割されたリクエスト配列を要素とした二次元配列を返す。
 * @param {object[]} requests - 分割対象のリクエスト配列
 * @param {number} chunkSize - 分割するサイズ
 * @return {object[][]} - 分割されたリクエスト配列を要素とした二次元配列
*/

function chunkRequest(requests,chunkSize) {
    let chunkedRequests = [];

    // whileループで元の配列から要素を取り出す。
    while(requests.length > 0) {
        chunkedRequests.push(requests.splice(0, chunkSize));
    }

    return chunkedRequests;
}
```

## 問題２の解決策
この問題の解決策としては、再帰関数を利用すると良いでしょう。
と、いうことで、`fetchParaRequests()`関数を作ることにします。
この関数は、`UrlFetchApp.fetchAll()`メソッドを実行し、レスポンス内に「同時に実行中のスクリプト」が含まれていたら、再実行用のリクエスト配列に、エラーが出たリクエストを追加します。

```js
/**
 * リクエスト配列を、fetchAll()メソッドを実行し、レスポンス内に、「同時実行数が多い」という
 * エラーが出たリクエストは、再実行用リクエスト配列に追加し、再実行用リクエスト配列を引渡し
 * 再帰実行する。
 * その他のレスポンスは、レスポンスの配列に追加され、関数終了時に返す。
 * 
 * @param {object[]} requests - 並列実行させるリクエストの配列
 * @param {number} count - 再帰実行カウント用。呼び出し時は、０を引き渡す。
 * @return {object[]} - レスポンスを要素とした配列
*/

function fetchParaRequests(requests,count) {
    let reRequests = [];
    let reses = [];
    Logger.log('再帰実行カウント： ' + count);
    count++;

    const res = UrlFetchApp.fetchAll(requests);

    for (let i = 0; i < res.length; i++) {     
        if (res[i].getContentText().includes('同時に実行中のスクリプト')) {
            //レスポンス内に「同時に実行中のスクリプト」という文字が含まれていた場合（＝同時実行数エラー）
            Logger.log('再実行 要素No.' + i);
            reRequests.push(requests[i]);//該当リクエストを再実行用配列に追加
        } else {
            reses.push(res[i]);//結果をレスポンス配列に追加
        }
    }

    if (reRequests.length != 0) {
        //再実行用配列の要素数が０でない（＝再実行対象のリクエストがある。）
        Utilities.sleep(10000);//１０秒休んで
        //再帰呼び出しする。再帰呼び出しの結果を reses に追加
        reses = reses.concat(fetchParaRequests(reRequests,count)); 
    }
    return reses;
}
```

## 既知の問題に対応したメインルーチン
前回の投稿のコードを、書き換えます。
今回、既知の問題対応のため、`chunkRequest()`関数と、再帰関数の`fetchParaRequests()`関数を新たに作ったので、それを処理に組み込みましょう。
さらに、実行数の制限がなくなったので、３０個の並列実行を、３００個の並列実行にしてみます。
リクエストで送信する「age」が、最大３３４という、ありえない数になってしまいますが...（汗

```js
function fetchPostPara300Req() {
    //ウェブアプリを３００個並列実行するサンプル
    const fetchUrl='[ウェブアプリのURL]';
    //DriveApp.getRootFolder().getName(); //ＡＰＩ実行時に４０１エラーが出た場合はこれを実行すると良さそう 
    //ＡＰＩを全体公開出来ない場合はトークンが必要。トークンをfetch直前に取得
    //const token = ScriptApp.getOAuthToken(); //ＡＰＩを全体公開出来ない場合この行が必要。

    //リクエスト配列の生成
    let requests = [];

    for (let i = 0; i < 300; i++) {
        //ウェブアプリに引き渡すデータ部分を生成。
        let data = {
            'name': 'Bob Smith',
            'age': 35 + i, //すべて同じリクエストだと寂しいので少し変える。
            'hobby' : '他人のソースコードのダメ出し。// : ? . % a',
            'pets': ['fido', 'fluffy']
        };

        //リクエストのJavaScriptオブジェクトを作成
        let request = {
            'url': fetchUrl,
            'method' : 'post',
            'headers': {
                'Content-Type': 'application/json'
                //,'Authorization': `Bearer ${token}` //ＡＰＩを全体公開出来ない場合この認証が必要
            },
            'payload' : JSON.stringify(data) //ウェブアプリに引き渡すデータ部分をJSON変換し、payloadに配置。
        };

        //リクエスト配列にリクエストのJavaScriptオブジェクト追加
        requests.push(request);
    }

    //１００個ずつのリクエストに分割
    const chunkedRequests = chunkRequest(requests,100);
    //１００個ずつリクエストを送信
    for (let j = 0; j < chunkedRequests.length; j++) {
        const res = fetchParaRequests(chunkedRequests[j],0);
        //レスポンスの確認
        for (let k = 0; k < res.length; k++) {
            Logger.log(res[k].getContentText());
        }
    }
}
```