/note/tech

Process Managerパターンで複雑な業務フローを見通しよく実装する

要約:

■ 1. 背景とProcess Manager導入前のアーキテクチャ

  • イベント駆動のシステムで処理の流れが複雑化してピタゴラスイッチ状態になることがある
  • あるイベントが発生してそれをトリガーに別の処理が動いてさらにその結果を受けて次の処理が連鎖していくうちに全体のフローを把握するのが困難になる
  • どこで何が処理されるのかを追うのに複数のハンドラを行ったり来たりしながら読み解かなければならない
  • エネルギー取引のドメインを扱うプロダクトを開発している
  • 公平な取引が行われていたことを証明するため官公庁への取引ログ提出を求められる場合があり任意の時点でのシステム状態を復元できる必要があるためCQRS+Event Sourcingパターンを採用してシステムを構築している
  • ある業務の完了をトリガーに別の業務を実行するというパターンは非常に多く見られる
  • 注文が作成されたら履歴を作成する約定したら通知を送るといった後続業務が多数存在している
  • Event Sourcingではイベントが一級市民であるため自然とイベントをトリガーとして非同期で後続処理を実行するパターンを採用することとなった
  • 一連の業務処理を一つのトランザクションで実行するのではなくイベントを介して非同期に連携させることによって異なる業務同士を疎結合に実装できたりユーザーリクエストに対するレスポンス時間の短縮にもつながるなどイベント駆動の利点を得られている

■ 2. 直面した課題

  • イベント駆動な業務プロセスの自動化を実現するため最初はイベントに対するハンドラを個別に定義するシンプルなアプローチを取っていた
  • このパターンはイベントを受けて1つの処理を行うという単純なケースには適している
  • 履歴作成や通知送信など多くの後続処理はこのパターンで実装している
  • しかし業務プロセスが複雑になるとこのパターンでは対応しきれなくなった
  • 2つの業務があり業務の開始位置が異なる場合:
    • 業務①はイベントAから始まり処理1→イベントB→処理2-1の順に進む
    • 業務②はイベントBから始まり処理2-2と進む
  • どちらの場合も途中でイベントBを受け取るが特定のイベントに対応して後続の処理を実行するだけの単純なイベントハンドラではイベントBを受けたときにこれがどの業務プロセスに属するイベントなのかを判別できないため次にどの処理に進んでいいのかが判断できない
  • この問題はイベントハンドラが前のステップのコンテキストを持たないことによって生じている
  • 対応策として業務プロセスに関するコンテキスト情報をイベントに含める方法が考えられる
  • しかしこれにはいくつかの課題がある:
    • 業務プロセスの制御に関する情報までイベントに含めてしまうとイベントが本来持つ業務で起こった事実を記録するという責務から逸脱してしまう
    • 異なる業務がお互いの存在を意識しなければならなくなり疎結合性が損なわれる
    • 業務プロセスが増えるたびにイベント定義が肥大化し保守が困難になる
    • 外部システムを経由する業務プロセスではそもそも自システムのコンテキスト情報をすべてイベントに含めてもらうこと自体が難しい場合もある
  • 一連の業務プロセスを独立したイベントハンドラの組み合わせで実装すると全体のフローを把握しづらくなる
  • どのイベントがどこで処理されるのかを追うために複数のハンドラを行き来しながら読み解く必要がありコードの可読性と保守性が低下する

■ 3. Process Managerパターンの概要

  • Process Managerパターンを導入することで課題の解決を図った
  • Process Managerはイベント駆動の処理フローの中に複数の集約間のメッセージ交換を仲介・調整する役割を追加するパターンである
  • 業務フローにおける現在のステップやコンテキスト情報を保持し受け取ったイベントに基づいて次に実行すべきアクションを決定して発行する
  • Process Managerは複数のステップからなる業務の流れを管理するステートマシンのように振る舞う
  • Process Managerの動作:
    • イベントを受け取る
    • 現在の状態を確認する
    • 次に行うべきアクションを決定して送信する
    • 状態を更新する
  • 実装時に意識したいのはProcess Managerはイベントを受け取りコマンドを発行するという責務に徹すること
  • 業務ロジック自体はコマンドハンドラ側に実装しProcess Managerはあくまでどのコマンドを発行するかを決めるルーティング役に専念する
  • こうすることでコマンドハンドラとProcess Managerにロジックが分散することを防ぎコードの見通しが良くなる
  • 単純なイベントハンドラとProcess Managerの違い:
    • イベントハンドラ:
    • 状態を持たない
    • 判断基準はイベントの内容のみ
    • 適する用途は1イベント→1処理
    • Process Manager:
    • 状態を持つ
    • 判断基準はイベント+現在の状態
    • 適する用途は複数ステップの業務プロセス

■ 4. 実装例における要件

  • エネルギー取引を扱うシステムにおいて注文を作成するプロセスを考える
  • 自システムは業務効率化のために注文をクローズドに管理するシステムである
  • それとは別に外部公開されている取引所システムが存在しユーザーはそちらにも注文を掲載したい場合がある
  • 2つの市場:
    • InternalMarket:自システムが管理する市場
    • ExternalExchange:外部システムで管理される外部取引所
  • ユーザーは自システムが管理する市場にのみ注文を掲載することもできるし外部システムの取引所またはその両方に掲載することもできる
  • 両市場を選択した場合の注文作成の流れ:
    • 自システムで注文を作成
    • 外部システムに注文作成をリクエスト
    • 外部システムからの作成完了通知を待つ
    • 外部取引所への掲載を反映
    • 自市場への掲載を反映
    • 作成完了を通知
    • ユーザーの画面に注文が反映される
  • どちらか片方の市場のみを選択した場合は上記のフローから不要なステップが省略される
  • 注文作成プロセスは上記のようにユーザー起点で始まる場合もあれば外部システムの取引所で注文が作成されたことをきっかけに自システムに注文を取り込む場合もある

