saga_orchestrator 0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/Readme.md +195 -0
- data/examples/main.rb +6 -0
- data/examples/workflow_models/definitions/test.rb +72 -0
- data/examples/workflow_models/functions/test.rb +28 -0
- data/examples/workflow_models/processors/test.rb +11 -0
- data/examples/workflow_models/rollback/test.rb +12 -0
- data/images/flow_chart.png +0 -0
- data/lib/helpers/enum_helper.rb +17 -0
- data/lib/saga_orchestrator.rb +38 -0
- data/lib/state_management/custom_exceptions/exceptions.rb +35 -0
- data/lib/state_management/run_states.rb +84 -0
- data/lib/state_management/sequences.rb +253 -0
- data/lib/state_management/state_engine.rb +122 -0
- data/lib/state_management/state_manager.rb +37 -0
- data/lib/state_management/states.rb +42 -0
- data/lib/transactions/compensatory.rb +55 -0
- data/lib/transactions/parameters.rb +27 -0
- data/lib/transactions/retriable.rb +11 -0
- data/lib/transactions/rollback_method.rb +25 -0
- data/lib/transactions/transaction.rb +99 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8f2d80f2049b75b37a4a7bba59a62f4044e520c8e8ef6e883d37c6f25b83370b
|
4
|
+
data.tar.gz: 70ca29bd90e75a3528b30e3a7cfbc74960aabc2fff1ec27514e8dd81f5a51019
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e80266e57ca6d8800e2b75c9035259f8f7c246e389646cab07ae925f3e509b97b3c035ec34201bbda37052caa7d5dbc89b5ecb0a692d6064fb04d032fcbcd42a
|
7
|
+
data.tar.gz: 81b6e4d48f690b5eb8d21e9070dc02c9afafe71f757e926f2c25ab3cc0bebb2fe1992cd56517bea03c893eef99442ace1a967c9acbe46397b8d6fc31f75ada9c
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024-2025 Jayanth Ravindran
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/Readme.md
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
# Saga Orchestration - define and run your desired workflows
|
2
|
+
|
3
|
+
Saga Orchestration gem is inspired by Microservice Patterns by Chris Richardson.
|
4
|
+
|
5
|
+
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.
|
6
|
+
|
7
|
+
## Description
|
8
|
+
|
9
|
+
The key goals this gem seeks to address are -
|
10
|
+
1. Enable transactions spanning singular or multiple servies to be addressed as a single, atomic operation?
|
11
|
+
2. Depict flowchart of states be depicted using a simple DSL
|
12
|
+
3. Branch to states based on conditions akin to flow charts
|
13
|
+
4. Rollback compensatory transactions in case of errors and failures. These can be mandated and hooks provided for the same
|
14
|
+
5. Visualize the sequence of steps executed as set of states ordered by sequence of operation and results at each stage
|
15
|
+
|
16
|
+
## Getting Started
|
17
|
+
|
18
|
+
### Dependencies
|
19
|
+
|
20
|
+
* Ruby 2.7 or higher
|
21
|
+
|
22
|
+
### Installing
|
23
|
+
|
24
|
+
```
|
25
|
+
gem install saga_orchestrator
|
26
|
+
```
|
27
|
+
|
28
|
+
### Executing program
|
29
|
+
|
30
|
+
#### Basic Terminology
|
31
|
+
|
32
|
+
Saga::StateEngine: Parent Class that can be inherited from to define the states within the work flow and sequence the same.
|
33
|
+
|
34
|
+
Saga::Orchestrator - Class responsible for running the StateEngine instance and produce end result of all states.
|
35
|
+
|
36
|
+
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.
|
37
|
+
|
38
|
+
Sequence - The order in which states should be executed
|
39
|
+
|
40
|
+
### How to build a workflow
|
41
|
+
|
42
|
+
To build workflows, the first step is to design the workflow. Let us take an example of a workflow as in the image below.
|
43
|
+
|
44
|
+

