GASでタイムアウトエラーを回避する方法【6分の壁/30分の壁】 | GASおじさんのブログ
GASのTips

GASでタイムアウトエラーを回避する方法【6分の壁/30分の壁】

6分の壁越える方法 GASのTips

みなさんこんにちは!GASおじさんです。

GAS中級者の前に必ず立ちはだかる「6分の壁」という問題があります。

GASの一度の実行時間は最大6分までで、それを越えるとタイムアウトエラーになってしまうという問題ですね。

今回はこの「6分の壁」を乗り越える方法について解説していきたいと思います!

なお、GoogleWorkspace有料プランの場合はGASの実行時間上限が6分ではなく30分となります。つまりこの場合は「30分の壁」となります。

GoogleWorkspace有償プランの利用者は、

  • 「6分」を「30分」
  • 「360秒」を「1800秒」

と置き換えながらご一読いただければと思います。

なお、Youtubeでも解説していますので、動画で見たい人は以下からどうぞ。

サンプルスクリプトを用意

まずはサンプルとなるスプレッドシートとスクリプトを用意します。

以下のようなスプレッドシートを新規作成してください。

  • A列に「index
  • B列に「経過秒数

というカラムをそれぞれ用意します。

次にスクリプトエディタを起動し、以下のスクリプトをコピペしてください。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  for(let index = 1; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)
    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

エディタに貼り付けたら保存して実行してみましょう。

すると、スプレッドシートに1行ずつ記録がされていくのが確認できると思います。

スクリプトの内容は、「indexを500回ループさせる」という内容ですので、

通常であればA列のindexが500になるまで記録されてほしいのですが、

実行して6分(360秒)経過すると、以下のようなタイムアウトエラーが発生します。

Exceeded maximum execution time

本記事のサンプルではindexが287回ループした時点でタイムアウトエラーとなりました。

これを287回で終わらせずに、500回まで完遂させるにはどうしたらいいか、という問題です。

GoogleWorkspaceの有償アカウントの場合は30分(1800秒)でタイムアウトエラーとなります。

解決方法

解決の大まかな流れは以下の通りです。

  1. 5分(300秒)経過した時点で実行を一度中断する
  2. トリガー設定して1分後に再開されるようにする
  3. スクリプトプロパティを活用して途中行から再開されるようにする
  4. 不要なトリガーやスクリプトプロパティを削除する

1個ずつ説明していきます。

5分(300秒)経過した時点で実行を一度中断する

実行開始してから6分(360秒)経過するとタイムアウトエラーとなってしまうので、5分(300秒)経過した時点で実行を一度中断することにしましょう。

なぜ5分なのか

なぜ5分なのかというと、キリがいいからです。それ以外は特に理由はありません。

今回は5分としておりますが、実際は6分以内であればなんでもいいので、「5分50秒」でも「5分30秒」でもOKです。

ただし、1回のループ処理が何秒くらいかかるのかを把握しておくことは重要です。

たとえば、1回のループが30秒かかるような処理の場合は、少なくとも30秒以上余裕を持たせる必要があるので、5分30秒以内で設定しなければならないでしょう。

実際の現場では、それぞれの環境にあった秒数を調整してください。

5分経過した時点でreturnして中断する

それではスクリプトを書き換えていきます。

以下のサンプルスクリプトでは、8行目のsecondsが経過秒数を表しています。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  for(let index = 1; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)
    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

なので、「secondsが300秒より大きければreturnする」というif文を書いてあげればいいですね。以下のようにスクリプトを書き換えましょう。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  for(let index = 1; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したらreturnして強制的に実行を中断する
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

10〜13行目が追加されました。こうすることで、実行開始から5分経過した時点で強制的に実行を中断することができます。

ちなみにこれで関数を実行してみると、indexが243となった時点実行が中断されました。

経過秒数は299.899秒で終了しています。約300秒ですね。

スクリプトエディタを確認すると、今回はタイムアウトエラーではなく、「実行完了」となっていることが確認できます。

これでひとまず、タイムアウトエラーを回避すること自体には成功しましたね。

トリガー設定して1分後に再開されるようにする

それでは次に、returnで強制的に中断する前に、1分後に実行が再開されるようトリガー設定をしていきましょう。

loop500times関数を以下のように書き換えてください。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  for(let index = 1; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したら、トリガーをセットして、returnする
      setTrigger();
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

12行目にsetTrigger関数の呼び出しを追加しました。なのでこのsetTrigger関数を定義しなければなりません。次のコードを追加でコピペしてください。

function setTrigger() {
  let triggers = ScriptApp.getScriptTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'loop500times'){
      ScriptApp.deleteTrigger(trigger);
    }
  }

  // 1分後にトリガーをセット(1分 = 60秒 = 1秒*60 = 1000ミリ秒 * 60)
  ScriptApp.newTrigger('loop500times').timeBased().after(1000 * 60).create();
}

