デバッグしていて遅くなり、帰ってから慌ててスーパーに買いものへ行ったりしていて時間がなくなったので今日はバドミントン練習をお休み。
データベースとメッセージキューの整合性を考える
昨日の続き。トランザクションを導入したことで mongodb と mq でのデータフローが意図せず変わってしまっていることが調査してわかった。
従来は次のように動いていた。
- api サーバーがエントリー情報を受け取る
- api ハンドラーがエントリー情報を mongodb に保存する
- api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する
- api ハンドラー内の producer が rabbitmq にメッセージを送信する
- consumer がメッセージを受信する
- consumer が mongodb からジョブ情報を参照する
- api ハンドラーがレスポンスを返す
- consumer がメッセージを処理する
トランザクションを導入したことで mongodb にデータが保存されるタイミングが変わってしまった。
- api サーバーがエントリー情報を受け取る
- api ハンドラーがトランザクションを開始する
- api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
- api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
- api ハンドラー内の producer が rabbitmq にメッセージを送信する
- consumer がメッセージを受信する
- consumer が mongodb からジョブ情報を参照するが、トランザクションがコミットされていない可能性がある
- api ハンドラーがトランザクションをコミットする
- api ハンドラーがレスポンスを返す
- consumer でエラーが発生する
処理の流れを見直して次のように改修した。内部実装の都合があってやや煩雑な変更となった。
- api サーバーがエントリー情報を受け取る
- api ハンドラーがトランザクションを開始する
- api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
- api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
- api ハンドラーがトランザクションをコミットする
- api ハンドラー内の producer が rabbitmq にメッセージを送信する
- consumer がメッセージを受信する
- consumer が mongodb からジョブ情報を参照する
- api ハンドラーがレスポンスを返す
- consumer がメッセージを処理する
これは単純にトランザクションのコミットタイミングと mq へのメッセージ送受信のタイミングを見直せばよいという話しではない。本質的に mongodb で管理しているジョブ情報と rabbitmq へ送信しているメッセージの整合性を保証することはできないということを表している。producer はメッセージ送信に失敗する可能性があるから、そのときにジョブ情報を書き換える必要はあるが、その前にトランザクションをコミットしてしまっているため、api ハンドラー内でデータのコミットタイミングが複数になってしまう。トランザクションを導入したメリットが失われてしまい、Unit of work のパターンも実現できない。consumer の処理に必要な情報を mongodb にあるデータとメッセージの2つに分割しているところが整合性の問題を引き起こしている。アーキテクチャ上の設計ミスと言える。consumer の処理に必要な情報はすべてメッセージに含めてしまい、メッセージを処理した後に mongodb に結果を書き込むといった設計にすべきだった。
初期実装のときからジョブ情報を mongodb で管理する必要はあるのか?という懸念を私はもっていた。要件や機能が曖昧な状況でもあり、メンバーもなんとなく db に管理情報を残しておいた方が将来的な変更に対応できて安心といった理由だったと思う。当時は整合性の問題が起きることに、私が気付いていなかったためにこの設計を見直すように強く指摘できなかった。トランザクションを導入したことで consumer が必要な情報を db に保持すると、db とメッセージ処理のタイミングにおける整合性の問題が生じるという学びになった。
いまとなってはこのジョブ情報を使う他の機能もあるため、この設計を見直すことはできない。今後の開発プロジェクトで db とメッセージを扱うときはこの経験を活かすためにふりかえりとして書いておく。