ZeroScript

ゼロからわかるスクリプト

【限界突破】GASの6分の実行制限の壁を回避する方法

f:id:tabemi:20210525225303j:plain
どうも、たべみです。


業務改善をしているとどうしても気になるGASの実行制限
個人アカウントは6分、その他組織アカウント(有料アカウント)は30分です。


さて、これらを超えないためにAPIの呼び出しを工夫をして減らしたりトリガーを設置したりと対策に苦労している方も多いのではないでしょうか。

私も以前、21万の資料をオーナー権を移譲し、さらにコピーを作成しなければならず、とても制限時間内に行えないかつトリガーを設置しても日が暮れるどころか、季節が変わるのでは。ということがありました。


そこで今回は実行制限を限りなくスルーできるゾンビコードを作成していきます。

解説

今回は、お店を例にしてみました。

f:id:tabemi:20210525214653p:plain:w600
order一覧。3つの料理を一つずつ順番に作った場合、2分15秒かかってしまいます。


SpreadSheet上に3つのオーダーがあり、それぞれA列に調理時間(ミリ秒)、B列に素材、C列に完成品(料理)を出力します。C列は調理後に出力されます。

1つの料理を作成するのにA列の調理時間がかかるとすると、
普通にFor文を回した場合、調理時間がそのまま加算され135000ミリ秒、つまり2分15秒かかってしまうことになります。


どうせなら6分超える時間に設定すればよかったと後悔していますが、、まあ、やっていきましょう笑


早速コードを見てみる

長々と前置き失礼しました。
百聞は一見に如かずです。

コード.gs
/**
 * SSにメニューを表示
 */
function onOpen() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const myMenu = [
    { name: 'サイドバーを表示', functionName: 'openSidebar' }
  ];
  ss.addMenu('調理', myMenu); //メニューを追加
}
function openSidebar() {
  const htmlOutput = HtmlService.createTemplateFromFile('index').evaluate().setTitle('快適');
  SpreadsheetApp.getUi().showSidebar(htmlOutput);
}

/**
 * 全てのオーダーを返す
 */
function getOrders() {
  const [ header,...order] = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getDataRange().getValues();
  return JSON.stringify(order);
}

function cooking(json) {
  const { i, cookingTime, material} = JSON.parse(json);
  Utilities.sleep(cookingTime);//調理中....

  const ramen = material +'ラーメン';
  const row = i*1 +2;
  const column = 3;
  SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getRange(row, column).setValue(ramen);//シートに反映
  return ramen;
}

続いてクライアント側

index.html
<head>
  <base target="_top">
</head>
<div id="box"></div>

<script>
  let orders;
  let stopWatch;
  let waitingClient = 0;//処理の終了を把握する
  // サーバ側のgetOrder関数を呼び出し、成功したらreqestOrder()
  google.script.run.withSuccessHandler(reqestOrder).withFailureHandler(missFunc).getOrders();
  function reqestOrder(json) {
    //グローバルに変数に
    orders = JSON.parse(json);
    //調理開始ボタンを作成
    const button = document.createElement('button');//料理開始ボタンを作成
    button.textContent = '調理開始';
    button.setAttribute('id', 'start-button');
    button.addEventListener('click', startCooking);//クリック時にstartCooking関数を実行
    const box = document.getElementById('box');
    box.appendChild( button );//Box要素に入れる
  }

  function startCooking(){
    stopWatch = new Date();
    document.getElementById('start-button').style.visibility = 'hidden';
    const timeInterval = 1000;//1秒間隔で実行
    let i = 0; 
    const limit = orders.length;
    waitingClient = limit;
    const timer = setInterval (() =>{
      const order = orders[i];//1つの注文を取得
      const [cookingTime, material] = order;//データの中身
      const data = { i, cookingTime, material};//サーバ側に渡す値
      console.log(data)
      //GASにデータを渡し、更新スタート
      google.script.run.withSuccessHandler(setState).withFailureHandler(missFunc).cooking(JSON.stringify(data));
      i++;
      if(i===limit) clearInterval(timer);//処理終了
    },timeInterval);
  }

  function setState(text){
    const p = document.createElement('p');//テキストで表示する
    p.textContent = text;
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
    waitingClient--;//調理数を一つ減らす
    if (waitingClient===0){
      const time = new Date()- stopWatch;
      return setState(time +"㎜秒");
    } 
  }

  function missFunc(){
    const p = document.createElement('p');//完成した料理をテキストで表示する
    p.textContent = 'error';
    p.style.color = 'red';
    const box = document.getElementById('box');
    box.appendChild( p );//Box要素に入れる
  }