以下のようになります。

こうすることで、300秒経過して実行中断される前に、その時点から1分後の特定の日時トリガーが設定されることになります。

たとえば、300秒経過した時点の日時が「2022年12月20日の12時00分」だったら、以下のような「2022年12月20日の12時01分」の特定の日時トリガーが設定されることになります。

これで中断から1分後にloop500times関数が再度実行されることになります。

スクリプトプロパティを活用して途中行から再開されるようにする

さて、これでトリガー設定も無事実装できました。しかし今のままだと、スクリプトが再開されたときに、indexが「1」から開始されてしまいます。

今回のケースでは、300秒時点で中断されたときのindexは「243」だったので、再開されるときは「244」からスタートされてほしいですよね。

それでは、この「244」という数字はどのように取得したらいいでしょうか?

スクリプトプロパティについて

ここで便利なのがスクリプトプロパティです。

スクリプトプロパティは、オブジェクト形式の情報をスクリプトエディタ上に保存することができるというもので、PropertiesServiceクラスを使うことで操作することができます。

スクリプトプロパティはScriptPropertiesクラスでも操作可能ですが、2022年12月20日現在、非推奨のクラスとなっておりますので、PropertiesServiceクラスを使うようにしましょう。

PropertiesServiceクラスについて、詳しくは以下の公式リファレンスを参照ください。

Properties Service  |  Apps Script  |  Google for Developers

スクリプトを修正

それではこのスクリプトプロパティを活用して、300秒時点でのindex番号を保存しましょう。以下のようにスクリプトを書き換えてみてください。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  for(let index = 1; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したら、スクリプトプロパティを設定し、トリガーをセットして、returnする
      PropertiesService.getScriptProperties().setProperty('nextIndex', index);
      setTrigger();
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

12行目を追加しました。

こうすることで、「nextIndex」というキーのプロパティを設定することができます。そしてのそのキーに対する値としては「index」、つまり今回でいうと「244」という数値が保存されます。

保存されたスクリプトプロパティは、スクリプトエディタ左側の歯車アイコンをクリックして出てくる「プロジェクトの設定」ページの下部で確認できます。

スクリプトプロパティに保存された値、今回でいうと「244.0」という値は、次の1行で取得することができます。

PropertiesService.getScriptProperties().getProperty('nextIndex')

これを利用して、スクリプトを以下のように書き換えてみましょう。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  let startIndex = Number(ScriptProperties.getProperty('nextIndex'));
  if (!startIndex) startIndex = 1; // もしstartIndexがnullの場合は1を代入
  for(let index = startIndex; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したら、スクリプトプロパティを設定し、トリガーをセットして、returnする
      PropertiesService.getScriptProperties().setProperty('nextIndex', index);
      setTrigger();
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }
}

6行目と7行目を追加し、8行目を修正しました。

  • 6行目・・・でスクリプトプロパティの値を取得し、変数startIndexに代入する(その際indexはfloat型の値になっているのでNumber変換しておく)
  • 7行目・・・スクリプトプロパティの値が設定されたいなかった場合(つまり最初の実行の場合)、startIndexは「1」とする
  • 8行目・・・for文の初期化式をlet index = startIndex;として、startIndexに代入された番号からループが開始されるようにする

ここであらためて最初から実行

ここまで書けたら、あらためて最初から実行してみたいと思います。

スプレッドシートの記録は一旦消去して真っ白にしておきます。

また、もしスクリプトプロパティが保存されている場合は、こちらも一旦消去しておきましょう。

