0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GAS】LockServiceの順番待ち エラー・タイムアウト・鍵の返し忘れで暴かれた鍵取りゲームの実態

0
Last updated at Posted at 2026-04-06

前提

この記事は、Google Workspace Enterpriseを利用し、
2026年3月現在のAppsScriptを使っています。
※公式仕様の解説ではなく、実験ベースの挙動検証です。


前回の記事
LockServiceの待機限界は30秒じゃない?AIの回答を疑って1800秒実行の壁に挑戦
で、「waitLock()の最大は 30秒(30000ミリ秒)」と主張するAIに疑問を持ち、トリガー実行 + LockServiceの限界値にチャレンジしました。
そこで生じた素朴な疑問「鍵を返さないままエラーやタイムアウトしたら?」に、今回迫ってみます。

鍵を持ったまま爆死したら?

またもやAIに聞いてみた。
「releaseLockにたどり着く前にエラーになったらLockはどうなる?」

chatGPT:👉 ロックは「自動的にはすぐ解放されない」
ロックは 最大30秒程度で自動解放される
それまでは他の処理は waitLock() で待たされる or タイムアウト
Gemini:鍵が解放されないため、後続の走者が全員タイムアウトで脱落する

鍵を持ったままタイムアウト

前回の記事ではインストールトリガーでも図形バインド(図形へスクリプト割り当して実行)でも実行時間制限30分って結論になったけど、
さすがに30分待つのは人生の無駄遣いなので、30秒のシンプルトリガーで実験。
sleep時間を31秒にしているので、途中で実行時間アウトになるはず。
これを10秒間隔で第3走者まで実行。

function onEdit(e){
  var time = new Date();
  var limitTime = new Date(time.getTime() + 300000);
  Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
  Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )

  var lock = LockService.getScriptLock();
  lock.waitLock(30000);   //30秒待ち
    Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

  Utilities.sleep(31000);  //31秒スリープ ここで30秒制限でエラーになるはず
    Logger.log("31秒経過"+ Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
  lock.releaseLock(); 
}

第1走者が鍵を持ったままタイムアウト。
その1秒後に第2走者に鍵が渡されるも、第1走者が使った「30秒」と鍵回収のラグ1秒」ですでに瀕死状態。10秒後にタイムアウト。
そして第3走者、前走者のエラー後、計算上は10秒余裕があるにもかかわらず、鍵を拝むこともなくタイムアウト。

タイムアウトの場合の鍵リレーは少し時間がかかるみたい。
しかも走者がたくさんいると遅れが雪だるま式に膨れ上がって、本来セーフのはずの走者まで巻き添え死する可能性がある。

エラー死の場合は?

次はエラーの場合。
さっきと同じくシンプルトリガーを使い、sleepを29秒にしてタイムアウトは回避しつつ、タイプミスエラーを起こすように設定。
これを10秒間隔で3回実行。

function onEdit(e){
  var time = new Date();
  var limitTime = new Date(time.getTime() + 30000);
  Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
  Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )

  var lock = LockService.getScriptLock();
  lock.waitLock(30000);   //30秒待ち
    Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

  Utilities.sleep(29000);  //29秒スリープ
    Logger.log("29秒経過"+ Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
    logger.log("エラーおこす");   //★ここで強制的にエラー
  lock.releaseLock(); 
}

第1走者がエラーになった瞬間(15:55:43)、まだ待機リミット(15:55:55)まで10秒以上も余裕がある第2走者に鍵が渡される……と思いきや!
なんと鍵は、第1走者が死んだ1秒後に、スタートしたばかりの「第3走者」へ。
待っていた第2走者は完全に無視され、鍵を手にすることなくタイムアウト。(起動時間30秒の最大値超え)
横取りした第3走者も、鍵を持ったまま30秒の寿命を使い切り、タイムアウト。

インストールトリガーでもやってみる

15秒経過後にわざとタイプミスでエラーになるようにし、インストールトリガーを使い10秒間隔で3回実行。