</script>

全体の流れ

前提として、onOpenはスプレッドシート起動時に実行するトリガーが設置されています。

  1. スプレッドシート上部ツールバーからサイドバーを表示する
  2. サイドバー(index.html)が表示され、クライアント側からGAS側のgetOrders関数が呼び出される
  3. クライアント側は、戻り値である2次元配列(オーダー一覧)を受け取る。
  4. クライアント側から配列を一つずつ取り出し、1秒ごとにGAS側へ送る(cooking関数)
  5. GAS側は受け取ったデータを処理する(cooking関数)

GASの関数実行は4回に分かれています。getOrders()とオーダーの数だけの(今回は3つ)cooking()となります。

そして肝心なことは、関数を同時に実行している点にあります。つまりcooking関数を直列ではなく、並列に処理をしていることになります。

GASのみに注目するとこうなります。

  • getOrders()はクライアント側にオーダー一覧を返して終了
  • cooking()は一つのオーダーを調理して終了
  • cooking()は1秒毎にオーダーが飛ばされる

応用するときのイメージは以下です。

  1. GASから処理全体のデータをクライアント側に投げる
  2. クライアント側から処理の一つずつをGASに投げる
f:id:tabemi:20210525214648p:plain:w500
全処理が終了。1分4秒ほどで終了しています。

google.script.runが非同期に処理を実行

クライアント側からGAS側のコードを呼び出すには、 google.script.run を使います。Webアプリを作るときによく使用する関数ですね。

使い方

google.script.run.withSuccessHandler( 正常に動いたときに実行する関数 ).withFailureHandler(エラーがおこったときに実行する関数 ).GAS側の関数(引数);

注意点

いくつか実装に当たって注意があります。

とはいっても実行時間制限

並列処理をしていても、一つの処理を実行するのに6分以上かかってはいけません
今回の例では、cooking関数(ひとつの調理時間)が6分を超えた時点で「実行時間が長すぎます」と例のエラーが発生してしまいます。
一つの関数に命を捧げましょう。

実行数制限

いくらでもGASを呼び出して時間を短縮してやる。と思いがちですが(私のこと)、GASには同時実行数にも制限がかかっており、無料・有料アカウントともに同時実行可能数が30までとなっています。
そのため、クライアント側で30を超えないように制御してください!

サーバーに負荷

timeIntervalは余裕をもって設定してください!

今回のような処理であれば、あまり気にすることはないですが、スプレッドシートを新しく作成したり、シートを挿入したりというような処理は、同時にできないことがあります。

といっても経験則でしかないので、、具体的にどれということは難しいですが、
サーバーに負荷が大きそうな処理同時実行の間隔を空けてみてください
明確な根拠をもって回答ができなくて、申し訳ありません。。

小話

htmlファイルにタグとタグを書いておりません。これはslideshareに記載されていたのを参考にしています。
現在、絶賛捜索中なのですが、見つかりません💦
Google勤務の方が書いていたように思います。。

信憑性のかけらもないお話でした、、、(かならず見つけ出す!!)

最後に

なんだか、歯切れの悪い最後になってしまったのですが、今回は以上となります。

クライアント側からサーバ側の処理を管理できるため、処理過程を記述することもできますよ

不明点ありましたら、コメントください!

今回は、GASの実行時間制限を回避する方法をお伝えしました。

ではでは~