全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / AiReview制作記 / Vol.14 AiReview制作記 Vol.1

記事 03

TypeFormのWebhookリトライとGASの同時実行——重複メール問題の原因と解決

受講者からの連絡だった。「添削レポートが2通届きました」。

2026-06-07 公開

受講者からの連絡だった。「添削レポートが2通届きました」。

同じ内容のメールが2通。スプレッドシートのQueueシートには、同じevent_idが2行並んでいた。

システムが二重に送信した。理由を探った。


問題は2か所にあった

調べると、原因が2つ存在することが分かった。どちらか一方だけを直しても、もう一方が原因で再発する。両方を同時に解決する必要があった。


原因1:TypeFormがWebhookをリトライする

TypeFormはWebhookを送信した後、一定時間内に200レスポンスが返ってこないと「失敗」と判断して再送する。

AiReviewのdoPostはWebhookを受け取り、Geminiで処理してメールを送信してから200を返す。Geminiで長文を生成するには数十秒〜数分かかる。TypeFormはその間にタイムアウトと判断してリトライする。

結果として同じフォーム回答がGASに2回届く。2回届けば2通送られる。

解決策は「即座に200を返してから処理する」だ。TypeFormのevent_idを使って重複を検出する。

function doPost(e) {
  const payload = JSON.parse(e.postData.contents);
  const eventId = payload.event_id;

  const queue = ss.getSheetByName('Queue');
  const rows = queue.getDataRange().getValues();
  const isDuplicate = rows.some(r => r[3] === eventId);
  if (isDuplicate) {
    return ContentService.createTextOutput(
      JSON.stringify({ status: 'duplicate' })
    ).setMimeType(ContentService.MimeType.JSON);
  }

  queue.appendRow(['PENDING', new Date(), JSON.stringify(payload), eventId]);
  return ContentService.createTextOutput(
    JSON.stringify({ status: 'queued' })
  ).setMimeType(ContentService.MimeType.JSON);
}

event_idがQueueにあれば即座に返す。なければQueueに追加して返す。どちらも1秒以内に200が返る。TypeFormはリトライしない。


原因2:GASのトリガーが同時に動く

1分ごとの定期トリガーでprocessQueue関数が動く。Gemini処理に2分かかると、1分目のトリガーが終わる前に2分目のトリガーが発火する。

2つのGASインスタンスが同じQueueシートを見ている。両方が同じPENDING行を「未処理」と判断して処理を始める。2通送られる。

解決策はLockServiceだ。

function processQueue() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(0)) {
    return; // 別のインスタンスが実行中
  }
  try {
    // Queueシートを処理
  } finally {
    lock.releaseLock();
  }
}

tryLock(0)はロックを取得できなければ即座にfalseを返す。「既に誰かが動いている」と分かった瞬間に退出する。ロックを取ったインスタンスだけが処理を続ける。


なぜ両方必要か

event_idの重複検出だけ入れた場合を考える。Queueに同じevent_idが1件しか入らないようになった。しかし2つのprocessQueueインスタンスが同じ1件を同時に処理すれば、やはり2通送られる。

LockServiceだけ入れた場合を考える。processQueueの同時実行は防げた。しかしdoPostが同じevent_idのWebhookを2回受け取った場合、Queueに2行登録される。2行あれば、別々の実行で両方が処理されて2通送られる。

2つの原因が独立して発生するため、対策も両方必要だ。


Googleが落ちた朝

修正を入れてしばらく後、GASから10件のエラーメールが届いた。processQueue → Error code INTERNALというエラーだ。

コードに問題があるのかと思い、スクリプトエディタのログを確認した。同時刻にGoogleのスプレッドシートサービスで内部エラーが記録されていた。

Googleのサーバーが約10分間、スプレッドシートへのアクセスに失敗していた。コードのバグではない。1分トリガーが10回発火した分のエラーメールが届いただけだ。

Queueシートを確認した。PENDING行はなかった。最後のリクエストは3週間前だった。ユーザーへの影響はゼロだった。

GASのエラーメールが来たとき、最初にQueueシートを確認する。PENDINGがなければ実害なし。これが判断の基準だ。


*次回は「Flex版設計:講座に依存しない汎用AI添削エンジンの作り方」*

*シンプルシステム株式会社 代表 伊藤勝彦*