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 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
+ ![workflow](images/flow_chart.png)
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,6 @@
1
+ require 'saga_orchestrator'
2
+ require_relative 'workflow_models/definitions/test'
3
+
4
+ obj = Saga::Orchestrator::new(Workflows::Test, 30)
5
+ puts obj.run()
6
+ puts obj.execution_sequence
@@ -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
@@ -0,0 +1,11 @@
1
+ module Processors
2
+ class Test
3
+ def self.processor input
4
+ input * 2
5
+ end
6
+
7
+ def self.input_processor input
8
+ input / 4
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module Rollback
2
+ class Test
3
+ def self.rollback input
4
+ input * 100000
5
+ end
6
+
7
+ def self.rollback2 input
8
+ raise StandardError, "Rollback error occurred"
9
+ end
10
+
11
+ end
12
+ 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,11 @@
1
+ # require_relative './parameters'
2
+ require_relative './transaction'
3
+
4
+ module Transactions
5
+
6
+ class RetriableTransaction < Transaction
7
+
8
+
9
+ end
10
+
11
+ 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: []