Gravioのアクションは、複数のコンポーネントを連結して構成されるフローで、データ処理の一連の流れを定義します。各コンポーネントは独立したスレッドで並列に動作し、自身のキューから入力Payloadを取得し、処理して出力Payloadを生成し、次のコンポーネントのキューにポストします。
アクションの実行モデルは以下のような形で進行します:
この一連の流れをバケツリレーのように次々とデータが受け渡されていくイメージで考えると理解しやすいでしょう。ただし、実際には同じPayloadが受け渡されるわけではなく、各コンポーネントが独自の処理を施した新しいPayloadを生成します。
シンプルなアクションフローでは、コンポーネントが直列に並んでいる場合、以下のようなフロー図で表現できます:
C01 -> C02 -> C03 -> [END]
この場合、各コンポーネントの動作は以下のようになります:
各コンポーネントは独立したスレッドで並列に動作していますが、Payloadの依存関係によって順序が保たれるため、C01、C02、C03と順に動いているように見えます。
従来のアクションフローでは、コンポーネントが直列に繋がれ、一見すると独立したスレッドで並列に動作しているようには見えませんでした。しかし実際には、あるコンポーネントがI/O待ちをしている間に別のコンポーネントが動作するため、アクション全体としてはCPUを効率よく使用していました。
新しいバージョンでは、アクションフローに明示的な分岐を導入できるようになりました。例えば、Parallelコンポーネントを使用することで、1つの入力Payloadを複製して複数の処理ラインに分岐させることができます:
+-> C11 -> C12
|
C01 -> Parallel -> C03 -> [END]
この例では、C01の出力PayloadがParallelコンポーネントでクローンされ、C03とC11それぞれのキューにポストされます。これによりC03の処理ラインとC11→C12の処理ラインが並列に実行されます。
並列処理の結果、C11のラインの出力Payloadとしては最後の出力Payloadはどのように扱うか決める必要があります。現在の実装では、C11ラインの出力Payloadは最後で破棄され、C03ラインの出力PayloadがアクションのEND(最終出力)に到達します。これは、複数のPayloadが[END]に届くとシステムの挙動が不明確になるのを防ぐためです。
分岐コンポーネントを使用すると、条件に応じて異なるコンポーネントへPayloadを転送できます。基本的な分岐の例を以下に示します:
C01 -> If -> C02 -> [END]
|
+-----------> C03
この場合、Ifコンポーネントの条件式がtrueなら出力PayloadはC02に、falseならC03に転送されます。分岐を使用することで、条件に応じた処理を効率的に実装できます。
Ifコンポーネントは条件分岐を実現するためのコンポーネントです。
Jump/JumpIfコンポーネントは別アクションを呼び出すための機能を提供します。
Call/CallIfコンポーネントも別アクションを呼び出しますが、Jumpと異なり、呼び出し元に制御が戻ります。
Call/JumpとCallIf/JumpIfの選択は、処理の流れによって決まります。Call/CallIfを使うとC12の後はC03に繋がりますが、Jump/JumpIfを使うとC12の後は仮想[END]に繋がり、出力Payloadはアクションの出力となります。
+-> C11 -> C12
|
C01 -> C02 -> C03 -> [END]
Parallelコンポーネントは入力Payloadをクローンして複数の処理パスに転送するためのコンポーネントです。
同一分岐コンポーネントからインスタンス化されるアクションは、同じActionIdだった場合は同じアクションインスタンスが使用されます。これにより、異なるパスから同じアクションが呼び出された場合でも、アクションの状態が共有されます。
例えば、以下のようなケースでは、2つの分岐で同じActionId(A1)のアクションが呼び出されても、処理が混乱することはありません:
(A1)
+-> C11 -> C12
| (A1)
+-> C11 -> C12
|
Split -> Call -> C03 -> [END]
複数の出力Payloadが分岐コンポーネントに届いた場合、最初のPayloadが届いた時にアクションをインスタンス化し、2回目以降は同一アクションインスタンスを使用します。
異なる分岐コンポーネントでは、同じActionIdでも別のアクションインスタンスが生成されます。これにより、異なるコンテキストで同じアクションを独立して実行できます:
(A2)
+-> C21 -> C22
| (A1)
+-> C11 -> C12
| (A1)
| +-> C11 -> C12
|
Split -> Jump -> Call -> [END]
このように、同じアクションを異なるコンテキストで使いたい場合は、別の分岐コンポーネントから呼び出すことが可能です。また、同じアクションをある場所ではCallで、別の場所ではJumpで使うことも可能です。
複数の出力Payloadをポストするコンポーネント(SplitやCSVRead/SensorDataDBなど)の後に、異なるアクションを呼び出したい場合、以下のようなパターンが使えます:
(A1)
+-> C11 -> C12
|
| (A2)
| +-> C21 -> C22
|
Split -> JumpIf -> JumpIf -> C03 -> [END]
JumpIfコンポーネントを使って条件に基づいて異なるアクションを呼び出すことで、プログラミング言語のswitch文のような制御フローを実現できます。また、PreMappingsで条件判断してActionIdを書き換えることでも、同様の制御が1つのコンポーネントで実現できます。
条件に応じて異なるアクションを実行したい場合、JumpIfコンポーネントを活用できます:
(A1)
+-> C11 -> C12
|
| (A2)
| +-> C21 -> C22
|
Split -> JumpIf -> JumpIf -> C03 -> [END]
各JumpIfコンポーネントのExprプロパティに条件を設定し、それぞれ呼び出したいActionIdを指定することで、様々な条件に応じた処理分岐を実現できます。
C02コンポーネントが分岐コンポーネントだとして、条件によってC03に直接ポストすることも、別アクションのC11にポストすることも可能です:
+-> C11 -> C12
|
C01 -> C02 -> C03 -> [END]
この場合、JumpやCallなどのフロー制御コンポーネントを使います。Jumpを使うとC12の後は仮想[END]に繋がりますが、Callを使うとC12の後はC03に繋がります。
別アクションのコンポーネントにポストしても、元のアクションの動作は停止しません。すべてのコンポーネントは並列に動作し、スレッドのスケジューリングはGo言語のランタイムに依存しています。例えば最初のPayloadが(A1)に行き最後は仮想の[END]に行きますが、2回目のPayloadが(A2)に行きます。
分岐処理とアクション間の連携を組み合わせることで、複雑なビジネスロジックも効率的に実装できます。並列処理の特性を活かし、I/O待ちなどによるボトルネックを解消しながら、処理全体の効率を高めることができます。