GASで処理を一時停止・待機(sleep)させる方法

GAS

※本記事はプロモーションを含みます

目次

Google Apps Script(GAS)を使ってスプレッドシートの業務自動化を進めていくと、コード自体は間違っていないはずなのに、なぜか途中で処理が落ちたり、一部のデータだけが欠落したりする不可解な現象に遭遇することがあります。なのでログを追いかけてみると、スクリプトが速く動きすぎていることが原因だと気づかされます。ただしこのような場合、GASの処理をあえて一時停止させることで、相手側のシステムやスプレッドシート自体の計算が追いつくのを待つ必要が出てきます。今回は、スクリプトの実行を特定の時間だけ待機させる手段について、具体的な書き方と注意すべき周辺事情をまとめていきます。

連続で処理を走らせたときに起きるタイミングのズレとエラー

あなたがスプレッドシートの行データを読み取って、1行ずつ外部のシステムに送信したり、Googleドライブ上に大量のPDFファイルを連続で生成したりするようなスクリプトを書いたとします。なので単体テストとして1行だけ、あるいは1つのファイルだけを処理したときには全く問題なく動いていたにもかかわらず、いざ本番環境で数十件のデータを一気にループ処理で回してみると、突如として真っ赤なエラーメッセージが画面を埋め尽くすことがあります。

このとき起きている現象の多くは、GAS側の処理速度と、リクエストを受け取る側のシステムが許容できる処理ペースとの間に生じるタイミングのズレです。というのはGASはクラウド上のサーバーで実行されており、ループ処理などは非常に高速に回るからです。ただし外部のWebサービスやアプリケーション・プログラミング・インターフェース(API)には、サーバーへの過剰な負荷を防ぐための制限が設けられているのが一般的です。なので短時間の間に立て続けにアクセスを行うと、相手側のシステムは攻撃を受けたと判断したり、あるいは単純にリクエストをさばききれなくなったりして、アクセスを遮断します。

これが有名な「429 Too Many Requests」というエラーです。というのはエラーメッセージの通り、リクエストが多すぎるという警告だからです。なのでGASの実行ログにこのエラーが残っていた場合、それはスクリプトの論理的なバグではなく、単にアクセスするペースが速すぎるという物理的な問題を示しています。

また外部APIを利用しない場合であっても、スプレッドシート内の複雑な計算式(QUERY関数やIMPORTRANGE関数など)に依存する処理を行っていると、不具合が起きます。というのはGASがセルに値を書き込んだ直後に別のセルからその計算結果を読み取ろうとしたとき、スプレッドシート側の再計算がまだ終わっておらず、古い値やエラー値を取得してしまうといったズレも発生するからです。なのでこれもやはり、スクリプトの進行スピードに対して、スプレッドシートの裏側の処理が追いついていないことが原因です。

このようなスピードの不一致によるエラーやデータの欠落を防ぐためには、スクリプトの進行を意図的に遅らせて、相手の準備が整うのを待つというアプローチが必要になります。

Utilities.sleepを使ってスクリプトの進行を待機させる

前述したような、速すぎることによって生じるタイミングのズレを解消するために、GASには処理を一時停止させる手段が用意されています。なぜならループ処理の中で外部APIを連続して叩く直前や、重い計算式が完了するのを待ちたいタイミングなど、どうしても処理と処理の間に時間的なクッションを挟みたいからです。

ここで注意すべきずれる点として、待機させる時間を大雑把に長く設定しすぎると、今度はスクリプト全体の実行時間が間延びしてしまい、GAS特有の実行時間制限に引っかかってしまうリスクが高まることが挙げられます。ただしGASは無限に動き続けることはできません。なので処理を待機させる設定は、エラーが出ないギリギリの短い時間を狙いつつ、確実性を担保できる秒数を見極める必要があります。

