👋

メッセージ送信とFCM

に公開

TL;DR

  • DB のレコード永続化と FCM 送信は “同一トランザクション” にまとめず、まず DB だけ確実にコミットする
  • 通知は ベストエフォート でも動くが、実運用では “トランザクショナル・アウトボックス” などで “後から必ず送る” 仕組みを入れると安全
  • 失敗時は リトライ/死活監視/メトリクス を用意し、ユーザー体験を壊さないフォールバック(未読バッジ、ポーリング更新など)を整えるとよい

なぜ DB と外部 API を同じトランザクションにしないか

性質 DB 書き込み FCM 呼び出し
ACID を保証 できる できない
レイテンシ 数 ms〜数 10 ms 数百 ms 以上・ネットワーク依存
冪等性 主キー制約で担保しやすい 送信重複でプッシュが複数回届くなどの副作用
失敗ハンドリング ロールバックで一括 部分失敗が多い(タイムアウト・4xx/5xx)

外部 API をトランザクションに含めるには 分散トランザクション (2PC) が必要ですが、FCM はその機構を提供していません。
したがって「まずアプリ DB だけ確実にコミットし、その後で通知処理を実行」が一般解になります。


失敗してもメッセージが残る設計は “正しい前提”

  1. ユーザーが送信 → メッセージは必ず残る(最重要)
  2. 通知は “読み取り利便性” 向上策であり、UX は下がっても データ欠損よりはマシ

ただし 通知ロストが多発するとクレーム要因 になるので、以下のような堅牢化を推奨します。


より信頼性を高める 3 つの代表パターン

1. トランザクショナル・アウトボックス(王道)

  1. DB トランザクション内で

    • messages テーブルにレコードを INSERT
    • outbox テーブルに send_push:{message_id} の“未送信イベント”を INSERT
  2. トランザクション COMMIT

  3. 別プロセス(Worker)が outbox をポーリングまたは DB の通知機構(Postgres NOTIFY など)で購読

  4. イベントを取り出し FCM 送信。成功したら outbox の行を削除/フラグ更新。失敗時はリトライ + バックオフ

メリット

  • DB コミットと“通知ジョブの登録”が同 atom で完結し整合性が保てる
  • Worker はスケールアウト可能・リトライも簡単

2. メッセージキュー / Pub‑Sub

  • 例: Cloud Pub/Sub, SQS, Kafka
  1. DB トランザクション完了後に 同じメッセージ内容を Queue に Publish
  2. 通知サービスが Queue を購読し FCM へ配送
  3. 失敗時は Dead‑Letter Queue へ送り監視

注意: キュー publish と DB コミットの間に障害が起こると “メッセージあり/通知なし” のギャップ が発生し得る。ここが気になる場合は Outbox + 変更データキャプチャ (CDC) を採用して完全保証に寄せる。

3. シンプル同期呼び出し(小規模向け)

await db.tx(async (tx) => {
  const message = await tx.insert(messages).values(...).returning();
});         // ここで確実にコミット

try {
  await sendFcm(message);
} catch (err) {
  log.error(err);
  // 失敗ログを保存し、非同期バッチで再送する仕組みを別途用意
}

メリット: 実装が最小。
デメリット: 送信部分が API レイテンシに跳ね返る & 落ちたら手動対応 が必要。
→ PoC や少人数サービスでは許容できますが、規模が大きくなったら①②へ移行するのがおすすめです。


失敗時の UX フォールバック

シーン 推奨策
通知が飛ばなかった チャット一覧を 定期ポーリング または Socket でリアルタイム更新 しておけば、アプリ復帰時に未読バッジで気付ける
重複送信 通知ペイロードに idempotency key (message_id) を入れ、クライアント側で same ID は捨てる
送信遅延 FCM 成功/失敗・再送のメトリクスを Prometheus + Grafana などで可視化し、閾値超えで PagerDuty 通知

まとめ

  • DB コミット → 通知処理 の順で OK。
  • とはいえ通知ロストを抑えたいなら トランザクショナル・アウトボックスキュー + リトライ が業界標準。
  • 小規模ならまずは単純実装 + 失敗ログを残し、後で Outbox への移行を計画しても良いです。
  • どのパターンを選んでも 冪等性・リトライ戦略・監視 をセットで設計するのが “一般的な考え方” です。

Discussion