👋
メッセージ送信とFCM
TL;DR
- DB のレコード永続化と FCM 送信は “同一トランザクション” にまとめず、まず DB だけ確実にコミットする
- 通知は ベストエフォート でも動くが、実運用では “トランザクショナル・アウトボックス” などで “後から必ず送る” 仕組みを入れると安全
- 失敗時は リトライ/死活監視/メトリクス を用意し、ユーザー体験を壊さないフォールバック(未読バッジ、ポーリング更新など)を整えるとよい
なぜ DB と外部 API を同じトランザクションにしないか
性質 | DB 書き込み | FCM 呼び出し |
---|---|---|
ACID を保証 | できる | できない |
レイテンシ | 数 ms〜数 10 ms | 数百 ms 以上・ネットワーク依存 |
冪等性 | 主キー制約で担保しやすい | 送信重複でプッシュが複数回届くなどの副作用 |
失敗ハンドリング | ロールバックで一括 | 部分失敗が多い(タイムアウト・4xx/5xx) |
外部 API をトランザクションに含めるには 分散トランザクション (2PC) が必要ですが、FCM はその機構を提供していません。
したがって「まずアプリ DB だけ確実にコミットし、その後で通知処理を実行」が一般解になります。
失敗してもメッセージが残る設計は “正しい前提”
- ユーザーが送信 → メッセージは必ず残る(最重要)
- 通知は “読み取り利便性” 向上策であり、UX は下がっても データ欠損よりはマシ
ただし 通知ロストが多発するとクレーム要因 になるので、以下のような堅牢化を推奨します。
より信頼性を高める 3 つの代表パターン
1. トランザクショナル・アウトボックス(王道)
-
DB トランザクション内で
-
messages
テーブルにレコードを INSERT -
outbox
テーブルにsend_push:{message_id}
の“未送信イベント”を INSERT
-
-
トランザクション COMMIT
-
別プロセス(Worker)が
outbox
をポーリングまたは DB の通知機構(Postgres NOTIFY など)で購読 -
イベントを取り出し FCM 送信。成功したら
outbox
の行を削除/フラグ更新。失敗時はリトライ + バックオフ
メリット
- DB コミットと“通知ジョブの登録”が同 atom で完結し整合性が保てる
- Worker はスケールアウト可能・リトライも簡単
2. メッセージキュー / Pub‑Sub
- 例: Cloud Pub/Sub, SQS, Kafka
- DB トランザクション完了後に 同じメッセージ内容を Queue に Publish
- 通知サービスが Queue を購読し FCM へ配送
- 失敗時は 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