対戦予定の再登録操作でLINE応答遅延が発生し、警告メッセージが重複表示された事象の原因切り分け
利用者がニャースとの対戦予定日を選択した際、1回目の操作後にBotからの応答が表示されなかった。応答がないと判断した利用者が日付選択を再度行った結果、以下の表示となった。
| 時刻 | 表示内容 | 送信元 |
|---|---|---|
| 20:43 | 日付を選択(利用者操作) | 利用者 |
| 20:44 | 【予定完了】相手: ニャース / 日程: 2026-08-02 | Bot |
| 20:44 | ニャース との試合が既に予定されています。先に結果報告を行ってください。 | Bot |
| 20:45 | ニャース との試合が既に予定されています。先に結果報告を行ってください。 | Bot |
利用者が日付選択を計2〜3回タップしたことで、同一操作のWebhookが複数回GASに到達。1回目が予定登録に成功した後、2回目以降は hasFIX チェックにより正しくブロックされた。警告が複数表示されたのは、操作回数分のWebhookそれぞれに対してBot が返信を行ったため。
LINE上の表示タイムスタンプとコードの処理フローから、以下の順序で処理が進行したと推定される。
params.date = "2026-08-02" が送信される。
doPost → doPostV2 → finalizeScheduleV2 が起動。LockService を取得し、registerSchedule を実行。matchesシートにFIXステータスで書き込み成功。
registerSchedule 内でmatchesシートを読み取り、hasFIX === true を検出。登録をブロックし、警告メッセージを返信。
hasFIX で検知。2つ目の警告メッセージが表示。
現行コードには2層の重複防止ロジックが実装されており、今回はこれが正しく機能した。
checkScheduleValidity()
matches.gs : 55-75行目
対戦相手を選択した時点で、matchesシートに当該ペアの FIX 行が存在するかを確認。存在すれば日付選択画面を表示せずに警告を返す。
registerSchedule() 内の hasFIX 判定
matches.gs : 101-103行目
LockService で排他制御を確保した上で、書き込み直前に再度 FIX 行の有無を確認。同時リクエストによるレースコンディションを防止。
LINE Messaging API の Reply API は、公式ドキュメントによりSLAが保証されていない。以下の状況で一時的な遅延が発生することが知られている。
今回はGAS側の処理自体は正常に完了しており(【予定完了】メッセージが最終的に配信されている)、遅延はLINEプラットフォーム〜クライアント間の配信遅延と判断される。
今後同様の遅延時のUX向上および安全性強化として、以下の対策が検討可能。
| 案 | 内容 | 効果 | 工数 | 優先度 |
|---|---|---|---|---|
| A |
Webhook冪等性(Idempotency)の導入 LINE Webhook の webhookEventId を CacheService に記録し、同一イベントの重複処理を防止する
|
高 | 小 | 推奨 |
| B |
重複警告メッセージの抑制 同一ユーザーへの同一内容の警告を短時間内に繰り返さないよう、 CacheService でスロットリングする
|
中 | 小 | 任意 |
| C |
日付選択後の処理中メッセージ Loading Animation(LINE Messaging API 対応)で処理中であることを即座にフィードバックする |
中 | 小 | 任意 |
CacheService を利用し、Webhookイベントごとに一意のIDを記録して重複処理を排除する。
| ステップ | 利用者の操作 | GAS内部の処理 | 該当ファイル |
|---|---|---|---|
| 1 | 「予定報告」とメッセージ送信 | モードを SCHED に設定対戦相手リストを Quick Reply で表示 |
main_v2.gs : 45-48 |
| 2 | 対戦相手を選択 | checkScheduleValidity() で事前チェック→ 通過した場合、日付選択UIを送信 |
main_v2.gs : 125-155 matches.gs : 55-75 |
| 3 | 日付を選択 | LockService で排他制御registerSchedule() でmatchesシートに書き込みsyncMatchesToStarChart() で星取表に同期 |
main_v2.gs : 160-184 matches.gs : 81-129 |
| 防御レイヤー | 実装箇所 | タイミング | 対象 |
|---|---|---|---|
| Layer 1 — 事前チェック | checkScheduleValidity()matches.gs : 55-75 |
対戦相手選択時 | 同一ペアの FIX 行の有無 |
| Layer 2 — 書き込み時再チェック | registerSchedule()matches.gs : 101-103 |
日付選択後・書き込み直前 | 同一ペアの FIX 行の有無(Lock下) |
| 排他制御 | LockService.getScriptLock()main_v2.gs : 162-163 |
日付選択後の処理全体 | 同時実行の直列化(30秒タイムアウト) |
| LINE replyToken | LINE プラットフォーム側 | 常時 | 同一トークンは1回のみ使用可 |
webhookEventId や x-line-request-id ヘッダを参照しておらず、リトライを新規リクエストと同等に処理する。今回は hasFIX チェックでデータ不整合を防げたが、結果報告など他のフローではこの保護がない。
| 用語 | 説明 |
|---|---|
FIX | matchesシートのステータス。対戦予定が確定済みであることを示す。 |
hasFIX | matchesシート内に当該チームペアの FIX 行が存在するかどうかのフラグ。 |
LockService | GAS の排他制御機構。スクリプトレベルのロックにより同時実行を直列化する。 |
CacheService | GAS のキー・バリューキャッシュ。ユーザーの操作状態(モード・選択相手等)を一時保持する。 |
webhookEventId | LINE Webhook イベントに付与される一意のID。冪等性制御に利用可能。 |
| 冪等性(Idempotency) | 同じ操作を複数回実行しても結果が変わらない性質。API設計における安全性の基本原則。 |