2通届いた。
添削フォームを送信したユーザーに、同じ添削レポートが2通届いた。1通は想定内、2通は不具合だ。Queueシートを見ると、同じevent_idの行が2件存在していた。2行ともDONEになっていた。
原因を調べると、独立した2つの問題が重なっていた。
Queueに同じエントリが2件入る理由
同じevent_idでQueueシートに行が2件入るには2つのシナリオがある。
1つ目:doPostが2回呼ばれた(TypeFormのリトライ)。2つ目:processQueueが同じPENDING行を2回処理した。
どちらが起きているかをGASの実行ログで確認した。両方が起きていた。
原因1:TypeFormのWebhookリトライ
TypeFormはWebhookに対して30秒以内にHTTP 200が返らないと、同じWebhookをリトライする。
Flex版のdoPostはQueueシートに書き込んでから200を返す。通常は1秒以内に終わる。しかし「Queueシートへの書き込みが完了する前に一瞬の遅延が発生した」というケースで、TypeFormが30秒待ってリトライすることがあった。
event_idでの重複チェックを実装した。
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
const eventId = payload.event_id;
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Queue');
const rows = sheet.getDataRange().getValues();
const isDuplicate = rows.slice(1).some(row => row[3] === eventId);
if (isDuplicate) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'duplicate' })
).setMimeType(ContentService.MimeType.JSON);
}
sheet.appendRow([new Date(), 'PENDING', JSON.stringify(payload), eventId]);
return ContentService.createTextOutput(
JSON.stringify({ status: 'queued' })
).setMimeType(ContentService.MimeType.JSON);
}
この修正でdoPostの重複問題は解決した。しかし重複メールは依然として発生した。
原因2:processQueueの同時実行
キュー方式では1分ごとにprocessQueueが起動する。Gemini処理に2〜3分かかる場合、1回目の実行が終わっていない状態で2回目のトリガーが起動する。
1回目:行1をPENDING→PROCESSINGに変更。Gemini呼び出し中。2回目(1分後起動):行1を見る。PROCESSINGになっている。
「PROCESSINGはスキップする」実装があってもレースコンディションが残る。processQueueが行ステータスをPROCESSINGに更新する前に2回目が起動すると、両方がPENDINGを掴む。GASのスプレッドシート書き込みには数十ミリ秒かかる。
解決策はLockServiceだ。
function processQueue() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(0)) {
return; // 別のインスタンスが実行中
}
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Queue');
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][1] !== 'PENDING') continue;
sheet.getRange(i + 1, 2).setValue('PROCESSING');
SpreadsheetApp.flush();
const payload = JSON.parse(rows[i][2]);
try {
generateAndSaveReport(payload);
sheet.getRange(i + 1, 2).setValue('DONE');
} catch (err) {
sheet.getRange(i + 1, 2).setValue('ERROR: ' + err.message);
}
break;
}
} finally {
lock.releaseLock();
}
}
tryLock(0)でロックが取れなければ即座にreturnする。同時実行は起きない。
2つを同時に修正する必要があった
原因1と原因2はどちらか一方だけを修正しても、完全な解決にはならない。
原因1のみ修正:event_idの重複チェックでQueueへの二重登録は防げる。しかし、processQueueの同時実行問題は残る。1件のPENDINGを2つのprocessQueueインスタンスが同時に処理すれば、やはり2通送信される。
原因2のみ修正:LockServiceで同時実行を防げる。しかし、Queueに同じペイロードが2件入っていれば、別の実行でそれぞれ処理されて2通送信される。
2つの原因が異なるタイミングで独立して発生するため、両方を修正して初めて「どのケースでも重複しない」状態になる。
修正後に届いたエラーメール
修正後、GASから10件のエラーメールが届いた日があった。エラー内容はprocessQueue → Error code INTERNAL。
Googleのサービス内部エラーだ。コードのバグではない。
Queueシートを確認すると、PENDINGの行が1件もなかった。最後に添削リクエストが来たのは修正から数日前だった。つまりユーザーへの影響はゼロだった。
GASのエラーメールを受け取ったとき「Queueシートを見てPENDING行がなければ実害なし」というのが判断基準になった。エラーの深刻さはユーザーへの影響で測る。
*シンプルシステム株式会社 代表 伊藤勝彦*