GoogleAppsScript屋さん

GoogleAppsScript のサンプルコードなどを載せていきます

ダイアログを使ってタイムアウトを回避する

GoogleAppsScript を使っていて度々悩まされるのは「6分でプログラムがタイムアウト終了する」ことです。

Quotas for Google Services  |  Apps Script  |  Google Developers

直ぐに終わる処理なら気にする必要ありませんが、データを処理するようなプログラムを書く場合、データが増えるにつれてどうしても処理時間が伸びてしまうため、いまは6分以内に処理が終了していたとしても、将来のことを考えるとタイムアウトの回避方法は考えておいた方が良いでしょう。

タイムアウト対策としてよく知られているのは「タイムアウト前に次のプログラム起動をトリガーで設定する」ことです。 それもひとつの方法ですが、より簡単にタイムアウトを回避する方法を考えたので紹介したいと思います。

簡単に説明すると「ダイアログからGASを細かく起動する」という方法です。 GAS では HTML ファイルをもとにダイアログを表示することができますが、HTML ファイルの中に GAS を埋め込むこともできます。 「6分でタイムアウトする」というのは GAS を一回起動した時の制限になりますので、トータルで10分かかる GAS の処理でも、HTML から1分間隔で10回 GAS を呼びだせば、トータル処理時間は同じでも、タイムアウトを回避することが出来ます。

もちろんこの方法が使用できないシチュエーションもあります。 例えばプログラムを定期実行している場合はダイアログを操作する人がいないため、この方法は使えません。 ですが「人が操作していて」「処理時間が長くなる」ようなケースでは効果的な回避方法になりえます。

それではサンプルとして「10秒間隔で1分間シートに書き込み続けるGAS」を「HTMLから10回呼び出す」プログラムを紹介します。 トータル処理時間は10分になりますので、これが最後まで動作したら「6分のタイムアウトを回避した」と言えるのではないでしょうか。

サンプルコード

以下2ファイルが登場します。

  • コード.gs
  • dialog.html

コード.gs

function dialog() {
  var html = HtmlService.createHtmlOutputFromFile("dialog");
  SpreadsheetApp.getUi().showModalDialog(html, "ダイアログ")
}

function continueWritingOneMinute() {  
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  for (var i = 0; i < 6; i++) {
    Utilities.sleep(10000);  //10秒
    sheet.appendRow([Utilities.formatDate(new Date(), "JST", "HH:mm:ss")]);
  }
  return true;
}

function msgBox(message) {
  Browser.msgBox(message);
}

dialog.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      var i = 0;
      
      // run() を呼び出す
      function onSuccess() {
        i++;
        if (i < 10) {
          run();
        } else {
          google.script.run.msgBox("処理が終了しました");
        }
      }
      // GAS を呼び出す
      function run() {
        google.script.run
          .withSuccessHandler(onSuccess)
          .continueWritingOneMinute();
      }
      
    </script>
  </head>
  <body>
    <input type="button" value="書き込む" onClick="run()">
  </body>
</html>

実行の流れ

当サンプルコードを実行した時の流れです。

  1. GAS側の dialog() を実行すると GAS 起動用のダイアログが表示されます。 f:id:rokuni62:20171016124228p:plain

  2. 「書き込む」ボタンを押すと処理が開始され、少し待つとシートに時刻が設定されていきます。 f:id:rokuni62:20171016124237p:plain

  3. 10分経つと処理終了のポップアップが表示されます。 f:id:rokuni62:20171016125501p:plain

  4. シートを確認してみると10分間、10秒間隔で時刻が設定されていることが確認できます。 f:id:rokuni62:20171016125538p:plain f:id:rokuni62:20171016125539p:plain

解説

(1) dialog() では dialog.html をダイアログとして表示しています。

function dialog() {
  var html = HtmlService.createHtmlOutputFromFile("dialog");
  SpreadsheetApp.getUi().showModalDialog(html, "ダイアログ")
}

(2) 呼び出された dialog.html の「書き込む」ボタンを押すと script タグ内の run() が呼び出されます。

<input type="button" value="書き込む" onClick="run()">

(3) run() では 1分間10秒間隔で書き込み続ける GAS のcontinueWritingOneMinute() を呼び出しています。

 // GAS を呼び出す
function run() {
  google.script.run
    .withSuccessHandler(onSuccess)
    .continueWritingOneMinute();
}

ここで重要なのは withSuccessHandler(onSuccess)のところです。 これは continueWritingOneMinute() が正常終了したら onSuccessを実行する、という意味になります。 もし単純に for 文で continueWritingOneMinute() を10回呼び出したとすると、それぞれの処理が平行に実行されてしまい、思った通りに動いてくれません。

(4) continueWritingOneMinute() は「シート1」というシートに対して10秒間隔で時刻を記載する処理を6回繰り返します(つまり1分)。

function continueWritingOneMinute() {  
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  for (var i = 0; i < 6; i++) {
    Utilities.sleep(10000);  //10秒
    sheet.appendRow([Utilities.formatDate(new Date(), "JST", "HH:mm:ss")]);
  }
  return true;
}

(5) continueWritingOneMinute() の処理が終了したら dialog.htmlonSuccess() が実行されます。

var i = 0;
      
// run() を呼び出す
function onSuccess() {
  i++;
  if (i < 10) {
    run();
  } else {
    google.script.run.msgBox("処理が終了しました");
  }
}

グローバル変数 i を呼出回数を記録するために用意しておき、10回未満であれば run() 経由で continueWritingOneMinute() を呼び出し、すでに10回呼び出しているのであれば処理終了メッセージを表示します。 「10回繰り返す」という処理を繰返し構文ではなく再帰処理で実現している形になります。 最後に処理終了メッセージを表示して処理は終了です。

参考

HTML Service: Communicate with Server Functions  |  Apps Script  |  Google Developers