受講者からの連絡だった。「添削レポートが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添削エンジンの作り方」*
*シンプルシステム株式会社 代表 伊藤勝彦*