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

## GASの実行時間制限から抜け出すには
GoogleAppsScript（GAS）には、実行時間制限があることで有名です。
実行時間制限はプランによって違いますが、６分または３０分の実行時間制限があります。
実行時間制限を超えると、「タイムアウト」となり、処理が中断されます。

実行時間制限を抜け出すには、以下の２つの方法があります。特に前者は有名です。
| | 概要 | 詳細 |
|:-----------|:-----------|:------------|
|方法１| トリガーを生成する | 処理の途中で経過時間を計算し、制限時間近くになったら、処理再開用のトリガーを作成し、終了する。 |
|方法２| ウェブアプリを並列実行 | 時間のかかる処理をウェブアプリ化して、呼び出し側から並列呼び出しする。|

前者「トリガーを生成する」方法は、検索すれば出て来るので他の記事にお任せして、私は後者の記事を書きます。
 
## GASを並列処理化して実行時間制限から抜け出そう
GASを並列処理化して効果が発揮されるのは、以下のサンプルのように、処理をループで実行している結果、全ての処理が終わらずタイムアウトエラーが起こる場合です。

```js
for (let i = 0; i < 30; i++) {
    //時間のかかる処理　sleepメソッドで代用
    Utilities.sleep(180000);
}
```

上記の場合、プランによってですが、２回目くらい、または、１０回目くらいの処理で、タイムアウトエラーが起こります。

このような場合は、ループ内の処理をウェブアプリ化して、
` UrlFetchApp.fetchAll() ` メソッドで並列処理をする方法を使ったらどうでしょう、というのが今回の話です。

## 並列処理のさせ方

#### 1. ループで実行させている処理を、ウェブアプリ化する。
GASのプロジェクトを新たに作成します。
ループで実行させている処理を、コードに書き込み、ウェブアプリ化しましょう。
上記のサンプルだと、以下のコードのようになると思います。プロジェクトのコードに書き込みましょう。

```js
function doPost(e) {
    //時間のかかる処理　sleepメソッドで代用
    Utilities.sleep(180000);
}
```

` doPost() ` 関数や　` doGet() ` 関数については、説明はしません。ぜひ調べてください。
`サンプル２.js`だと面白味がないので、少し改造して、引数を持たせましょう。
以下のようになります。プロジェクトのコードに上書きしましょう。

```js
function doPost(e) {
    const contents = JSON.parse(e.postData.contents);//パースする。
    const name = contents.name;
    const hobby = contents.hobby;
    const age = contents.age;

    //時間のかかる処理　sleepメソッドで代用
    Utilities.sleep(180000);
    return ContentService.createTextOutput('私の名前は' + name + '、年齢は' + age + '、趣味は' + hobby);
}
```

プロジェクトをウェブアプリとしてデプロイします。

#### 2.fetchAll()メソッドで並列実行する。 
もう一つプロジェクトを作成します。
` UrlFetchApp.fetchAll() ` メソッドで、先ほどデプロイしたウェブアプリを並列実行させるコードを書きます。

ウェブアプリを実行するときは、` UrlFetchApp.fetch() `メソッドを使うのが一般的です。しかし、` UrlFetchApp.fetch() `メソッドは、ひとつのウェブアプリを実行し、レスポンスが返ってくるまで待ちます。並列実行ではなく、直列実行となってしまいます。
` UrlFetchApp.fetchAll() ` メソッドは、複数のウェブアプリを実行し、全てのレスポンスが返ってくるまで待つという、**並列実行**を可能とするメソッド[^1]です。

[^1]: メソッドの詳細は[こちら](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app?hl=ja#fetchallrequests "Google For Developers")

` UrlFetchApp.fetchAll() ` メソッドは、引数に、 要素にリクエストのJavaScript オブジェクトを格納した配列を渡します。コードは以下のようになるでしょう。

コードを書いたら、fetchPostPara30Req関数を実行します。

```js
function fetchPostPara30Req() {
    //ウェブアプリを３０個並列実行するサンプル
    const fetchUrl='[ウェブアプリのURL（1.で、デプロイしたときURLが表示されます。それをコピペ）]'
    //DriveApp.getRootFolder().getName(); //ＡＰＩ実行時に４０１エラーが出た場合はこれを実行すると良さそう 
    //ＡＰＩを全体公開出来ない場合はトークンが必要。トークンをfetch直前に取得
    //const token = ScriptApp.getOAuthToken(); //ＡＰＩを全体公開出来ない場合この行が必要。
    
    //リクエスト配列の生成
    let requests = [];
    
    for (let i = 0; i < 30; i++) {
        //ウェブアプリに引き渡すデータ部分を生成。
        const data = {
            'name': 'Bob Smith',
            'age': 35 + i,
            'hobby' : '他人のソースコードのダメ出し。// : ? . % a',
            'pets': ['fido', 'fluffy']
        };
    
        //リクエストのJavaScriptオブジェクトを作成
        const request = {
            'url': fetchUrl,
            'method' : 'post',
            'headers': {
                'Content-Type': 'application/json'
                //,'Authorization': `Bearer ${token}` //ＡＰＩを全体公開出来ない場合この認証が必要
            },
            'payload' : JSON.stringify(data) //ウェブアプリに引き渡すデータ部分をJSON変換し、payloadに配置。
        };
        
        //リクエスト配列にリクエストのJavaScriptオブジェクト追加
        requests.push(request);
    }
    
    const res = UrlFetchApp.fetchAll(requests);//リクエスト配列をリクエスト指示
    
    //レスポンスの確認
    for (let j = 0; j < res.length; j++) {
    Logger.log(res[j].getContentText());
    }
}
```
:::note
* ウェブアプリが全体公開できないとき、トークンの取得とセットが必要です。６行目と２６行目のコメントアウトをはずしてください。
* トークン取得とセットした場合でうまく動かない場合は、４行目のコメントアウトをはずすと動くようになります。いろいろ実験したのですが、なぜかは分かりません。経験的にそうなっています。
:::


## 既知の問題
* `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)

## 既知の問題の対策
上記、既知の問題の対策についての投稿は、こちらです↓

https://qiita.com/Cheap-Engineer/items/28e2874d8c528264982f