|
45
|
+
|
46
|
+
1. Create a child class inheriting from Saga::StateEngine
|
47
|
+
2. Add a function register_states within the child class to setup all the states as below.
|
48
|
+
|
49
|
+
'''
|
50
|
+
def state_registration
|
51
|
+
|
52
|
+
register_states do |add_state|
|
53
|
+
|
54
|
+
add_state.standard :state_01 do |state|
|
55
|
+
state.call Functions::Test.method(:test_func)
|
56
|
+
state.params do |p|
|
57
|
+
p.set_type :input_params
|
58
|
+
end
|
59
|
+
state.process_output Processors::Test.method(:processor)
|
60
|
+
end
|
61
|
+
|
62
|
+
add_state.standard :state_02 do |state|
|
63
|
+
state.call Functions::Test.method(:test_func2)
|
64
|
+
state.params do |p|
|
65
|
+
p.set_type :last_result
|
66
|
+
end
|
67
|
+
state.process_input Processors::Test.method(:input_processor)
|
68
|
+
state.process_output Processors::Test.method(:processor)
|
69
|
+
end
|
70
|
+
|
71
|
+
add_state.compensatory :state_03 do |state|
|
72
|
+
state.call Functions::Test.method(:test_func3)
|
73
|
+
state.params do |p|
|
74
|
+
p.set_type :last_result
|
75
|
+
end
|
76
|
+
state.rollback_method do |rollback|
|
77
|
+
rollback.call Rollback::Test.method(:rollback2)
|
78
|
+
rollback.params do |p|
|
79
|
+
p.set_type :last_result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
state.process_input Processors::Test.method(:input_processor)
|
83
|
+
state.process_output Processors::Test.method(:processor)
|
84
|
+
end
|
85
|
+
|
86
|
+
add_state.standard :state_04 do |state|
|
87
|
+
state.call Functions::Test.method(:test_func4)
|
88
|
+
state.params do |p|
|
89
|
+
p.set_type :last_result
|
90
|
+
end
|
91
|
+
state.process_input Processors::Test.method(:input_processor)
|
92
|
+
state.process_output Processors::Test.method(:processor)
|
93
|
+
end
|
94
|
+
|
95
|
+
add_state.standard :cond01 do |state|
|
96
|
+
state.call Functions::Test.method(:conditional_test)
|
97
|
+
state.params do |p|
|
98
|
+
p.set_type :last_result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
'''
|
106
|
+
|
107
|
+
### How to define a state
|
108
|
+
|
109
|
+
States can be of two types:
|
110
|
+
1. Standard - Can be hooked to a function and operates in the mode of retriable or pivot transactions
|
111
|
+
2. 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
|
112
|
+
|
113
|
+
Key parameters to define a function:
|
114
|
+
1. .call: <mandatory> -> provide the hook to any function in your project using method() as in the example above
|
115
|
+
2. .params: <mandatory> -> defines the parameter to be provided as input. Define a p.type within the block against state.params. Inputs may be of four types:
|
116
|
+
> 1. input_params: The parameters provided at the time the state engine is invoked. This is explained in the sections ahead
|
117
|
+
> 2. last_result: The result from the last state
|
118
|
+
> 3. direct: Direct inputs where an absolute set of values may be provided.
|
119
|
+
p.type :direct
|
120
|
+
p.input ##input value## // can be anything example [1,2,3] or {x: 3,y: 6} etc
|
121
|
+
> 4. none: No input
|
122
|
+
3. .process_input: In case one wishes to process the input prior to passing to function, this may be added and a function hooked
|
123
|
+
4. .process_output: In case one wishes to process the output post passing to function, this may be added and a function hooked
|
124
|
+
5. .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.
|
125
|
+
|
126
|
+
#### How to sequence states
|
127
|
+
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.
|
128
|
+
|
129
|
+
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.
|
130
|
+
The following is an example of a function that is added to the child class to provide your own sequence
|
131
|
+
|
132
|
+
'''
|
133
|
+
def sequence_states
|
134
|
+
describe_flows do |seqs|
|
135
|
+
|
136
|
+
#this creates the first sequence with name :seq_a
|
137
|
+
seqs.start :seq_a do |seq|
|
138
|
+
seq.init state_name :state_01 #always add an init to start the sequence. Use state_name to invoke a particular registered state
|
139
|
+
seq.then state_name :state_02 #to define the next state.
|
140
|
+
seq.then_conditional state_name :cond01 do |t| #the state defined here has to provide a true or false result
|
141
|
+
t.on_true state_name :state_03
|
142
|
+
t.on_false state_name :state_04
|
143
|
+
end
|
144
|
+
seq.end #closes the sequence
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
'''
|
149
|
+
|
150
|
+
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.
|
151
|
+
To invoke a sub sequence, replace the state_name with > sequence_name :seq_name
|
152
|
+
|
153
|
+
### How to run the state engine
|
154
|
+
If lets say, our child class is defined as Workflows::Test, you can initiate the state engine as follows:
|
155
|
+
'''
|
156
|
+
|
157
|
+
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
|
158
|
+
|
159
|
+
obj.run() #to run the state engine. This will return result with two keys: status and result
|
160
|
+
|
161
|
+
obj.execution_sequence # you can use this to get a hash contains each node that was run and results at each node level
|
162
|
+
|
163
|
+
'''
|
164
|
+
|
165
|
+
state can be:
|
166
|
+
1. :success - completely ran all the way to end
|
167
|
+
2. :error - in case of error in between that halted execution
|
168
|
+
3. :rollback - in case of rollback
|
169
|
+
|
170
|
+
And that is it. You are good to go.
|
171
|
+
|
172
|
+
|
173
|
+
## Tests
|
174
|
+
|
175
|
+
I will be adding some tests soon to the same.
|
176
|
+
|
177
|
+
## Authors
|
178
|
+
|
179
|
+
Contributors names and contact info
|
180
|
+
|
181
|
+
Jayanth Ravindran
|
182
|
+
email: jayanth.ravindran@gmail.com
|
183
|
+
|
184
|
+
## Version History
|
185
|
+
|
186
|
+
* 0.1
|
187
|
+
* Initial Release
|
188
|
+
|
189
|
+
## License
|
190
|
+
|
191
|
+
This project is licensed under the MIT License - see the MIT-LICENSE file for details
|
192
|
+
|
193
|
+
## Acknowledgments
|
194
|
+
|
195
|
+
Inspired from Chris Richardson's articles on managing transactions across services
|
data/examples/main.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'saga_orchestrator'
|
2
|
+
require_relative '../processors/test'
|
3
|
+
require_relative '../functions/test'
|
4
|
+
require_relative '../rollback/test'
|
5
|
+
|
6
|
+
module Workflows
|
7
|
+
class Test < Saga::StateEngine
|
8
|
+
|
9
|
+
def state_registration
|
10
|
+
|
11
|
+
register_states do |add_state|
|
12
|
+
|
13
|
+
add_state.standard :sample do |state|
|
14
|
+
state.call Functions::Test.method(:test_func)
|
15
|
+
state.params do |p|
|
16
|
+
p.set_type :input_params
|
17
|
+
end
|
18
|
+
state.process_output Processors::Test.method(:processor)
|
19
|
+
end
|
20
|
+
|
21
|
+
add_state.standard :sample2 do |state|
|
22
|
+
state.call Functions::Test.method(:test_func2)
|
23
|
+
state.params do |p|
|
24
|
+
p.set_type :last_result
|
25
|
+
end
|
26
|
+
state.process_input Processors::Test.method(:input_processor)
|
27
|
+
state.process_output Processors::Test.method(:processor)
|
28
|
+
end
|
29
|
+
|
30
|
+
add_state.compensatory :sample3 do |state|
|
31
|
+
state.call Functions::Test.method(:test_func3)
|
32
|
+
state.params do |p|
|
33
|
+
p.set_type :last_result
|
34
|
+
end
|
35
|
+
state.rollback_method do |rollback|
|
36
|
+
rollback.call Rollback::Test.method(:rollback2)
|
37
|
+
rollback.params do |p|
|
38
|
+
p.set_type :last_result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
state.process_input Processors::Test.method(:input_processor)
|
42
|
+
state.process_output Processors::Test.method(:processor)
|
43
|
+
end
|
44
|
+
|
45
|
+
add_state.standard :cond01 do |state|
|
46
|
+
state.call Functions::Test.method(:conditional_test)
|
47
|
+
state.params do |p|
|
48
|
+
p.set_type :last_result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def sequence_states
|
57
|
+
describe_flows do |seqs|
|
58
|
+
seqs.start :seq_a do |seq|
|
59
|
+
seq.init state_name :sample
|
60
|
+
seq.then state_name :sample2
|
61
|
+
seq.then_conditional state_name :cond01 do |t|
|
62
|
+
t.on_true state_name :sample
|
63
|
+
t.on_false state_name :sample2
|
64
|
+
end
|
65
|
+
seq.then state_name :sample3
|
66
|
+
seq.end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Functions
|
2
|
+
class Test
|
3
|
+
def self.test_func input
|
4
|
+
puts "in test_func: input - #{input}"
|
5
|
+
res = input + 2
|
6
|
+
puts "result = #{res}"
|
7
|
+
res
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.test_func2 input
|
11
|
+
puts "in test_func2: input - #{input}"
|
12
|
+
res = input - 10
|
13
|
+
puts "result = #{res}"
|
14
|
+
res
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.test_func3 input
|
18
|
+
raise StandardError, "An error occurred"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.conditional_test input
|
22
|
+
res = input > 0
|
23
|
+
puts "result = #{res}"
|
24
|
+
res
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
Binary file
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EnumHelper
|
2
|
+
def enum(name, values)
|
3
|
+
# Define instance variable
|
4
|
+
attr_reader name
|
5
|
+
|
6
|
+
define_method("#{name}=") do |arg1|
|
7
|
+
instance_variable_set("@#{name}",arg1)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Define supporting methods
|
11
|
+
values.each_key do |key|
|
12
|
+
define_method("#{key}?") do
|
13
|
+
instance_variable_get("@#{name}") == key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'state_management/state_manager'
|
2
|
+
require_relative 'state_management/state_engine'
|
3
|
+
module Saga
|
4
|
+
class Orchestrator
|
5
|
+
|
6
|
+
def initialize(cls_state_engine, *args)
|
7
|
+
@args = args
|
8
|
+
obj = cls_state_engine.new(*@args)
|
9
|
+
@state_manager = StateManager::new(obj)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run()
|
13
|
+
@state_manager.run()
|
14
|
+
|
15
|
+
res = {}
|
16
|
+
|
17
|
+
if @state_manager.success?
|
18
|
+
res[:status] = :success
|
19
|
+
res[:result] = @state_manager.result
|
20
|
+
elsif @state_manager.rollback?
|
21
|
+
res[:status] = :rollback
|
22
|
+
res[:result] = @state_manager.result
|
23
|
+
else
|
24
|
+
res[:status] = :error
|
25
|
+
res[:result] = @state_manager.error
|
26
|
+
end
|
27
|
+
|
28
|
+
return res
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def execution_sequence()
|
33
|
+
@state_manager.execution_sequence
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class NodeNotRunError < StandardError
|
2
|
+
attr_reader :details
|
3
|
+
|
4
|
+
def initialize(message, details = nil)
|
5
|
+
super(message)
|
6
|
+
@details = details
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class NodeTargetFunctionMissingError < StandardError
|
11
|
+
attr_reader :details
|
12
|
+
|
13
|
+
def initialize(message, details = nil)
|
14
|
+
super(message)
|
15
|
+
@details = details
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NullStateError < StandardError
|
20
|
+
attr_reader :details
|
21
|
+
|
22
|
+
def initialize(message, details = nil)
|
23
|
+
super(message)
|
24
|
+
@details = details
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class RollbackMethodMissing < StandardError
|
29
|
+
attr_reader :details
|
30
|
+
|
31
|
+
def initialize(message, details = nil)
|
32
|
+
super(message)
|
33
|
+
@details = details
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Saga
|
2
|
+
|
3
|
+
class RunState
|
4
|
+
|
5
|
+
def initialize state_name
|
6
|
+
@state_name = state_name
|
7
|
+
@status = nil
|
8
|
+
@time_of_run = Time.now.gmtime
|
9
|
+
@input = nil
|
10
|
+
@result = nil
|
11
|
+
@error = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize state_name, result
|
15
|
+
@state_name = state_name
|
16
|
+
@status = nil
|
17
|
+
@time_of_run = Time.now.gmtime
|
18
|
+
@result = nil
|
19
|
+
@error = nil
|
20
|
+
|
21
|
+
update(result)
|
22
|
+
end
|
23
|
+
|
24
|
+
def update result
|
25
|
+
@status = result&.[](:status)
|
26
|
+
|
27
|
+
case @status
|
28
|
+
when :success
|
29
|
+
@result = result[:result]
|
30
|
+
when :error
|
31
|
+
@error = result[:message]
|
32
|
+
when :rollback
|
33
|
+
@result = result[:result]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def outcome
|
38
|
+
outcome = ''
|
39
|
+
|
40
|
+
case @status
|
41
|
+
when :success
|
42
|
+
outcome = @result
|
43
|
+
when :error
|
44
|
+
outcome = @error
|
45
|
+
when :rollback
|
46
|
+
outcome = @result
|
47
|
+
end
|
48
|
+
|
49
|
+
outcome
|
50
|
+
end
|
51
|
+
|
52
|
+
def unwrap
|
53
|
+
{
|
54
|
+
state_name: @state_name,
|
55
|
+
status: @status,
|
56
|
+
time_of_run: @time_of_run,
|
57
|
+
outcome: outcome()
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class RunStates
|
64
|
+
def initialize
|
65
|
+
@run_states = []
|
66
|
+
end
|
67
|
+
|
68
|
+
def push state_name, result
|
69
|
+
@run_states.push(RunState::new(state_name,result))
|
70
|
+
end
|
71
|
+
|
72
|
+
def dump
|
73
|
+
results = {}
|
74
|
+
ctr = 0
|
75
|
+
@run_states.each do |rs|
|
76
|
+
results[ctr] = rs.unwrap()
|
77
|
+
ctr += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
results
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
require_relative './custom_exceptions/exceptions'
|
2
|
+
require_relative '../transactions/transaction'
|
3
|
+
require_relative '../helpers/enum_helper'
|
4
|
+
|
5
|
+
module Saga
|
6
|
+
|
7
|
+
class SequenceNode
|
8
|
+
extend EnumHelper
|
9
|
+
|
10
|
+
enum :type, { process: 0, conditional: 1, stop: 2 }
|
11
|
+
enum :action_type, {transaction:0, sequence:1}
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@name = nil
|
15
|
+
self.type = :process
|
16
|
+
self.action_type = :transaction
|
17
|
+
@do = nil
|
18
|
+
@parent_node = nil
|
19
|
+
@on_success_go = nil
|
20
|
+
@on_false_go = nil
|
21
|
+
@run_state = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def run params = nil, last_state_result=nil
|
25
|
+
|
26
|
+
if self.type == :stop
|
27
|
+
return {status: :close}
|
28
|
+
end
|
29
|
+
|
30
|
+
if @do.nil?
|
31
|
+
raise NodeTargetFunctionMissingError.new("Node has no target task.", { reason: "State node has no transaction task.", code: 1001 })
|
32
|
+
end
|
33
|
+
|
34
|
+
if @do.is_a?(Transactions::Transaction)
|
35
|
+
res = @do.execute(params, last_state_result)
|
36
|
+
@run_state = true
|
37
|
+
@run_state = false if !res&.[](:result) && self.type == :conditional
|
38
|
+
return res
|
39
|
+
end
|
40
|
+
|
41
|
+
return nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def next
|
45
|
+
|
46
|
+
if @run_state.nil?
|
47
|
+
raise NodeNotRunError.new("Node has not been run yet.", { reason: "Next can only be decided after node has been run.", code: 1001 })
|
48
|
+
end
|
49
|
+
|
50
|
+
if @run_state == true
|
51
|
+
return @on_success_go
|
52
|
+
else
|
53
|
+
return @on_false_go
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_parent_node node
|
58
|
+
@parent_node = node
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_do task
|
62
|
+
|
63
|
+
if task.nil?
|
64
|
+
raise NullStateError.new("State not defined.", { reason: "State has not been defined or registered.", code: 1001 })
|
65
|
+
end
|
66
|
+
|
67
|
+
if task.is_a?(Sequence)
|
68
|
+
@action_type = :sequence
|
69
|
+
end
|
70
|
+
@do = task
|
71
|
+
@name = task.state_name
|
72
|
+
end
|
73
|
+
|
74
|
+
def name
|
75
|
+
@name
|
76
|
+
end
|
77
|
+
|
78
|
+
def set_type sym_type
|
79
|
+
self.type = sym_type
|
80
|
+
end
|
81
|
+
|
82
|
+
def point_to node
|
83
|
+
if node.is_a?(Sequence)
|
84
|
+
@on_success_go = node.first
|
85
|
+
else
|
86
|
+
@on_success_go = node
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_parent node
|
91
|
+
@parent_node = node
|
92
|
+
end
|
93
|
+
|
94
|
+
def has_on_fail_pointer?
|
95
|
+
!@on_false_go.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
def has_on_success_pointer?
|
99
|
+
!@on_success_go.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def on_fail_point_to node
|
103
|
+
if node.is_a?(Sequence)
|
104
|
+
@on_false_go = node.first
|
105
|
+
else
|
106
|
+
@on_false_go = node
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def activity
|
111
|
+
@do
|
112
|
+
end
|
113
|
+
|
114
|
+
def on_true activity
|
115
|
+
|
116
|
+
seq_node = SequenceNode::new()
|
117
|
+
seq_node.set_do activity
|
118
|
+
|
119
|
+
self.point_to seq_node
|
120
|
+
|
121
|
+
seq_node.set_parent_node self
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
def on_false activity
|
126
|
+
|
127
|
+
seq_node = SequenceNode::new()
|
128
|
+
seq_node.set_do activity
|
129
|
+
|
130
|
+
self.on_fail_point_to seq_node
|
131
|
+
|
132
|
+
seq_node.set_parent_node self
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_conditional &block
|
137
|
+
block.call(self) if block_given?
|
138
|
+
|
139
|
+
return_nodes = []
|
140
|
+
return_nodes.push(@on_success_go) if !@on_success_go.nil?
|
141
|
+
return_nodes.push(@on_false_go) if !@on_false_go.nil?
|
142
|
+
return_nodes
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
class Sequence
|
148
|
+
|
149
|
+
def initialize name
|
150
|
+
@name = name
|
151
|
+
@prev_node = []
|
152
|
+
@initial_node = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def first
|
156
|
+
@initial_node
|
157
|
+
end
|
158
|
+
|
159
|
+
def init task
|
160
|
+
seq_node = SequenceNode::new()
|
161
|
+
seq_node.set_do task
|
162
|
+
@prev_node.push(seq_node)
|
163
|
+
@initial_node = seq_node
|
164
|
+
end
|
165
|
+
|
166
|
+
def end
|
167
|
+
seq_node = SequenceNode::new()
|
168
|
+
seq_node.set_type :stop
|
169
|
+
|
170
|
+
begin
|
171
|
+
@prev_node.each do |nd|
|
172
|
+
nd.point_to seq_node
|
173
|
+
end
|
174
|
+
seq_node.set_parent_node @prev_node
|
175
|
+
@prev_node.clear
|
176
|
+
@prev_node.push(seq_node)
|
177
|
+
rescue NoMethodError => e
|
178
|
+
puts "Within End Error : #{e.message}"
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
def then task
|
184
|
+
seq_node = SequenceNode::new()
|
185
|
+
seq_node.set_do task
|
186
|
+
|
187
|
+
begin
|
188
|
+
@prev_node.each do |nd|
|
189
|
+
nd.point_to seq_node
|
190
|
+
end
|
191
|
+
seq_node.set_parent_node @prev_node
|
192
|
+
@prev_node.clear
|
193
|
+
@prev_node.push(seq_node)
|
194
|
+
rescue NoMethodError => e
|
195
|
+
puts "Within Then Error : #{e.message}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def then_conditional task, &block
|
200
|
+
seq_node = SequenceNode::new()
|
201
|
+
seq_node.set_do task
|
202
|
+
seq_node.set_type :conditional
|
203
|
+
|
204
|
+
begin
|
205
|
+
@prev_node.each do |nd|
|
206
|
+
nd.point_to seq_node
|
207
|
+
end
|
208
|
+
seq_node.set_parent_node @prev_node
|
209
|
+
|
210
|
+
@prev_node = seq_node.on_conditional(&block)
|
211
|
+
|
212
|
+
rescue NoMethodError => e
|
213
|
+
puts "Within Then Conditional Error : #{e.message}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
class Sequences
|
220
|
+
|
221
|
+
def initialize
|
222
|
+
@sequences = {}
|
223
|
+
@active_sequence = nil
|
224
|
+
@start = nil
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_sequence_by_name name
|
228
|
+
@sequences[name]
|
229
|
+
end
|
230
|
+
|
231
|
+
def first_node
|
232
|
+
@start.first
|
233
|
+
end
|
234
|
+
|
235
|
+
def start name, &block
|
236
|
+
seq = Sequence::new(name)
|
237
|
+
@sequences[name] = seq
|
238
|
+
@active_sequence = seq
|
239
|
+
@start = seq
|
240
|
+
block.call(seq) if block_given?
|
241
|
+
end
|
242
|
+
|
243
|
+
#sub sequence of a sequence
|
244
|
+
def sub name, &block
|
245
|
+
seq = Sequence::new(name)
|
246
|
+
@sequences[name] = seq
|
247
|
+
@active_sequence = seq
|
248
|
+
block.call(seq) if block_given?
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require_relative './custom_exceptions/exceptions'
|
2
|
+
require_relative './states'
|
3
|
+
require_relative './sequences'
|
4
|
+
require_relative './run_states'
|
5
|
+
require_relative '../helpers/enum_helper'
|
6
|
+
|
7
|
+
module Saga
|
8
|
+
|
9
|
+
class StateEngine
|
10
|
+
extend EnumHelper
|
11
|
+
|
12
|
+
enum :run_status, { success: 0, fail: 1, rollback: 2 }
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
@params = args
|
16
|
+
@states = States::new()
|
17
|
+
@sequences = Sequences::new()
|
18
|
+
@last_result = nil
|
19
|
+
@complete = false
|
20
|
+
self.run_status = :success
|
21
|
+
@error = nil
|
22
|
+
@executing_node = nil
|
23
|
+
@run_states = RunStates::new()
|
24
|
+
end
|
25
|
+
|
26
|
+
def register_states(&block)
|
27
|
+
block.call(@states) if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def describe_flows(&block)
|
31
|
+
@sequences = Sequences::new()
|
32
|
+
block.call(@sequences) if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
def state_name name
|
36
|
+
@states.get_state_by_name name
|
37
|
+
end
|
38
|
+
|
39
|
+
def sequence_name name
|
40
|
+
@sequences.get_sequence_by_name name
|
41
|
+
end
|
42
|
+
|
43
|
+
def sequence_states
|
44
|
+
keys = @states.keys
|
45
|
+
return if keys.size == 0
|
46
|
+
|
47
|
+
describe_flows do |seqs|
|
48
|
+
seqs.start :base do |seq|
|
49
|
+
keys.each_with_index do |state_name, index|
|
50
|
+
if index ==0
|
51
|
+
seq.init state_name state_name
|
52
|
+
else
|
53
|
+
seq.then state_name state_name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
seq.end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
|
63
|
+
@executing_node = @sequences.first_node
|
64
|
+
|
65
|
+
if @executing_node.nil?
|
66
|
+
raise NullStateError.new("Current sequence has no start node.", { reason: "Sequence not initialised before run.", code: 1001 })
|
67
|
+
else
|
68
|
+
conduct_sequence @executing_node
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
def result
|
74
|
+
@last_result
|
75
|
+
end
|
76
|
+
|
77
|
+
def error
|
78
|
+
@error
|
79
|
+
end
|
80
|
+
|
81
|
+
def execution_sequence
|
82
|
+
@run_states.dump
|
83
|
+
end
|
84
|
+
|
85
|
+
def conduct_sequence active_node
|
86
|
+
current_node = active_node
|
87
|
+
|
88
|
+
begin
|
89
|
+
if current_node.is_a?(Sequence)
|
90
|
+
conduct_sequence current_node.activity.first
|
91
|
+
else
|
92
|
+
@executing_node = current_node
|
93
|
+
res = current_node.run @params, @last_result
|
94
|
+
|
95
|
+
@run_states.push(@executing_node.name, res)
|
96
|
+
|
97
|
+
execution_status = res&.[](:status)
|
98
|
+
if execution_status == :close
|
99
|
+
@complete = true
|
100
|
+
return
|
101
|
+
elsif execution_status == :rollback
|
102
|
+
self.run_status = :rollback
|
103
|
+
@complete = true
|
104
|
+
elsif execution_status == :error
|
105
|
+
@complete = true
|
106
|
+
self.run_status = :fail
|
107
|
+
@error = res&.[](:message)
|
108
|
+
return
|
109
|
+
end
|
110
|
+
@last_result = res&.[](:result) if !current_node.conditional? #conditional nodes gives true / false responses which will not be considered as results
|
111
|
+
end
|
112
|
+
|
113
|
+
current_node = current_node.next
|
114
|
+
|
115
|
+
if current_node.nil?
|
116
|
+
@complete = true
|
117
|
+
end
|
118
|
+
|
119
|
+
end while !current_node.nil? && !@complete
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Saga
|
2
|
+
|
3
|
+
class StateManager
|
4
|
+
|
5
|
+
def initialize obj_state_engine
|
6
|
+
@state_engine = obj_state_engine
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
@state_engine.state_registration
|
11
|
+
@state_engine.sequence_states
|
12
|
+
@state_engine.run
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
@state_engine.success?
|
17
|
+
end
|
18
|
+
|
19
|
+
def rollback?
|
20
|
+
@state_engine.rollback?
|
21
|
+
end
|
22
|
+
|
23
|
+
def result
|
24
|
+
@state_engine.result
|
25
|
+
end
|
26
|
+
|
27
|
+
def error
|
28
|
+
@state_engine.error
|
29
|
+
end
|
30
|
+
|
31
|
+
def execution_sequence
|
32
|
+
@state_engine.execution_sequence
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../transactions/retriable'
|
2
|
+
require_relative '../transactions/compensatory'
|
3
|
+
require_relative './custom_exceptions/exceptions'
|
4
|
+
|
5
|
+
module Saga
|
6
|
+
|
7
|
+
class States
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@states = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def standard name, &block
|
14
|
+
transac = Transactions::RetriableTransaction::new(name)
|
15
|
+
block.call(transac) if block_given?
|
16
|
+
@states[name] = transac
|
17
|
+
end
|
18
|
+
|
19
|
+
def compensatory name, &block
|
20
|
+
transac = Transactions::CompensatoryTransaction::new(name)
|
21
|
+
block.call(transac) if block_given?
|
22
|
+
if !transac.valid?
|
23
|
+
raise RollbackMethodMissing.new("Rollback method missing for compensatory state", { reason: "Add rollback method for compensatory state", code: 1001 })
|
24
|
+
end
|
25
|
+
@states[name] = transac
|
26
|
+
end
|
27
|
+
|
28
|
+
def sequence_states(&block)
|
29
|
+
block.call(@sequences) if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_state_by_name name
|
33
|
+
@states[name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def keys
|
37
|
+
@states.keys
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative './rollback_method'
|
2
|
+
require_relative './transaction'
|
3
|
+
|
4
|
+
module Transactions
|
5
|
+
|
6
|
+
class CompensatoryTransaction < Transaction
|
7
|
+
|
8
|
+
def initialize name
|
9
|
+
super(name)
|
10
|
+
@rollback = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
!(
|
15
|
+
@rollback.nil?
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def rollback_method &block
|
20
|
+
@rollback = Transactions::RollbackMethod::new()
|
21
|
+
block.call(@rollback) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_error_action e
|
25
|
+
begin
|
26
|
+
case @parameters.type
|
27
|
+
when :direct
|
28
|
+
input = @parameters.input
|
29
|
+
res = @rollback.method.call(input)
|
30
|
+
when :last_result
|
31
|
+
input = @last_result
|
32
|
+
res = @rollback.method.call(input)
|
33
|
+
else
|
34
|
+
res = @rollback.method.call(*@input_params)
|
35
|
+
end
|
36
|
+
|
37
|
+
@result = {
|
38
|
+
status: :rollback,
|
39
|
+
result: res
|
40
|
+
}
|
41
|
+
|
42
|
+
return @result
|
43
|
+
rescue Exception => e
|
44
|
+
error_message = "Error in rollback at state (#{@name}) : #{e.message}"
|
45
|
+
@error = {
|
46
|
+
status: :error,
|
47
|
+
message: error_message
|
48
|
+
}
|
49
|
+
return @error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../helpers/enum_helper'
|
2
|
+
|
3
|
+
|
4
|
+
module Transactions
|
5
|
+
|
6
|
+
class Parameters
|
7
|
+
extend EnumHelper
|
8
|
+
|
9
|
+
enum :type, { input_params: 0, last_result: 1, direct:2, none: 3}
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.type = :input_params
|
13
|
+
@params = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_type val
|
17
|
+
self.type = val
|
18
|
+
end
|
19
|
+
|
20
|
+
def input val
|
21
|
+
@params = val
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative './parameters'
|
2
|
+
module Transactions
|
3
|
+
|
4
|
+
class RollbackMethod
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@method_to_call = nil
|
8
|
+
@params = Parameters::new()
|
9
|
+
end
|
10
|
+
|
11
|
+
def call func_name
|
12
|
+
@method_to_call = func_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def method
|
16
|
+
@method_to_call
|
17
|
+
end
|
18
|
+
|
19
|
+
def params &block
|
20
|
+
block.call(@params) if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative './parameters'
|
2
|
+
require_relative './transaction'
|
3
|
+
|
4
|
+
module Transactions
|
5
|
+
class Transaction
|
6
|
+
|
7
|
+
def initialize name
|
8
|
+
@name = name
|
9
|
+
@method_to_call = nil
|
10
|
+
@parameters = nil
|
11
|
+
@input_processing_function = nil
|
12
|
+
@output_processing_function = nil
|
13
|
+
@input_params = nil
|
14
|
+
@last_result = nil
|
15
|
+
@result = nil
|
16
|
+
@error = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def call func_name
|
20
|
+
@method_to_call = func_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def state_name
|
24
|
+
@name
|
25
|
+
end
|
26
|
+
|
27
|
+
def params &block
|
28
|
+
obj_param = Parameters::new()
|
29
|
+
block.call(obj_param) if block_given?
|
30
|
+
@parameters = obj_param
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_input process_func
|
34
|
+
@input_processing_function = process_func
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_output process_func
|
38
|
+
@output_processing_function = process_func
|
39
|
+
end
|
40
|
+
|
41
|
+
def param_type
|
42
|
+
@parameters.type
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_error_action e
|
46
|
+
|
47
|
+
@error = {
|
48
|
+
status: :error,
|
49
|
+
message: "Error in processing at state (#{@name}): #{e.message}"
|
50
|
+
}
|
51
|
+
|
52
|
+
@error
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute input_params, last_state_result
|
57
|
+
|
58
|
+
res = nil
|
59
|
+
input = nil
|
60
|
+
|
61
|
+
@input_params = input_params
|
62
|
+
@last_result = last_state_result
|
63
|
+
|
64
|
+
begin
|
65
|
+
case @parameters.type
|
66
|
+
when :direct
|
67
|
+
input = @input_processing_function.nil? ? @parameters.input : @input_processing_function.call(@parameters.input)
|
68
|
+
res = @method_to_call.call(input)
|
69
|
+
when :last_result
|
70
|
+
input = @input_processing_function.nil? ? @last_result : @input_processing_function.call(@last_result)
|
71
|
+
res = @method_to_call.call(input)
|
72
|
+
when :none
|
73
|
+
res = @method_to_call.call()
|
74
|
+
else
|
75
|
+
if @input_processing_function.nil?
|
76
|
+
res = @method_to_call.call(*@input_params)
|
77
|
+
else
|
78
|
+
res = @method_to_call.call(@input_processing_function.call(*@input_params))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if !@output_processing_function.nil?
|
83
|
+
res = @output_processing_function.call(res)
|
84
|
+
end
|
85
|
+
|
86
|
+
@result = {
|
87
|
+
status: :success,
|
88
|
+
result: res
|
89
|
+
}
|
90
|
+
|
91
|
+
return @result
|
92
|
+
rescue Exception => e
|
93
|
+
return on_error_action(e)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: saga_orchestrator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.15'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jayanth Ravindran
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Framework to help employ the Saga Orchestration patterns in ruby or rails
|
14
|
+
applications. Secondly, it makes it easier for firms to visualize the entire flow
|
15
|
+
as a set of steps.
|
16
|
+
email: jayanth.ravindran@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- MIT-LICENSE
|
22
|
+
- Readme.md
|
23
|
+
- examples/main.rb
|
24
|
+
- examples/workflow_models/definitions/test.rb
|
25
|
+
- examples/workflow_models/functions/test.rb
|
26
|
+
- examples/workflow_models/processors/test.rb
|
27
|
+
- examples/workflow_models/rollback/test.rb
|
28
|
+
- images/flow_chart.png
|
29
|
+
- lib/helpers/enum_helper.rb
|
30
|
+
- lib/saga_orchestrator.rb
|
31
|
+
- lib/state_management/custom_exceptions/exceptions.rb
|
32
|
+
- lib/state_management/run_states.rb
|
33
|
+
- lib/state_management/sequences.rb
|
34
|
+
- lib/state_management/state_engine.rb
|
35
|
+
- lib/state_management/state_manager.rb
|
36
|
+
- lib/state_management/states.rb
|
37
|
+
- lib/transactions/compensatory.rb
|
38
|
+
- lib/transactions/parameters.rb
|
39
|
+
- lib/transactions/retriable.rb
|
40
|
+
- lib/transactions/rollback_method.rb
|
41
|
+
- lib/transactions/transaction.rb
|
42
|
+
homepage: https://rubygems.org/gems/saga_orchestrator
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.5.9
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Describe orchestration workflow as definitions and run within a state engine
|
65
|
+
test_files: []
|