LoginSignup
2
2

実行時間制限を抜け出そう! GoogleAppsScript(GAS)の並列処理化のススメ

Last updated at Posted at 2023-12-21

GASの実行時間制限から抜け出すには

GoogleAppsScript(GAS)には、実行時間制限があることで有名です。
実行時間制限はプランによって違いますが、6分または30分の実行時間制限があります。
実行時間制限を超えると、「タイムアウト」となり、処理が中断されます。

実行時間制限を抜け出すには、以下の2つの方法があります。特に前者は有名です。

概要 詳細
方法1 トリガーを生成する 処理の途中で経過時間を計算し、制限時間近くになったら、処理再開用のトリガーを作成し、終了する。
方法2 ウェブアプリを並列実行 時間のかかる処理をウェブアプリ化して、呼び出し側から並列呼び出しする。

前者「トリガーを生成する」方法は、検索すれば出て来るので他の記事にお任せして、私は後者の記事を書きます。

GASを並列処理化して実行時間制限から抜け出そう

GASを並列処理化して効果が発揮されるのは、以下のサンプルのように、処理をループで実行している結果、全ての処理が終わらずタイムアウトエラーが起こる場合です。

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

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

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

並列処理のさせ方

1. ループで実行させている処理を、ウェブアプリ化する。

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

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

doPost() 関数や doGet() 関数については、説明はしません。ぜひ調べてください。
サンプル2.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です。

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

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

function fetchPostPara30Req() {
    //ウェブアプリを30個並列実行するサンプル
    const fetchUrl='[ウェブアプリのURL(1.で、デプロイしたときURLが表示されます。それをコピペ)]'
    //DriveApp.getRootFolder().getName(); //API実行時に401エラーが出た場合はこれを実行すると良さそう 
    //APIを全体公開出来ない場合はトークンが必要。トークンをfetch直前に取得
    //const token = ScriptApp.getOAuthToken(); //APIを全体公開出来ない場合この行が必要。
    
    //リクエスト配列の生成
    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}` //APIを全体公開出来ない場合この認証が必要
            },
            '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());
    }
}
  • ウェブアプリが全体公開できないとき、トークンの取得とセットが必要です。6行目と26行目のコメントアウトをはずしてください。
  • トークン取得とセットした場合でうまく動かない場合は、4行目のコメントアウトをはずすと動くようになります。いろいろ実験したのですが、なぜかは分かりません。経験的にそうなっています。

既知の問題

  • UrlFetchApp.fetchAllメソッドに引き渡すリクエスト数が多いと、以下のようなエラーが起こります。1回で引き渡すリクエスト数は100個ぐらいが良いでしょう。
    2023-12-21_09h33_30.png
  • 検証の結果、同時実行数が多いと、レスポンスの一部に以下のようなHTMLが返ってきます。同時実行数のエラー閾値は、よくわかりません。100個実行でもエラーが出ないときもあれば、60個でもエラーが出るときがあります。
    2023-12-21_15h26_57.png

既知の問題の対策

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

  1. メソッドの詳細はこちら

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2