Saga Orchestration - define and run your desired workflows
Saga Orchestration gem is inspired by Microservice Patterns by Chris Richardson.
It provides a framework to employ the Saga Orchestration patterns described in detail in his books and articles. Secondly, it makes it easier for firms to visualize the entire flow as a set of steps.
This is available on github at: Saga Orchestration Gem
Description
The key goals this gem seeks to address are -
- Enable transactions spanning singular or multiple servies to be addressed as a single, atomic operation?
- Depict flowchart of states be depicted using a simple DSL
- Branch to states based on conditions akin to flow charts
- Rollback compensatory transactions in case of errors and failures. These can be mandated and hooks provided for the same
- Visualize the sequence of steps executed as set of states ordered by sequence of operation and results at each stage
Getting Started
Dependencies
- Ruby 2.7 or higher
Installing
gem install saga_orchestrator
Executing program
Basic Terminology
Saga::StateEngine: Parent Class that can be inherited from to define the states within the work flow and sequence the same.
Saga::Orchestrator - Class responsible for running the StateEngine instance and produce end result of all states.
States - In system parlance terms, this could be a specific event like Payment Processing, New user assignment, create transaction etc. Each state can be hooked to a function with guidance on nature of function input.
Sequence - The order in which states should be executed
How to build a workflow
To build workflows, the first step is to design the workflow. Let us take an example of a workflow as in the image below.
-
Create a child class inheriting from Saga::StateEngine
-
Add a function state_registration within the child class as below to setup all the states as below. Use register_states features to register various states. (sample code below)
def state_registration register_states do |add_state| add_state.standard :state_01 do |state| state.call Functions::Test.method(:test_func) state.params do |p| p.set_type :input_params end state.process_output Processors::Test.method(:processor) end add_state.standard :state_02 do |state| state.call Functions::Test.method(:test_func2) state.params do |p| p.set_type :last_result end state.process_input Processors::Test.method(:input_processor) state.process_output Processors::Test.method(:processor) end add_state.compensatory :state_03 do |state| state.call Functions::Test.method(:test_func3) state.params do |p| p.set_type :last_result end state.rollback_method do |rollback| rollback.call Rollback::Test.method(:rollback2) rollback.params do |p| p.set_type :last_result end end state.process_input Processors::Test.method(:input_processor) state.process_output Processors::Test.method(:processor) end add_state.standard :state_04 do |state| state.call Functions::Test.method(:test_func4) state.params do |p| p.set_type :last_result end state.process_input Processors::Test.method(:input_processor) state.process_output Processors::Test.method(:processor) end add_state.standard :cond01 do |state| state.call Functions::Test.method(:conditional_test) state.params do |p| p.set_type :last_result end end end end
How to define a state
States can be of two types:
- Standard - Can be hooked to a function and operates in the mode of retriable or pivot transactions
- Compensatory - Similar to standard function. But requires an additional input of a rollback function. In the event of an error, the rollback function is called
Key parameters to define a function:
- .call: -> provide the hook to any function in your project using method() as in the example above
- .params: -> defines the parameter to be provided as input. Define a p.type within the block against state.params. Inputs may be of four types:
- input_params: The parameters provided at the time the state engine is invoked. This is explained in the sections ahead
- last_result: The result from the last state
- direct: Direct inputs where an absolute set of values may be provided. p.type :direct p.input ##input value## // can be anything example [1,2,3] or {x: 3,y: 6} etc
- none: No input
- .process_input: In case one wishes to process the input prior to passing to function, this may be added and a function hooked
- .process_output: In case one wishes to process the output post passing to function, this may be added and a function hooked
- .rollback_method: only available and mandatory for compensatory states. You may hook a function to perform rollback and define the parameters to pass to the function. This usually becomes the end of the workflow as a rollback is considered a fail and closure.
How to sequence states
One simple way to sequence states is to do nothing. If you have defined the states in the order in which they should execute and there are no conditionals, this would work well.
But if you have defined the states in any order and wish to put conditionals in the flow sequence, you can follow the steps below. The following is an example of a function that is added to the child class to provide your own sequence
def sequence_states
describe_flows do |seqs|
#this creates the first sequence with name :seq_a
seqs.start :seq_a do |seq|
seq.init state_name :state_01 #always add an init to start the sequence. Use state_name to invoke a particular registered state
seq.then state_name :state_02 #to define the next state.
seq.then_conditional state_name :cond01 do |t| #the state defined here has to provide a true or false result
t.on_true state_name :state_03
t.on_false state_name :state_04
end
seq.end #closes the sequence. This can be inserted within any sub sequence or conditional flow and it will close execution immediately
end
end
end
In addition to the above, you can also define a sub sequence using seqs.sub instead of seqs.start. There should be only one seqs.start as this is taken as the the point to start execution of the state engine. To invoke a sub sequence, replace the state_name with:
sequence_name :seq_name
How to run the state engine
If lets say, our child class is defined as Workflows::Test, you can initiate the state engine as follows: '''
obj = Saga::Orchestrator::new(Workflows::Test, 30, 40) #class followed by any number of input parameters. This will be referred as :input_params as explained above under definition
obj.run() #to run the state engine. This will return result with two keys: status and result
obj.execution_sequence # you can use this to get a hash contains each node that was run and results at each node level
'''
state can be:
- :success - completely ran all the way to end
- :error - in case of error in between that halted execution
- :rollback - in case of rollback
And that is it. You are good to go.
Tests
I will be adding some tests soon to the same.
Authors
Contributors names and contact info
Jayanth Ravindran email: jayanth.ravindran@gmail.com
Version History
- 0.1
- Initial Release
License
This project is licensed under the MIT License - see the MIT-LICENSE file for details
Acknowledgments
Inspired from Chris Richardson's articles on managing transactions across services