GoogleAppsScript
gas
Slack
Webhook

Webhookで起動したGAS(Google AppsScript)の応答を非同期処理で高速化する

はじめに

GAS最高ですか,最高ですね。
私は普段slackからwebhookでGASをバシバシ叩いているのですが,簡単にweb上にシステムを放り投げられるのは快感があります。しかし,GASにも弱点はあります。よく挙げられるのがやはり処理が遅い問題でしょうか。

特にslackのslash commandsやinteractive messageの機能を活用していると,そこには3秒でTimeoutという壁があります。GASを高速化するノウハウは世の中に色々とありますが,やはり限界はあります。例えばAPIを叩く処理は必然的に重くなりがちで,また回数を減らすにも限界があります。

先にクライアント側へ応答を返した後にゆっくり重い処理を実行できれば良いのですが,GASは非同期処理は基本的にお断りになっており難しい状況があります。

今回はGASのcacheサービスと定期実行の機能を活用して擬似的に非同期処理を実現できたので,簡単に共有させていただきます。

GASでの非同期処理の実現

今回扱う例

あくまで例として,以下のようなGASスクリプトがあったとします。

function doPost(e){
  addJobQue(e.parameter.a, e.parameter.b);
  return "OK";
}

function doSomething(a, b){
  //API読み出しやSpreadsheetの書き込み等重い処理
}

WebhookでGASが発火し,受け取ったJSONの要素を引数に処理を実行,応答として"OK"等適当な言葉を返すとします(実際には応答もJSONに落とし込む等しますが)。
重要であることは,主な処理と応答には関係がないにも関わらず,応答を返す前に処理の完了を待たなくてはいけない点です。timeoutまでの時間は刻々と迫っています。

そこで今回は,doPost()内では直接処理を実行するのではなくジョブキューの追加のみを行い,GASの定期実行の機能でそのジョブキューを後から捌いていく事で非同期処理を実現しました。上記スクリプトは以下のように書き換えられます。

function doPost(e){
  addJobQue(e.parameter.a, e.parameter.b);
  return "OK";
}

function addJobQue(a, b){
  //ジョブキューを追加だけしてすぐに終了.
}

function timeDrivenFunction(){
  //定期実行でジョブキューを読み出し,各々の引数でdoSomething()をキューの数だけ実行.
}

function doSomething(a, b){
  //API読み出しやSpreadsheetの書き込み等重い処理.
}

上記例ではdoPost()が発火した際に行う処理はジョブキューの追加のみであり,高速化が見込まれます。
各関数の内容は後ほどで説明いたします。

ジョブキューの実装と実行

ジョブキューを溜める場所はスプレッドシートでも良いのですが,もっとシンプルにできないかと考えた結果,GASのcache機能をその場所として活用できないかと思いつきました。
GASのcacheは(key, value)型の簡易ストレージキャッシュです。

Class CacheService -Apps Script(Google Developers)
https://developers.google.com/apps-script/reference/cache/cache-service

GASのcacheには
1. Document Cache
2. Script Cache
3. User Cache
の3種類がありますが、これらはスコープの違いであり動きは同じです。

var str "cacheにはstringを入れます";
cache = CacheService.getScriptCache();

//cache.put(key, string, lifespan[s])で保存.
cache.put("key", str, 60*60*24);

//cache.get(key)で読み出し.
var data = cache.get("key");

valueには文字列のみが保存でき,最大24時間(60*60*24秒)保存することができます。
今回はこのcacheにオブジェクトを配列として蓄積していき,それを定期実行で後から読み出します。保存期間の問題はありますが,定期実行のサイクルが遥かに短い場合問題にはなりません。

ジョブキューの追加

function addJobQue(a, b){
  //引数をオブジェクトとしてまとめる
  var newObj = {
    "a": a,
    "b": b
  }

  cache = CacheService.getScriptCache();
  var data = cache.get("key");

  //cacheの中身がnullならば空配列に,nullでないならstrを配列に変換する.
  if(data==null){
    data = [];
  }else{
    data = data.split(';');
  }

  //オブジェクトであるnewDataをstrに変換して配列に追加.
  data.push(JSON.stringify(newObj));

  //配列を;で分割するstrに変換.
  cache.put("jobId", data.join(';'), 60*2); 

  return;
}

まず,引数aとbをオブジェクトとしてまとめます。次にcacheから既存のジョブキュー配列を読みだす訳ですが,cacheにはstring型しか格納できないため,配列は";"で区切られたstringに変換してあります。

["a", "b", "c"] -> "a;b;b"

cacheから読み出し復元した配列に対して,引数を格納したオブジェクトをpushで追加します。この際も,cacheにはstringしか格納できないため,オブジェクトをJSON.stringfy()で文字列に変換しています。

その後再度cacheに追加してやれば,ジョブキューの蓄積は完了です。上記ではcacheの保持期間を2分としていますが,これはdoSomething()の処理の重さによって適切に決めましょう。

ジョブキューの実行

次に,溜まっていくジョブキューをGASの定期実行機能にて捌いていきます。

スクリーンショット 2018-08-05 1.41.22_edited-1.png
定期実行は赤い丸の部分ですね。定期実行では以下のようなスクリプトを実行します。

function timeDrivenFunction(){
  //cacheを取得
  cache = CacheService.getScriptCache();
  var data = cache.get("key");

  //cacheの読み書きの競合が怖いのでなるべく早く消しておく
  cache.remove("key");

  //cacheの中身がnullならば空配列に,nullでないならstrを配列に変換.
  if(data==null){
    return;
  }else{
    data = data.split(';');
  }

  //配列の中身をstrからJSON(object)に戻し,処理を実行する
  for(var i=0; i<data.length; i++){
    data[i] = JSON.parse(data[i]);
    doSomething(data[i].a, data[i].b);
  }
  return;
}

前半は先ほどと同じ内容です。
後半では配列に格納されているオブジェクトを表す文字列をオブジェクトに戻し,doSomething()へオブジェクト内の引数を投げています。
定期実行の間隔についてはdoSomething()の処理の内容やサービスの規模によりますが,一応以下の式で表せます。

(定期実行の間隔) > (実行間隔あたりに積まれるジョブキューの最大数) * (ジョブキュー1つあたりの実行時間)

私の場合は課題がtimeout3秒の制約であって処理自体は数秒で終わるもののだったので,上記の定期実行は1分毎に実行しています。
この辺はサービスの規模や内容によりますが,今回はcacheの追加や読み出し,削除にLock制御を設けていないので,規模が大きくなる場合は競合を防ぐため適切にLockしなければなりません。

結果

元々の実行時間のスクショを取っていないのがお恥ずかしいですが,元々1.2秒ほどかかっていた処理が上記まで短縮できました。元々の処理はスプレッドシートの書き換えとその内容のslackへの通知(WebAPIを叩く)でした。

今回は処理の内容が最大1分程度遅れても問題ない内容のために使えた方法だとは思いますが,汎用性も高いと思うので是非GASで何とか非同期処理ができないかと考えた際ほんの少しでも参考になれば嬉しいです。

参考

Class CacheService -Apps Script(Google Developers): https://developers.google.com/apps-script/reference/cache/cache-service
GoogleAppsScript(GAS)でのcacheあれこれ: https://befool.co.jp/blog/8823-scholar/gas-use-cache/