■ 5. ProcessManagerインターフェースと呼び出し構造

  • ProcessManagerの核となるインターフェース:
    • CanHandle:このProcess Managerが処理すべきイベントかを判定
    • Execute:イベントを受け取り状態に応じてコマンドを発行
    • ExecFunc:コマンドバスへの委譲関数でこれを通じてコマンドを実行し他の集約を操作する
  • Workerは複数のProcessManagerを保持しておりイベントを受け取ると対応するProcessManagerを探して実行する
  • commandBusはコマンドをコマンドハンドラにルーティングするコンポーネントである

■ 6. 状態遷移の設計

  • Process Managerの設計ではまずは業務プロセスを構成するステップとその状態遷移を整理することから始める
  • 業務のステップ:
    • NotStarted:プロセス未開始
    • AwaitingExternalCreation:外部システムでの作成待ち
    • AwaitingExternalListing:外部取引所へ掲載されたことをマークする処理待ち
    • AwaitingInternalListing:自市場への掲載されたことをマークする処理待ち
    • Completed:プロセス完了
  • イベント:
    • OrderCreated:自システムで注文が作成された
    • ExternalOrderCreated:外部システムで注文が作成された
    • ExternalOrderRejected:外部システムでの注文作成が拒否された
    • ListedOnExternal:外部取引所への掲載が完了した
    • ListedOnInternal:自市場への掲載が完了した
  • もともとのリクエストの内容によって同じステップ・イベントでも次のステップが変わったり同じイベントが異なるステップで発生したりする
  • 業務のコンテキストや現在のステップに応じて処理を分岐させる必要があるため単純なイベントハンドラの組み合わせでは処理の流れを把握することはかなり難しくなる
  • Process Managerのメソッドとして各イベントのハンドラを実装していくため状態遷移を表形式でまとめると実装時に役立った
  • 業務プロセスの状態を保持する構造体:
    • ProcessID:業務プロセスを一意に識別するID
    • Step:現在のステップ
    • OrderID:対象の注文ID
    • MarketDest:掲載先
  • ProcessIDがポイントである
  • 業務プロセスの開始時に例えば注文作成時にProcessIDを生成し後続のイベントにはこのProcessIDのみを含める
  • イベント定義にプロセスのコンテキスト情報を含める代わりにProcessIDを使って複数のイベントにまたがるプロセスを識別し必要なコンテキストはStateから取得する
  • 外部システムを経由したコンテキストの受け渡しについては今回のケースにおいては外部システムがリクエストIDを受け取り結果通知時に同じIDを返す仕組みであったためProcessIDをリクエストIDに含めることによって実現した
  • 自システムに必要なコンテキスト情報をすべて外部システムに引き回してもらうのは現実的でない場合が多いがリクエストIDのような一般的な仕組みを活用することで実現できるところもこのパターンの利点である

■ 7. Executeメソッドとイベントハンドラの実装

  • Executeメソッドではイベントの種類に応じたハンドラを呼び出し次の状態を保存する
  • イベントハンドラは内部的にコマンドハンドラを呼び出しコマンドを実行する
  • このとき新たにイベントが発生するため即座にコミットしてしまうと次の処理が現在の処理の完了を待たずに始まってしまい古い状態を参照して処理されてしまう可能性がある
  • これを防止するためにコマンドの実行と状態の保存を同一トランザクション内で行うようにしている
  • イベントハンドラでは現在のProcess Managerの状態に応じて処理を分岐させる
  • ExternalOrderCreatedイベントを処理するハンドラでは現在のステップによって注文作成が自システム起点なのか外部システムなのかを判別し適切なコマンドを発行している
  • ListedOnExternalイベントを処理するハンドラではもともとの注文作成時に指定された掲載先に応じて次のステップを分岐させている
  • 以上のようにして複雑な業務プロセスをイベント駆動の利点を損なうことなく見通しよく実装することができた

■ 8. まとめ

  • Process Managerが解決する課題:
    • 単純なイベントハンドラでは複数ステップにまたがる業務プロセスでどの文脈のイベントかを判別できない
    • 処理が複数のハンドラに分散し全体のフローを把握するのが困難になる
    • Process Managerに業務プロセスに関する知識を集約し現在のステップやコンテキストを状態として保持することでイベント駆動の利点を損なうことなく複雑な業務プロセスを見通しよく実装できる
  • 設計するときのポイント:
    • まず業務プロセスをステップと状態遷移で整理すると実装しやすい
    • 業務ロジックはコマンドハンドラ側に実装しProcess Managerはルーティングに徹する
    • Process Managerは強力なパターンだが単純なケースには過剰な抽象化になり得るため使いどころを見極める必要がある
    • 履歴作成や通知送信といった単純な後続処理はシンプルなイベントハンドラで実装している
  • イベント駆動のシステムで業務プロセスが複雑化してきたときはProcess Managerパターンの導入を検討する