OutSystems Developer Cloud (ODC) では、OutSystems Developer Cloudのアーキテクチャ設計 - マイクロサービスがほぼ強制される に書いた通り、マイクロサービスで設計・実装していくことになりそう。
その場合、複数のサービスにまたがるトランザクションをどう実現するかを検討する。
ここでは実装までは踏み込まない。後日実装してみて、この記事のアーキテクチャは修正するかもしれない。
マイクロサービスにしたときの困りごと:複数のサービスにまたがるトランザクションをどう実現するか
例として以下の構成で検討してみる。
UIを持つApp上でユーザーがある処理をリクエストしたとき、
- サービスA (Service Action) で検索を行う
- Aの結果を使ってサービスBにリクエスト(内部で更新を伴う)
- A,Bの結果を受けてサービスCにリクエスト(内部で更新を伴う)
- Cの処理中に例外が発生しロールバック。このときサービス同士、またサービスと呼び出し元は別のトランザクションなので、Bで行った更新はロールバックされない
このロールバックされないトランザクションをどう救うか、というのがここでの問題。
解決策としてのSaga
Sagaというのは、複数のサービスにまたがるトランザクションを、ローカルトランザクションのつながりとして実現する方法。
ローカルトランザクションは1つのサービス(OutSystemsならService Action)単位でのトランザクションを指す。OutSystemsの仕組みにより、Servcie Action全体で1つのトランザクションになる。
実装方法には、大まかに2種類(ChoreographyとOrchestration)あり、後者にはさらにOutSystemsの公式動画で紹介されている方法と、より一般的なMessage Brokerを使う方法がある(他にも実装方式はあるかもしれないが)。
Sagaに言及した多くのドキュメントでは、各サービスの仲立ちにMessage Brokerを置くが、ここではOutSystems内で完結する方法を検討する。2023/09/12のONEで発表された機能によっては、OutSystemsでEvent Driven Architectureを使ってネイティブに実装できるようになるかもしれない。
必要を満たす範囲で最もシンプルな方法を取りたい。
個人的な優先順位は
- 全てを1つのApp内に収める
- OutSystemsが紹介しているSaga
- Choreography
- 一般的なOrchestration
Sagaの用語確認:ローカルトランザクション
Sagaは複数のサービスにまたがるトランザクションで、各サービスが別のトランザクションを形成する(サービスはService ActionやREST APIで実現するので自動的に別のトランザクションになる)。
各サービスに対応するトランザクションをローカルトランザクションと呼ぶ。
Sagaの用語確認:補償トランザクション
複数のサービスにまたがる処理をある程度進めた段階でエラーが発生し、状態を開始前に戻さなければならないとする。
このとき、エラーが発生した時点から、開始時点に向けてさかのぼりながら、コミット済みの更新を打ち消す処理(追加したレコードの削除、逆の計算、など)を行う。この時のトランザクションを補償トランザクションと呼ぶ。
Choreography
Choreography Sagaは、一連の処理を、サービスリクエスト⇒サービス終了時にイベントを発生させる⇒そのイベントを見ている別のサービスが起動する⇒そのサービス終了時にイベントを発生させる⇒…… という形式で実現する。
実装例としては、例えばLight BPTを使った方法が考えられる。
- ローカルトランザクション毎にLight BPTを作成する
- ローカルトランザクションの最後でイベント登録用のEntityにInsert
- イベント登録用のEntityへのInsertをLaunch Onに指定しておいた次のローカルトランザクションが起動する
- エラーが発生したときには、エラー用のEntityにInsertし、そのEntityをLaunch Onに指定している補償トランザクションを発行するLight BPTを起動
Orchestration
各ローカルトランザクションの進行を中央で管理するOrchestratorという処理を持つタイプのSaga。
UIから呼ばれるActionをOrchestratorにするパターン(公式動画のパターン)
いくつかのWebサイトでSagaの説明を確認したが、ローカルトランザクション完了後に他のローカルトランザクションを起動する際にはメッセージかイベントを使うとしているものが多かった。
その点で、ここで示す方法は厳密にSagaと言っていいのかどうかわからないが、以下のOutSystemsの公式動画でSagaと呼んでいるので、そう扱うことにする。
公式動画に、このパターンの解説があるので引用する。
以下の画像のStartからEndまでが1つのServer Action。Server Action内で呼んでいる個別のService Action呼び出しは、それぞれ別のトランザクションとなるため、更新がある場合はOutput Parameterをチェックして補償トランザクションを発行する。また、例外をハンドルした際にも、実行時点より前の各リクエストを対象に、逆順に補償トランザクションを発行していく。
(Architecture Patterns in ODC > Distributed Transactions Patterns のLesson Materials p.22より、2023/09/13に引用)
Orchestratorと各サービスのやり取りをMessageで行うパターン
公式動画のパターンの場合、
- サービスAに対して検索
- サービスBに対して更新
- サービスCに対して更新をかけたら、エラーになった
- サービスBに対して補償トランザクションを発行しようとしたら、サービスBが一時的に動作していなかった
というような場合に、回復する仕組みを用意しないといけない。
Sagaとして、より正統と思われるMessage Brokerを各ローカルトランザクションの仲立ちにするパターンであれば、(Message Brokerが信頼できれば)サービスBが復帰したときにMessageを読みに行って補償トランザクションを発行することで復旧できる。
現時点では、OrchestratorをBPTのProcessで作成し、Waitを使って、各ローカルトランザクションの終了を待つ方法をとることになる。
ONE Lisbonで発表されたEvent Driven Architecture⇒翌日にはツイートは削除されていた
2023/09/12にLisbonで開催されたONEにおいて、Event Driven Architectureが発表された、というツイートがあったのだが、13日夜にチェックしたら削除されていた。
現時点では、内容が不明だが、もしかしたら、Sagaの実装に使えるかもしれない。
NEWLY unveiled at #ONEOutSystems: Event-Driven Architecture - the ability to streamline development and reacting in real-time to the changes in your app portfolio. Coming soon to ODC! 👀 pic.twitter.com/Px5VtYyhmZ
— OutSystemsDev 🚀 (@OutSystemsDev) September 12, 2023