この問題を回避し、意図した通りの時間だけ処理をストップさせるためには、Utilities.sleepというツール(メソッド)を使います。というのはこのメソッドは、指定した時間だけスクリプトの実行を完全に停止し、時間が経過した後に自動的に次の行から処理を再開してくれるからです。

Logger.log("処理開始");
Utilities.sleep(3000); // 3秒待機
Logger.log("3秒経過後に再開");

上記のコードでは、最初のログを出力した後、スクリプトが一旦動きを止めます。カッコの中に指定している数字はミリ秒(1000分の1秒)単位です。なので1秒待機させたい場合は「1000」を渡し、3秒待機させたい場合は「3000」を渡します。ただし秒数で直接「3」と書いてしまうと、わずか3ミリ秒しか待機せず、ほぼ一瞬で通り過ぎてしまうため、単位の勘違いには気をつけてください。

このUtilities.sleepを、たとえばforループやforEachメソッドを使った繰り返し処理のブロックの中に組み込むことで、1件処理するごとに数秒のインターバルを強制的に設けることができます。なので外部システムへのリクエストが一定の間隔を空けて送信されるようになり、APIのレートリミット回避を図りながら、安全に全件の処理を完走させることが可能になります。

これは簡単な1行を追加するだけの記述ですが、スクリプトの安定性を劇的に向上させる力を持っています。ただしループの中に書くということは、その待機時間がループの回数分だけ掛け算になって跳ね返ってくるという事実を忘れてはいけません。

APIの利用制限回避以外で一時停止が必要になる場面

GASの処理にsleepを挟む目的として一番多いのは、やはり外部APIの利用制限を回避することです。ただしスプレッドシート業務に密着したスクリプトを書いていると、それ以外の場面でも待機時間が欲しくなる瞬間に直面します。

よくあるのが、Googleドライブ上でファイルをコピーして、それをすぐにPDF化してメールで送信するような一連の自動化フローです。というのはGASのDriveAppなどを使ってテンプレートファイルを複製した場合、プログラム上はファイルの作成命令を出してすぐに次の行に進むからです。なのでGoogleドライブの裏側では、新しいファイルのインデックスを作成したり、実データをストレージに完全に書き込んだりするのに、わずかながらタイムラグが発生することがあります。

その結果、ファイルを作成した直後の行でそのファイルをPDFとしてエクスポートしようとすると、「ファイルが見つかりません」といったエラーが出たり、あるいはファイルの中身がまだ空っぽの状態でPDF化されてしまったりする現象が起こり得ます。なのでこのようなクラウドの非同期的な振る舞いによるズレを吸収するためにも、ファイル作成の直後に数秒間のsleepを入れて、ドライブ側の処理が完全に終わるのを待つという対応が非常に有効です。

またスプレッドシートの操作においても、GASからシートに大量の関数を書き込み、その計算結果の値だけを別のシートに転記したいというケースがあります。というのは書き込んだ関数が複雑で、何万行ものデータを参照している場合、シートのセルの値が「Loading…」から本来の計算結果に変わるまでに数秒の時間を要するからです。なのでもしGASがその計算完了を待たずに値を取得してしまうと、転記先には「Loading…」という文字列やエラーがそのまま書き込まれてしまいます。

このような場合も、関数を書き込んだ後にSpreadsheetApp.flush()を呼んで変更をシートに適用させた上で、念のためにUtilities.sleepで数秒の待機時間を設けることで、シートの再計算が完了するのをじっくりと待つことができます。なのでsleepは外部システムへの気遣いだけでなく、Google自身のサービス群が内部で処理を終えるのを待ってあげるための「ゆとり」としても機能するわけです。

待機時間の蓄積によるGASの実行時間上限にどう立ち向かうか

スクリプトに一時停止の処理を組み込むと、たしかにタイミングのズレやAPI制限によるエラーは減り、動作は安定します。ただしここで絶対に直面する別の大きな問題があります。それが、GASの実行時間の上限です。というのはGASは1回のスクリプト実行につき、原則として最大6分間(無料のGoogleアカウントの場合)しか動かすことができないからです。なので6分を超えると、どれだけ重要な処理の途中であっても容赦なく強制終了させられてしまいます。

