GASは30秒でタイムアウトする。TypeFormは30秒以内にHTTP 200が返ってこないとWebhookをリトライする。Geminiは事業計画書の添削に1〜3分かかることがある。
この3つが組み合わさると、どうなるか。
Webhookを受け取ってGeminiを呼んで処理が終わるまで待つ。30秒超えるとGASが強制終了する。TypeFormはタイムアウトだと判断してリトライする。2回目の処理が間に合えば、ユーザーに同じレポートが2通届く。
キュー方式の設計
WebhookのReceiveとGeminiの処理を分離する。
Webhookを受け取ったら → QueueシートにPENDING行を追加して → 即座に200を返す。
1分ごとに起動するトリガー関数が → QueueシートのPENDING行を取り出して → Gemini処理を実行する。
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
const eventId = payload.event_id;
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Queue');
sheet.appendRow([
new Date(),
'PENDING',
JSON.stringify(payload),
eventId
]);
return ContentService.createTextOutput(
JSON.stringify({ status: 'queued' })
).setMimeType(ContentService.MimeType.JSON);
}
TypeFormへの応答は1秒以内に返る。TypeFormはリトライしない。Gemini処理は1分以内に別のトリガーが開始する。
processQueueの実装
1分ごとに起動するトリガーから呼ばれるprocessQueue関数はQueueシートのデータを処理する。
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; // 1回の実行で1件だけ処理
}
} finally {
lock.releaseLock();
}
}
tryLock(0)は「ロックが取れなければ即座にfalseを返す」。別のインスタンスが実行中であれば即座に終了する。
SpreadsheetApp.flush()でステータス変更を即座に確定してから処理に入る。これをしないとステータス変更がバッファに溜まったまま次の処理が始まることがある。
1回の実行で1件だけ処理する理由は、Gemini処理に数分かかることがあるためだ。2件目を始めるとGASの実行時間上限(6分)に引っかかる可能性がある。
1分トリガーの設定
function setupTrigger() {
ScriptApp.newTrigger('processQueue')
.timeBased()
.everyMinutes(1)
.create();
}
このコードを1回だけ実行すると、以降は1分ごとにprocessQueueが自動起動する。GASのダッシュボード(左メニュー「トリガー」)でトリガーの一覧と最終実行ログを確認できる。
Queueシートの構造
| A列 | B列 | C列 | D列 ||---|---|---|---|| 受信日時 | ステータス | ペイロード(JSON) | event_id |
ステータスは PENDING・PROCESSING・DONE・DUPLICATE・ERROR の5種類だ。
Queueシートはスプレッドシートのタブとして存在するため、Googleドライブから開いて手動で確認できる。エラーが起きた行のペイロードを見れば、どのユーザーの添削が失敗したか分かる。
デメリットを受け入れる
キュー方式には遅延が生じる。フォームを送信してから添削レポートが届くまで5〜10分かかることもある。
TypeFormの送信完了画面に「添削レポートは数分以内にメールでお送りします」というメッセージを設定した。遅延を設計の一部として受け入れて、ユーザーに伝える。
遅延と引き換えに得られる安定性の方が価値がある。
*次回は「重複メールが送られた原因を2段階で特定した話」*
*シンプルシステム株式会社 代表 伊藤勝彦*