はじめに
GAS使いの皆さんこんにちは、よきGASライフをお過ごしでしょうか?
先日LockServiceを使った重複対策をしていたときにふと、疑問が浮かびました。
自分はずっと、GASの LockService を「キューみたいに順番待ちさせるやつ」だと思って使ってた。waitLock って書いてあるし、なんとなく「並んで待つ感じね」で読み流して、特に深く考えずに何個もスクリプトに突っ込んでた。
ある日ふと「これ、複数リクエストが来たとき内部的に何やってるんだろう?」が気になって調べてみたら、自分の理解はわりと違っていた。
たぶん同じ勘違いしてる人、がけっこういると思う。
「GAS キュー」とか「GAS 順番待ち」で検索してこの記事に辿り着いた人向けに、勘違いしていた自分の話としてまとめておきます。
LockServiceは「キュー」じゃなくて「鍵」
結論を一行で。
LockService はキュー(順番待ちの行列)ではなく、排他制御(鍵)です。
イメージで言うと、
- キュー → 順番に並んでて、全員いつかは処理される前提(行列)
- ロック → 一人ずつ通すための門番。先に取った人が解放するまで他は入れない
waitLock(ms) で「鍵が空くまで最大 ms ミリ秒待つ」ができるので、外から見ると順番待ちっぽく見える。これがたぶん誤解の入り口。
実際には「待ち列の順序」は保証されてないし、待ちきれなかった人は例外で落ちる。「並んでる」のではなくて「鍵の前で押し合いへし合いしてる」が正しい。
たぶん多くの人が勘違いしてる3つ
1. FIFO保証されてない
これが一番びっくりした。
「先に waitLock を呼んだ人が先に通れる」と思ってた。でも、公式ドキュメントを読んでも「先着順で渡す」とは書かれていない。Apps Script の実装としては、複数の実行が同時にロック取得を試みたとき、どの実行が次に取れるかは決まっていない。
なので、
- A が先に
waitLockを呼んだ - 直後に B が
waitLockを呼んだ - A が解放した瞬間、次に取るのは A?B? → 未定義
「キューだから A → B の順で動くはず」を前提に設計すると、ハマる。
2. waitLock(ms) の上限がそこそこ短い
待ち時間の上限はミリ秒で指定するけど、現実的に長く待たせるユースケースには向いてない。
- 並列実行が増えると、待たされる側は普通にタイムアウト例外で死ぬ
- リトライを書いていないと、その実行ぶんのタスクはただ消える
「大量タスクを順に捌く」みたいなのを LockService だけで実現しようとすると、ここで詰む。
3. 「順番待ちっぽく見える」だけのカラクリ
結局のところ、
- 鍵を取れた1つだけが動く
- 残りは鍵が空くのを待つ
- 空いたら誰か1つが取る(誰かは未定義)
これを外から見ると「ちゃんと順番に処理されてる気がする」となる。同時実行が少ない&処理が一瞬で終わる場合は、たいてい結果的に到着順っぽく動くので、ますます気付かない。
自分はここでずっと「キューだな〜」と思っていた。違いました。
じゃあ本物のキューが必要なときは?
正直に書くと、自分のユースケースでは本物のキューが必要な案件は今のところ無かった。同時実行を防いで「ちょっと順番待ち」したい程度なら、LockService で必要十分だった。
ただ、もし「順序を厳密に守って大量タスクを捌きたい」が出てきたら、選択肢としてはこのあたりになりそう。
-
スプレッドシート+定期トリガー+LockService …
待合室をスプシにして、トリガーで1件ずつ消化。LockService は「定期トリガーの重複実行防止」に使う -
GCP Cloud Tasks …
GASの6分上限を超えて本格的に順序・リトライをやるならこっち
要は、
- 同時実行を捌く番人が欲しい → LockService
- タスクを並べて順に処理したい → キュー(自作 or Cloud Tasks)
役割が違うので、片方で済むかは要件次第。
まとめ
- LockService は キュー(行列)ではなく排他制御(鍵)
- FIFO は保証されていない
- 「順番待ちっぽく見える」のは結果論で、設計の前提にしてはいけない
- 同時実行を防ぎたいだけなら LockService で十分。順序保証や大量処理が必要なら、別途キューの仕組みを足す
「キューだと思って使ってる」のと「鍵だと知って使ってる」のは、見た目同じコードでも事故ったときの怖さが全然違う。同じ勘違いをしていた人の参考になれば。