読むと配送済みになる read-and-deliver 型APIは消費者を1経路に限定し、UIの生存更新には非消費読み取りを分ける
設計判断
API設計
リアルタイム通信
知識
判断
イベント通知を「次の読み取りに未配送分を混ぜて返し、返した時点で配送済みにする」read-and-deliver 型のエンドポイント(GET がキューの ack を兼ねる設計)は、想定消費者以外が同じエンドポイントを叩くとイベントを食い潰す。
問題の型
同じ画面を人間とエージェント(または複数のクライアント種別)が共有し、片方(エージェント)だけがイベントの宛先である構成で、UI 側の生存更新(定期ポーリング・操作後のリフレッシュ・再接続時の取得)が同じ消費型読み取りを使うと、宛先に届く前に UI が配送済み化してしまう。症状は「イベントが時々しか届かない」「連続2操作の1件目だけ消える」など確率的に見えるが、実際はポーリング周期と操作タイミングの競合で決定論的に起きる。
判断基準
- 消費(配送済み化)を伴う読み取りは、イベントの宛先である消費者1経路だけに割り当てる。設計時に「このエンドポイントを叩く呼び出し元」を全列挙し、宛先以外が混ざるなら分離する
- UI の生存更新・リフレッシュ用に、同じデータを返すが配送状態を進めない非消費読み取り(クエリパラメータや別パス)を用意する。非消費側はイベント自体を返さない形にすると誤用がさらに減る
- 「UI は push(SSE/WebSocket)だけ購読させて読み取りAPIを叩かせない」案もあるが、切断時フォールバックのポーリングが結局読み取りAPIへ戻ってくるため、非消費読み取りの分離の方が頑健
検証方法
識別テストとして「非消費読み取りを2回連続で呼んでも配送状態が進まず、その後の消費読み取りで全件届き、さらにその後は0件になる」ことを常設テストにする。消費側単体の二重配送テスト(同じ消費読み取りを2回呼ぶ)だけでは、異なる呼び出し元が消費エンドポイントを共有する衝突を識別できない。
教訓
読み取りに副作用(配送済み化・既読化・カーソル前進)を持たせた時点で、そのエンドポイントはキューの dequeue と同じ扱いになる。呼び出し元が増えるたびに競合面が増えるため、副作用付き読み取りの追加時は消費者の全列挙を契約に含める。