たとえば、100件のデータを外部APIに送信する処理があり、レートリミットを回避するために1件ごとに3秒のsleepを入れたとします。なのでこの場合、3秒 × 100件 = 300秒となり、待機している時間だけで5分を消費してしまいます。ただし実際の通信処理やスプレッドシートの読み書きにかかる時間を足し合わせると、6分の壁をあっさりと越えてしまう可能性が非常に高いです。

つまり、処理を安定させるために待機時間を増やすと、今度はタイムアウトによる強制終了のリスクが高まるという、ジレンマに陥るのです。なのでこの問題にどう立ち向かうべきかは、GASを書く上で常に頭を悩ませるテーマです。

一つの対応策としては、待機時間を極力短く削り込むことです。というのは本当に3秒待つ必要があるのか、1.5秒(1500ミリ秒)ではダメなのかをテストし、安全に通信できる最小のインターバルを探り当てるアプローチだからです。ただしこれには限界があり、API側の機嫌やネットワークの混雑具合によっては、昨日まで1.5秒で動いていたものが今日はエラーになる、という不安定さを抱えることになります。

より根本的な解決を目指すのであれば、6分以内に終わる分量だけを処理して一旦スクリプトを終了させ、残りの処理は数分後にトリガーを使って再起動させるという、いわゆるバッチ処理の分割実行の仕組みを作る必要があります。なのでスクリプトのプロパティストアやスプレッドシートの端っこに「どこまで処理が終わったか」の進捗行数をメモしておき、次回起動時はその続きから再開するというロジックを組み込みましょう。

待機時間を意図的にコントロールするということは、すなわち全体の実行時間の管理もセットで行わなければならないということです。というのはsleepは魔法の杖ではなく、時間を消費するというコストを支払って安定性を買っているからであり、その認識を持つことが大切です。

適切なミリ秒数の見極めとエラーハンドリングの組み合わせ

ループのたびに必ず一定の時間を待機させるという書き方は、実装がシンプルでわかりやすい反面、常に時間を消費し続けるというデメリットがあります。ただし外部APIのレートリミットが「1分間に60回まで」というような明確な基準を公開しているのであれば、1秒に1回なら絶対に制限に引っかからないという計算が立ちます。なので1000ミリ秒のsleepを固定で入れておくのは理にかなっています。

ただしAPIによっては「短時間に集中してリクエストが来た場合のみ制限する」といった、少し曖昧なブラックボックスの仕様になっていることもあります。なのでこの場合、毎回毎回sleepで待たせるのは実行時間の無駄遣いになるかもしれません。

そこで、実務的な工夫としてよく用いられるのが、エラー処理(エラーハンドリング)と一時停止を組み合わせる手法です。というのは通常時は一切の待機時間を設けずに全力のスピードで処理を進め、もし「429 Too Many Requests」などのエラーが返ってきたときだけ、try-catch文でそのエラーをキャッチするからです。なのでそこで初めて長めの待機時間(たとえば10秒=10000ミリ秒など)を挟んでから、同じリクエストを再試行するという書き方を採用しましょう。

このアプローチであれば、API側が許容してくれるうちは最速で処理が進み、制限に引っかかって怒られたときだけおとなしく待機する、というメリハリの効いた動作を実現できます。なので無駄な待機時間を削減できるため、GASの6分制限という壁に対しても強い耐性を持つことができます。

また、再試行するたびに待機時間を長くしていく「エクスポネンシャル・バックオフ」と呼ばれるアルゴリズムを取り入れることもあります。というのは最初は2秒待ち、それでもダメなら次は4秒、その次は8秒というように、サーバーへの負担を加速度的に減らしていくという考え方だからです。

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