外部サービスとの整合性の付き合い方
はじめに
Voicyでバックエンドエンジニアをしているmasaです。
先日、こちらの記事で大型機能だったコラボ収録のリリースを振り返りました。
こちらの記事で触れていた、開発の上で難しかったポイントのうち、「Voicyサービスと外部サービス間の整合性の担保」について記事にできればと思います。
コラボ収録ってどんな機能?
前回の記事でも書いたのでそちらと同じ表現になりますが、一言で表すと、「リアルタイムで離れた距離にいる参加者と顔を合わせながら収録ができる機能」です。
ざっくりイメージとしては、ZoomやGoogle Meetのようなオンライン通話機能 + 収録機能 といったイメージになります。
コラボ収録を開始するステップ
コラボ収録機能を利用するステップとしては、
- ホストが収録をするためのルームを作成し、ルームURLを参加者に共有する
- 参加者がURLリンクからルームに入る
- 参加者が入室したタイミングでセッションが作られる。1つのルームを使いまわして何度もコラボ収録を行えるようにしたいため、セッションという概念が生まれる。
- ルームとセッションの関係のイメージは、カラオケのようなイメージで、1つのルームに1つのセッションしか同時に存在することはできないが、セッションが終わると同一のルームで別の人と別のセッションを開始できるといったイメージになります。
- ホストが収録開始する
- ホストが収録を開始すると、収録を停止するまでの間の会話が録音されます
- ホストが収録停止する
- ホストが収録した音源を放送として公開する
といった流れになります。
コラボ収録を実現するための構成
コラボ収録で収録を開始できるようにするために、すごくざっくり表現すると以下のような構成で当初実装していました。
コラボ収録機能において、リアルタイムでの通話機能や録音機能はAgoraという外部サービスを使用しています。
ユーザーから収録開始のリクエストを受け取ると、Voicyサーバー経由でAgora側に録音開始を依頼し、それが成功したら、Voicy側で持っているデータである収録セッションの状態を「収録中」を示すステータスに変更し、それも成功したらユーザーにレスポンスを返すというような構成になっていました。
一見するとこれで問題ないようにも見えるのですが、この構成だと「Agora側で録音開始が行われたが、その後Voicyサーバーでセッションの状態を更新する前に何かしらのエラーが発生してしまった」というような場合に、
- Voicy側では録音が開始されていない
- Agoar側では録音が開始されている
という形になってしまい、それぞれが管理しているデータ間で不整合が起こってしまいます。
データの不整合を解決するには?
この問題を解決するために、以下のような構成に変更をしました。
大きくは変わっていないですが、上述のものとの変更点として、
- Agoraとやりとりする前に「録音準備中」のステータスを準備した
- Agoraからのレスポンスで、「録音が既に開始されているために発生したエラー」の場合は例外を握りつぶす
という対応を行いました。
この構成にすることで、初めの実装では、「録音がAgora側で開始されてしまうと、その後セッションの状態を変える前にエラーが発生した場合に再度録音ができない」という形になってしまっていたものを、「セッションの状態が収録中でなければ、Agora側に何度でも録音開始の依頼を送ることができる」といった形に変更することができました。
つまり、
- 外部サービスとやりとりする前にその状態を記録できるようにする
- 外部サービスがすでに期待している動作をしているなら何事もなかったかのように振る舞う
という対応を入れることによって録音開始の処理を冪等にすることができ、もし処理が不完全な状態で終わってしまったとしても何度でもリトライできる形になりました。
処理を冪等にできるようになったことでのメリット
冪等な処理になったことのメリットとして、上述のように
- 成功するまで同じ一連の処理を何度も実行でき、整合性の取れたデータを作成できる
ということがありますが、これ以外にも、「モジュールをまたぐような巨大なトランザクションをなくせる」ということがありました。
モジュールをまたぐような巨大なトランザクションをなくせる
ユースケースを実現させるために、システムの中で1つのモジュールだけで実現できることもあれば、複数のモジュールを使わないと実現できないことがあるかと思います。
1つのモジュールだけで実現できるのであれば、DBとやりとりしてデータの作成や更新が必要な際に必要な単位でトランザクションで囲えばデータの整合性に関しては問題ないかと思います。
一方で、複数のモジュールを扱う場合には、呼び出す側でトランザクションを張り、そのトランザクションの中で別モジュールの公開された処理を呼び出すような形になると思います。
この作りになると、
- 呼ばれる側ではどこでトランザクションを作られているのかがわからない
- 呼ばれる側でトランザクションを張ることはできるが、期待したトランザクションと異なる形のトランザクションになる可能性があり、整合性の取れないデータを作ってしまう可能性がある
といったことになり得ます。
複数のモジュールを跨いだトランザクションということは、「モジュールを跨いだ一連の処理の流れの中でデータの整合性が取れていること」が期待されていることになると思いますが、呼ばれる側のモジュールで予期せぬエラーが発生した場合、この期待と反した結果になり得てしまいます。
この起こり得る不整合をなくすためにも冪等な処理は使えるなと思います。
例えば、呼ばれる側のモジュール(Aモジュール)の中の処理が冪等になっていると、その処理は何度実行しても同じ結果を得られるので、呼ぶ側のモジュール(Bモジュール)では、Aモジュールを跨がない形でトランザクションを張ることができるようになり、一連の処理の結果で不整合を起こさないようにすることができるようになります。
(呼ぶ側のBモジュールが呼ばれる側のAモジュールの結果を気にしなくてよくなる)
つまり、「各モジュールは、自分の気にする領域だけでトランザクションを張ればいい」といったことができるようになることで、データの不整合の発生を防ぐことに繋げられるかと思います。
まとめ
外部サービスと自分たちのサービスとの間のデータ整合性を保たせるのは難しい問題かと思いますが、今回は処理を冪等にすることでこの問題を解決することができました。
今回この対応をするまで、外部サービスとの付き合い方でこの手札を自分の中で持てていなかったので、新たに1つ手札を増やすことができたのは非常に良かったです。
今後も学び続けていろんな手札を自分の中で増やしていくことで、より信頼性の高いシステムを作っていければと思います!
Discussion