function time_test(){
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  const value = sheet.getActiveCell().getValue(); //アクティブセルの値を取得
  if(value != ""){    //アクティブセルの値が空欄じゃなければ以下を実行
    Logger.log("開始したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
    var lock = LockService.getScriptLock();
    lock.waitLock(30000);      //★30秒待ち
      Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

    Utilities.sleep(15000);
      Logger.log("15秒経過:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
      logger.log("エラーおこす");   //★ここで強制的にエラー
      
    lock.releaseLock();
    Logger.log("ロック解除したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
  } 
}

どの回でも前の走者がエラーになった瞬間に鍵を受け取って走り出しています。
久々にとてもきれいなリレー。
「シンプルトリガー」と「インストール型トリガー」、公式にはロックの精度についての言及はないけど、今回の実験を見る限り、短命なシンプルトリガーは鍵の回収や後続への通知が間に合わず、システム内部でのパニック(同期のズレ)が起きやすいように見える。
確実なロックリレーを求めるなら、余裕のある「インストール型」を使うほうが無難かもしれない。

ここまでの結論

鍵を持ったままタイムアウト死/エラー死しても鍵はシステムが自動回収する。
ただし、回収スピードは実行環境によって変わる。
へたすると回収時間が積み重なって、本来間に合っているはずの走者までがドミノ倒死する可能性あり。
また、処理の順番も、先入れ先出しが保証されるわけではない。

waitLock待機時間よりも前の走者の処理が長かったら

waitLock待ち時間10秒だけど、処理時間15秒に設定。
第1走者が鍵を返すころには第2走者はwaitLockのタイムアウト予定。
図形バインドにして数秒おきに複数回実行すると、鍵はどこへ行く?

function time_test(){
    var time = new Date();
    var limitTime = new Date(time.getTime() + 10000);
    Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
    Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )
    var lock = LockService.getScriptLock();
    lock.waitLock(10000);      //★10秒待ち
      Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

    Utilities.sleep(15000);
      Logger.log("15秒経過:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
      
    lock.releaseLock();
    Logger.log("ロック解除したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
}

第1走者が鍵を返すのは49:09、第2走者の待機リミットは49:07。わずか2秒を待ちきれずタイムアウト。
第1走者の鍵は第3走者(待機リミット49:12)がキャッチし処理が走ります。
しかしロック解除は49:24なので、第4走者の待機リミット49:15には間に合わず、第4走者脱落。
予想どおりの挙動でした。

時間を長くする。実行時間=待機時間=5分

実行時間、待ち時間ともに5分間にした以下のコードを約5秒おきに実行。
今回もさっきと同じく順当に鍵のリレーが行われると思いきや・・・・

function time_test(){
    var time = new Date();
    var limitTime = new Date(time.getTime() + 300000);
    Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
    Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )
    var lock = LockService.getScriptLock();
    lock.waitLock(5*60*1000);      //★5分待ち
      Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

    Utilities.sleep(5*60*1000);      //★sleep 5分
      Logger.log("5分経過:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
      
    lock.releaseLock();
    Logger.log("ロック解除したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
}

第1走者のロック解除10:30:36、
第2走者の待機リミット10:30:41なので、鍵は第2走者に渡せるはずなのに
またもや第3走者が横取りしました。

さっきの入れ替わりは、無理にエラーを起こすなどしたからだと思っていたけど、今回は待ち時間が長いだけで特にエラーとかはなく、順当にロック解除している。それにもかかわらず横取りが発生するってことは、LockServiceの順番待ちは、やっぱり「先入れ先出し」は保証されないってこと?
この機能って、フォーム送信とかが同時多発的に実行されたときに順番整理してくれるものだと思っていたけど、その前提が崩れてしまいました。

ここまでの結論

ロック解除されたときに待ってた走者に鍵が渡されるけど、必ずしも順番どおりとは限らない。
ログから推察するに、waitLock は「列に並んで順番を待つ」仕組みではなく、一定の間隔で鍵を確認しに行くポーリング方式に近い挙動をみせている。
たまたま確認しに行ったタイミングが合ったのが第3走者だったということで。
待機時間が長くなるほど、このチェックタイミングのズレが致命傷になり、順番が入れ替わるリスクが高くなりそう。
このLockService、順番どおりに並べるというよりも、前の処理に後ろの処理が追い付くことによる情報の上書きを制御するためと考えるべきかな。

鍵を返し忘れたら?

releaseLock();の書き忘れ、やりませんか?私はしょっちゅうです、はい。
ということでこれも実験してみましょう。

まずはシンプルトリガーで。

function onEdit(e){
  Logger.log("開始したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

  var lock = LockService.getScriptLock();
  lock.waitLock(30000);   //30秒待ち
    Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

  Utilities.sleep(5000);  //5秒スリープ
    Logger.log("処理終了"+ Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
  //lock.releaseLock(); //★releaseLock()をコメントアウト
}

またもや順番の入れ替わりが起きましたが、鍵は順当に返されているみたい。
しかし順番がカオスすぎてわけわかりません(困惑)
画像を見ると

  1. 第1走者が終了後、正常に第2走者へバトンタッチ。
  2. 第2走者が終了後、次に鍵をゲットしたのは第3でも第4でもなく、最後尾にいた第5走者。
  3. 第5走者が終わって、その1秒後にようやく第3走者が鍵を手にし、
  4. 第3走者のあと、最後に残された第4走者へ鍵が渡される

もはや、列に並ぶ意味がまったくない、無法状態です。

時間を長くする。実行時間=待機時間=5分

図形バインドで以下のコードを1秒間隔で5回実施。

function time_test(){
    var time = new Date();
    var limitTime = new Date(time.getTime() + 300000);
    Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
    Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )
    var lock = LockService.getScriptLock();
    lock.waitLock(5*60*1000);      //★5分待ち
      Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

    Utilities.sleep(5*60*1000);   //★sleep 5分
      Logger.log("5分経過:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
      
    //lock.releaseLock();   //★releaseLock()をコメントアウト
    Logger.log("処理終了:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
}

またしても横取り事件が発生。
第1走者が終了後、待機リストに並んでいた第2走者・第3走者を飛び越えて、はるか後方の第4走者が鍵をゲット。
第4走者が5分間の処理をしている間に、抜かされた第2走者と第3走者はあえなくwaitLockのタイムアウト。第1走者からすなおに鍵を渡されていれば処理ができたはずなのに、無念!

※第2走者以降、開始時間と「開始したよ」ログに1~3秒程度の乖離が生じています。処理が重なることでシステムが重くなっていたためと思われます。この目に見えない遅延もまた、後続の寿命を削る雪だるまの種になっていそうです。

releaseLock()を入れた結果もやはり追い越し発生。
今回は第1走者から第3走者へ。

待機時間とsleep時間を短縮し、1分/45秒/30秒 と実験してみたが、結果はほぼ変わらず。
ときには1番走者を追い越して2番走者が最初に走り出すなんてことも。
(もう、なにがなんだか)

ここまでのまとめ

releaseLock()があってもなくても、鍵は遅滞なく回収される。
でもどっちにしろ順番は保証されない。
でもreleaseLock()があったほうが、鍵の回収が一瞬なので、少しはマシかも。

30秒ルール、復活?

ここで私は思い出しました。
AIが「releaseLock()は30秒!」と力説していたことを。
彼らが主張していたのは、こういうことも含めてなのではないか?

待機時間とsleep時間をともに余裕をもたせた25秒にし、releaseLock()もちゃんと入れて再度実行!
少しはお行儀よく順番待ちできるかな?

function time_test(){
    var time = new Date();
    var limitTime = new Date(time.getTime() + 25000);
    Logger.log("開始したよ:" + Utilities.formatDate(time,"JST","HH:mm:ss"));
    Logger.log("待機リミット:" +   Utilities.formatDate(limitTime,"JST","HH:mm:ss")  )
    var lock = LockService.getScriptLock();
    lock.waitLock(25000);     //★25秒待ち
      Logger.log("鍵をもらったよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));

    Utilities.sleep(25000);   //★sleep 25秒
      Logger.log("25秒経過:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
      
    lock.releaseLock();
    Logger.log("鍵を返したよ:" + Utilities.formatDate(new Date(),"JST","HH:mm:ss"));
}

結果は「30秒ルール、関係なし」。

4回中1回しか成功しないという、もはや成功が「たまたま」と思えるレベルの低確率。むしろ releaseLock() なしの方が好成績という謎の結果まで出てしまい、私の困惑は最高潮。

インストールトリガーを試してみる

ここで私はまた思い出しました。
さっき、インストールトリガーでエラー死の実験をしたとき、とてもきれいなリレーになったことを。
再び「スプレッドシートから」「編集時」にして、約1秒間隔で適当なセルに数字を入力してみる。
コードは先ほどと変えず、待機時間とsleep時間はともに25秒のままにしておこう。

まずはreleaseLock();なしで。

実行結果:4回中2回成功。
インストールトリガーだから良いってわけでもなさそう。
成功した場合でも、図のようにタイムラグが1秒~2秒程度発生することがあり、そういうことも、次の走者へのバトンタッチがうまくいかない原因の1つかもしれない。

※図は4番走者以降は省略しています。


次にreleaseLock();ありで行きましたが、順番成功率は、まさかの1/4。
中には実行開始後「開始したよ」ログまで数秒かかってそこで追い越しが発生しているものもあり、このためLockServiceにたどり着いたときには、すでに当初の実行順が崩壊していた可能性があるものも。

しかし、鍵の受け渡しのタイムロスはほぼゼロ。
これがreleaseLock();の効果でしょう。

※図は5番走者を省略しています。

どっちにしろ、インストールトリガーだからといって順番成功率が上がるわけではないみたい。
さっきのエラー死の実験も、たまたま成功しただけかも。

まとめ

  • システムの自動鍵開放は意外と優秀:タイムアウトでもエラーでもreleaseLock()忘れでも、システムが数秒以内に鍵を回収してくれる。後続が永久にストップすることはない。
  • 順番(FIFO)は保証されない:待機の順番は絶対ではなく、各走者がポーリング形式で鍵の確認をし、鍵が開いた瞬間にたまたまタイミングが合った走者が受け取る、運ゲー要素。
  • releaseLock() の有無による差:入れなければさらに順番が乱れるが、入れても保証はされない。ただ、鍵の回収のタイムラグはほぼなく、スムーズ。
    ※releaseLock() を書かない設計は推奨されません。意図しない遅延や競合の原因になるため、素直に明示的な開放をしましょう。

教訓:LockServiceは「整列係」ではなく、ただの「門番」

  • 順番に依存した設計をしない:「順番にやってくれるはず」と思い込んで処理するとデータが狂う
  • 待機時間は十分に:追い越しやラグを考慮し、waitLockの引数は少し余裕を持たせる

image.png

これらはGoogleが公開している公式仕様ではなく、あくまで2026年3月現在の実機検証に基づく観測事実と、そこから導き出した私なりの考察です。環境やアカウント種別によって挙動が変わる可能性はありますが、現場で起きている怪現象を読み解く一つの物差しになれば幸いです。


Googleフォームで同時に大量送信されても踏ん張る!LockServiceで順番制御!try - catch - finally でバトンを繋げ!
この記事で、「Googleフォームの送信が同時多発しても順番どおりに受付したい」というニーズから調べ始めた LockService 。
1人ずつ入れることはできるけど、順番までは守れないという残念な結果になってしまったけど、しかし現場では「送信された順」に処理したいケースが多々あるはず!
GoogleFormシリーズでは、このあと、この門番を手なずけつつ、可能な限り順番を維持する方法を考えてみたいと思います。
LockServiceでは順番は守れない?受付番号で順序を保証する方法

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?