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個ぐらいが良いでしょう。
- 検証の結果、同時実行数が多いと、レスポンスの一部に以下のようなHTMLが返ってきます。同時実行数のエラー閾値は、よくわかりません。100個実行でもエラーが出ないときもあれば、60個でもエラーが出るときがあります。
既知の問題の対策
上記、既知の問題の対策についての投稿は、こちらです↓