RENOY

GAS LockServiceで二重実行を防ぐ使い方|ステータスガードとflushを併用する理由

目次

「同じメールが2通届いた」「同じ請求データが2行できている」——GASで自動化を進めると、どこかで出会うのが二重実行の問題です。

私たちが関わった実案件(匿名)でも、AppSheetのBotから呼ばれるGASが、同じレコードに対して多重に発火しました。呼び出し元の設定を見直すだけでは防ぎきれず、最終的にはGAS側で守る構成にしています。

この記事では、そのGAS側の実装に絞って、LockService・ステータスガード・flushの3つをなぜ併用するのかを整理します。

この記事の結論

  • 二重実行は呼び出し元の設定だけでは防ぎきれない
  • LockServiceは同時実行を防ぐだけで2回目は止めない
  • ロック・ステータス確認・flushの3点で受け側を冪等にする

GASが二重実行される原因:実案件で起きたこと

実案件では、AppSheetのBotからGASを呼び出す構成でした。ところが、1回の操作で同じレコードに対してGASが何度も実行されていました。

調べてみると、原因はAppSheetのGrouped Actionでした。複数のアクションをまとめて実行する機能ですが、各サブアクションが別々のイベントとして扱われるため、それぞれがBotの発火条件に引っかかっていたのです。

最初はAppSheet側のBot条件を絞って対応しようとしました。ただ、条件をいくら工夫しても、イベントが複数発生する構造そのものは変わりません。そこで方針を切り替え、GAS側でも二重実行を防ぐことにしました。

なお、AppSheetやGASの挙動・仕様は変わることがあるため、最新の情報は必ず公式ドキュメントで確認してください。

GAS LockServiceの使い方

LockServiceは、GASで排他制御をかけるための仕組みです。ロックを取得した実行だけが処理を進め、他の実行はロックが解放されるまで待つか、諦めるかを選べます。

ロックにはスクリプト単位・ドキュメント単位・ユーザー単位の種類があります。Botのように複数の呼び出しが同じ処理に集まる場面では、スクリプト単位のロックが基本です。

実装の骨格は次のとおりです(関数名は説明用の仮のものです)。

function handleRecord(rowId) {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10000)) return; // ロックが取れなければ今回は見送る

  try {
    const row = findRow(rowId);
    if (row.status === '処理済み') return; // ステータスガード

    doWork(row);                     // 本来の処理
    setStatus(rowId, '処理済み');    // ステータスを更新
    SpreadsheetApp.flush();          // 書き込みを確定させる
  } finally {
    lock.releaseLock();              // 必ず解放する
  }
}

ポイントは、ロックの解放を finally に置くことです。途中でエラーが起きてもロックが残り続けないようにします。待ち時間の長さは、処理の重さに合わせて調整してください。

ステータスガードとflushを併用する理由

ここが本記事の核心です。LockServiceだけでは、二重実行は防げません。

LockServiceが防ぐのは「同時」だけ

LockServiceが止めるのは、2つの実行が同時に処理へ入ることです。1回目が終わってロックを解放したあとに2回目が届けば、2回目も普通にロックを取得して、同じ処理をもう一度実行してしまいます。

Grouped Actionのように、少しずつ時間をずらして複数のイベントが届く場面では、まさにこれが起きます。

ステータスガードで「2回目」を止める

そこで、ロックを取ったら最初にステータス列を確認し、処理済みならすぐ抜けるガードを置きます。これで、順番に届いた2回目以降の呼び出しは何もせずに終わります。

ロックとガードはセットで初めて意味を持ちます。ロックがなければ、2つの実行が同時にステータスを読んで「未処理」と判断し、両方とも処理してしまう可能性があります。

flushで書き込みを確定させる

最後のピースがflushです。スプレッドシートへの書き込みは、GASの内部でまとめて反映されることがあり、書いた直後に確定しているとは限りません。

ステータスを更新したのに確定する前にロックを解放すると、次の実行が古いステータスを読んでしまい、ガードがすり抜けられます。SpreadsheetApp.flush() で書き込みを確定させてからロックを解放することで、この隙間をふさぎます。

3つの役割をまとめると次のとおりです。

仕組み防ぐものこれだけだと残る穴
LockService同時に処理へ入ること順番に届いた2回目
ステータスガード処理済みの再実行同時に読んだ場合のすり抜け
flush古いステータスの読み取り単体では効果なし

処理全体の流れは次のようになります。

二重実行の対策:受け側だけで冪等にする

この案件から持ち帰った判断軸は一つです。呼び出し元が1回だけ呼ぶと信じないこと。

冪等とは、同じ処理を何回実行しても結果が1回分と変わらない性質のことです。呼び出し元の設定を工夫して「1回しか呼ばれないようにする」のは、条件の見落としや仕様の変化に弱い守り方です。今回のように、ツール側の仕組みで想定外のイベントが発生することもあります。

一方、受け側のGASが冪等になっていれば、何回呼ばれても結果は変わりません。呼び出し元がAppSheetでも、フォームでも、別のシステムでも、同じ考え方で守れます。

もちろん、呼び出し元の条件を整えることも無駄ではありません。余計な実行が減れば、処理の負荷も下がります。ただし、それは最終防衛線ではないという位置づけです。最後に守るのは受け側、という順番で設計します。

よくある質問

Q. GASの二重実行はなぜ起きるのですか? 呼び出し元が同じ処理を複数回送ることがあるためです。たとえばAppSheetのGrouped Actionでは、各サブアクションが別々のイベントとして扱われ、同じレコードに対してGASが多重に発火することがあります。

Q. LockServiceだけでGASの二重実行は防げますか? 防げません。LockServiceは同時に走ることを防ぐ仕組みで、順番に届いた2回目の呼び出しは止められません。ロック取得後にステータス列で処理済みかを確認し、処理済みなら抜けるガードを併用する必要があります。

Q. GASでSpreadsheetApp.flushはなぜ必要ですか? スプレッドシートへの書き込みが即座に確定するとは限らないためです。ステータスを更新したらflushで確定させてからロックを解放すると、次に来た実行が最新のステータスを読めるようになり、ガードが正しく働きます。

まとめ

  • AppSheetのGrouped Actionなど、呼び出し元の仕組みで二重実行は起こりうる
  • LockServiceは同時実行を防ぐが、順番に届いた2回目は止められない
  • ステータスガードで処理済みを抜け、flushで書き込みを確定させてからロックを解放する
  • 判断軸は、呼び出し元を信じず受け側だけで冪等にすること

RENOYでは、GASやAppSheetを使った業務自動化の設計・開発から、こうした「たまに二重に動く」「原因が分からない」といった不具合の切り分けまでお手伝いしています。「いまの自動化が安心して使える作りになっているか見てほしい」という段階のご相談も歓迎です。まずはお気軽に無料個別相談(オンライン・60分)をご利用ください。サービス内容をまとめた資料は資料請求からご覧いただけます。

#GAS#LockService#AppSheet#二重実行#業務自動化