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

開発記録 / AIFlex制作記 / Vol.15 AIFlex制作記 Vol.1

記事 02

スプレッドシートをキューとして使い、GASの30秒制限を回避する

GASは30秒でタイムアウトする。TypeFormは30秒以内にHTTP 200が返ってこないとWebhookをリトライする。Geminiは事業計画書の添削に1〜3分かかることがある。

2026-06-07 公開

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段階で特定した話」*

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