それでは実行してみます。

300秒経過すると…

今回はindexが「223」まで記録されて中断されました。

この時点でスクリプトプロパティを確認してみます。

nextIndexに「224.0」が設定されていますね。

また、トリガーも確認してみましょう。

1分後に実行されるトリガーが設定されています。

そして1分待ってスプレッドシートを確認すると…

またスクリプトが動き始めました!224番から記録が再開されています!

その後、445番時点でまた300秒経過したため、再びスクリプトは中断。

そしてまた1分後に再開されて、無事500番まで到達することができました!

不要なトリガーやスクリプトプロパティを削除する

これにて実装完了!

…といいたいところですが、最後に一仕事だけやっておきましょう。

現状のままだと、500周のループが周った後に、不要なトリガーとスクリプトプロパティが残ってしまいます。

不要なトリガー
不要なスクリプトプロパティ

どちらも実行完了後は不要な情報です。

特にスクリプトプロパティのほうは次回スクリプトを実行するときに、実行内容に影響してしまうので、削除しなければなりません。

500周のループが終わった後に、トリガーとプロパティを削除するコードを書き加えましょう。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  let startIndex = Number(PropertiesService.getScriptProperties().getProperty('nextIndex'));
  if (!startIndex) startIndex = 1; // もしstartIndexがnullの場合は1を代入
  for(let index = startIndex; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したら、スクリプトプロパティを設定し、トリガーをセットして、returnする
      PropertiesService.getScriptProperties().setProperty('nextIndex', index);
      setTrigger();
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }

  // 500周し終えたらトリガーを削除
  let triggers = ScriptApp.getScriptTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'loop500times'){
      ScriptApp.deleteTrigger(trigger);
    }
  }

  // 500周し終えたらスクリプトプロパティを削除
  PropertiesService.getScriptProperties().deleteProperty('nextIndex');  
}

26〜31行目でトリガーを削除し、34行目でスクリプトプロパティを削除しています。

これにて実装完了です!

まとめ

以上、「GASでタイムアウトエラーを回避する方法」について解説してきました。

あらためて、完成形のコードを載せておきます。

function loop500times() {
  let startTime = new Date(); // ①実行開始時点の日時
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('シート1');

  let startIndex = Number(PropertiesService.getScriptProperties().getProperty('nextIndex'));
  if (!startIndex) startIndex = 1; // もしstartIndexがnullの場合は1を代入
  for(let index = startIndex; index <= 500; index++){
    let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      // 300秒経過したら、スクリプトプロパティを設定し、トリガーをセットして、returnする
      PropertiesService.getScriptProperties().setProperty('nextIndex', index);
      setTrigger();
      return;
    }

    sheet.getRange(index+1, 1).setValue(index);   // A列にindexを入力
    sheet.getRange(index+1, 2).setValue(seconds); // B列に経過秒数を入力
    SpreadsheetApp.flush();
    Utilities.sleep(1000);  // 1秒スリープさせる(1秒 = 1000ミリ秒)
  }

  // 500周し終えたらトリガーを削除
  let triggers = ScriptApp.getScriptTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'loop500times'){
      ScriptApp.deleteTrigger(trigger);
    }
  }

  // 500周し終えたらスクリプトプロパティを削除
  PropertiesService.getScriptProperties().deleteProperty('nextIndex');  
}

function setTrigger() {
  let triggers = ScriptApp.getScriptTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'loop500times'){
      ScriptApp.deleteTrigger(trigger);
    }
  }

  // 1分後にトリガーをセット(1分 = 60秒 = 1秒*60 = 1000ミリ秒 * 60)
  ScriptApp.newTrigger('loop500times').timeBased().after(1000 * 60).create();
}

これでGAS中級者が必ずぶち当たる壁、通称「6分の壁(または30分の壁)」を乗り越えることができますね!

ポイントは「いかにスクリプトプロパティを使いこなせるか」にかかっています。

スクリプトプロパティについて理解があやふやな人は、リファレンスを読んだりコードを動かしてみたりして、理解を深めるようにしましょう。

あらためて公式リファレンスを載せておきますね。

Properties Service  |  Apps Script  |  Google for Developers

それではまた次回!

コメント

タイトルとURLをコピーしました