ruby_reactor 0.1.0
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/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/README.md +570 -0
- data/Rakefile +12 -0
- data/documentation/DAG.md +457 -0
- data/documentation/README.md +123 -0
- data/documentation/async_reactors.md +369 -0
- data/documentation/composition.md +199 -0
- data/documentation/core_concepts.md +662 -0
- data/documentation/data_pipelines.md +224 -0
- data/documentation/examples/inventory_management.md +749 -0
- data/documentation/examples/order_processing.md +365 -0
- data/documentation/examples/payment_processing.md +654 -0
- data/documentation/getting_started.md +224 -0
- data/documentation/retry_configuration.md +357 -0
- data/lib/ruby_reactor/async_router.rb +91 -0
- data/lib/ruby_reactor/configuration.rb +41 -0
- data/lib/ruby_reactor/context.rb +169 -0
- data/lib/ruby_reactor/context_serializer.rb +164 -0
- data/lib/ruby_reactor/dependency_graph.rb +126 -0
- data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
- data/lib/ruby_reactor/dsl/reactor.rb +151 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
- data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
- data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
- data/lib/ruby_reactor/error/base.rb +16 -0
- data/lib/ruby_reactor/error/compensation_error.rb +8 -0
- data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
- data/lib/ruby_reactor/error/dependency_error.rb +8 -0
- data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
- data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
- data/lib/ruby_reactor/error/undo_error.rb +8 -0
- data/lib/ruby_reactor/error/validation_error.rb +8 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
- data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
- data/lib/ruby_reactor/executor/input_validator.rb +39 -0
- data/lib/ruby_reactor/executor/result_handler.rb +103 -0
- data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
- data/lib/ruby_reactor/executor/step_executor.rb +319 -0
- data/lib/ruby_reactor/executor.rb +123 -0
- data/lib/ruby_reactor/map/collector.rb +65 -0
- data/lib/ruby_reactor/map/element_executor.rb +154 -0
- data/lib/ruby_reactor/map/execution.rb +60 -0
- data/lib/ruby_reactor/map/helpers.rb +67 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
- data/lib/ruby_reactor/reactor.rb +75 -0
- data/lib/ruby_reactor/retry_context.rb +92 -0
- data/lib/ruby_reactor/retry_queued_result.rb +26 -0
- data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
- data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
- data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
- data/lib/ruby_reactor/step/compose_step.rb +107 -0
- data/lib/ruby_reactor/step/map_step.rb +234 -0
- data/lib/ruby_reactor/step.rb +33 -0
- data/lib/ruby_reactor/storage/adapter.rb +51 -0
- data/lib/ruby_reactor/storage/configuration.rb +15 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
- data/lib/ruby_reactor/template/base.rb +15 -0
- data/lib/ruby_reactor/template/element.rb +25 -0
- data/lib/ruby_reactor/template/input.rb +48 -0
- data/lib/ruby_reactor/template/result.rb +48 -0
- data/lib/ruby_reactor/template/value.rb +22 -0
- data/lib/ruby_reactor/validation/base.rb +26 -0
- data/lib/ruby_reactor/validation/input_validator.rb +62 -0
- data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
- data/lib/ruby_reactor/version.rb +5 -0
- data/lib/ruby_reactor.rb +159 -0
- data/sig/ruby_reactor.rbs +4 -0
- metadata +178 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
module Reactor
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
base.instance_variable_set(:@inputs, {})
|
|
9
|
+
base.instance_variable_set(:@steps, {})
|
|
10
|
+
base.instance_variable_set(:@return_step, nil)
|
|
11
|
+
base.instance_variable_set(:@middlewares, [])
|
|
12
|
+
base.instance_variable_set(:@input_validations, {})
|
|
13
|
+
base.instance_variable_set(:@async, false)
|
|
14
|
+
base.instance_variable_set(:@retry_defaults, { max_attempts: 3, backoff: :exponential, base_delay: 1 })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
include RubyReactor::Dsl::TemplateHelpers
|
|
19
|
+
include RubyReactor::Dsl::ValidationHelpers
|
|
20
|
+
|
|
21
|
+
def inputs
|
|
22
|
+
@inputs ||= {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def steps
|
|
26
|
+
@steps ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def return_step
|
|
30
|
+
@return_step
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def middlewares
|
|
34
|
+
@middlewares ||= []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def input_validations
|
|
38
|
+
@input_validations ||= {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def async(async = true)
|
|
42
|
+
@async = async
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def async?
|
|
46
|
+
@async ||= false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def retry_defaults(**kwargs)
|
|
50
|
+
if kwargs.empty?
|
|
51
|
+
@retry_defaults ||= { max_attempts: 1, backoff: :exponential, base_delay: 1 }
|
|
52
|
+
else
|
|
53
|
+
@retry_defaults = {
|
|
54
|
+
max_attempts: kwargs[:max_attempts] || 1,
|
|
55
|
+
backoff: kwargs[:backoff] || :exponential,
|
|
56
|
+
base_delay: kwargs[:base_delay] || 1
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# rubocop:disable Metrics/ParameterLists
|
|
62
|
+
def input(name, transform: nil, description: nil, validate: nil, optional: false, redact: false,
|
|
63
|
+
&validation_block)
|
|
64
|
+
# rubocop:enable Metrics/ParameterLists
|
|
65
|
+
inputs[name] = {
|
|
66
|
+
transform: transform,
|
|
67
|
+
description: description,
|
|
68
|
+
optional: optional,
|
|
69
|
+
redact: redact
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Handle validation
|
|
73
|
+
return unless validate || validation_block
|
|
74
|
+
|
|
75
|
+
validator = create_input_validator(validation_block || validate)
|
|
76
|
+
input_validations[name] = validator
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def step(name, impl = nil, &block)
|
|
80
|
+
builder = RubyReactor::Dsl::StepBuilder.new(name, impl, self)
|
|
81
|
+
|
|
82
|
+
builder.instance_eval(&block) if block_given?
|
|
83
|
+
|
|
84
|
+
step_config = builder.build
|
|
85
|
+
steps[name] = step_config
|
|
86
|
+
step_config
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def compose(name, composed_reactor_class = nil, &block)
|
|
90
|
+
builder = RubyReactor::Dsl::ComposeBuilder.new(name, composed_reactor_class, self, &block)
|
|
91
|
+
|
|
92
|
+
builder.instance_eval(&block) if block_given?
|
|
93
|
+
|
|
94
|
+
step_config = builder.build
|
|
95
|
+
steps[name] = step_config
|
|
96
|
+
step_config
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def map(name, reactor_class = nil, &block)
|
|
100
|
+
builder = RubyReactor::Dsl::MapBuilder.new(name, reactor_class, self, &block)
|
|
101
|
+
|
|
102
|
+
builder.instance_eval(&block) if block_given?
|
|
103
|
+
|
|
104
|
+
step_config = builder.build
|
|
105
|
+
steps[name] = step_config
|
|
106
|
+
step_config
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def returns(step_name)
|
|
110
|
+
@return_step = step_name
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def middleware(middleware_class)
|
|
114
|
+
middlewares << middleware_class
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_inputs(inputs_hash)
|
|
118
|
+
errors = {}
|
|
119
|
+
|
|
120
|
+
input_validations.each do |input_name, validator|
|
|
121
|
+
# Skip validation if input is optional and not provided
|
|
122
|
+
next if inputs[input_name][:optional] && !inputs_hash.key?(input_name)
|
|
123
|
+
|
|
124
|
+
input_data = inputs_hash[input_name]
|
|
125
|
+
# Validate by wrapping the individual input in a hash with its name
|
|
126
|
+
result = validator.call({ input_name => input_data })
|
|
127
|
+
|
|
128
|
+
errors.merge!(result.error.field_errors) if result.failure? && result.error.respond_to?(:field_errors)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if errors.empty?
|
|
132
|
+
RubyReactor.Success(inputs_hash)
|
|
133
|
+
else
|
|
134
|
+
error = RubyReactor::Error::InputValidationError.new(errors)
|
|
135
|
+
RubyReactor.Failure(error)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Entry point for running the reactor
|
|
140
|
+
def run(inputs = {})
|
|
141
|
+
reactor = new
|
|
142
|
+
reactor.run(inputs)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def call(inputs = {})
|
|
146
|
+
run(inputs)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
class StepBuilder
|
|
6
|
+
include RubyReactor::Dsl::TemplateHelpers
|
|
7
|
+
include RubyReactor::Dsl::ValidationHelpers
|
|
8
|
+
|
|
9
|
+
attr_accessor :name, :impl, :arguments, :run_block, :compensate_block, :undo_block, :conditions, :guards,
|
|
10
|
+
:dependencies, :args_validator, :output_validator, :retry_config
|
|
11
|
+
|
|
12
|
+
def initialize(name, impl = nil, reactor = nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@impl = impl
|
|
15
|
+
@reactor = reactor
|
|
16
|
+
@arguments = {}
|
|
17
|
+
@run_block = nil
|
|
18
|
+
@compensate_block = nil
|
|
19
|
+
@undo_block = nil
|
|
20
|
+
@conditions = []
|
|
21
|
+
@guards = []
|
|
22
|
+
@dependencies = []
|
|
23
|
+
@args_validator = nil
|
|
24
|
+
@output_validator = nil
|
|
25
|
+
@async = false
|
|
26
|
+
@retry_config = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def argument(name, source, transform: nil)
|
|
30
|
+
@arguments[name] = {
|
|
31
|
+
source: source,
|
|
32
|
+
transform: transform
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(&block)
|
|
37
|
+
@run_block = block
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def compensate(&block)
|
|
41
|
+
@compensate_block = block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def undo(&block)
|
|
45
|
+
@undo_block = block
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def where(&predicate)
|
|
49
|
+
@conditions << predicate
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def guard(&guard_fn)
|
|
53
|
+
@guards << guard_fn
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wait_for(*step_names)
|
|
57
|
+
@dependencies.concat(step_names)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_args(schema_or_validator = nil, &block)
|
|
61
|
+
if block_given?
|
|
62
|
+
@args_validator = build_input_validator(block)
|
|
63
|
+
elsif schema_or_validator
|
|
64
|
+
@args_validator = build_input_validator(schema_or_validator)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_output(schema_or_validator = nil, &block)
|
|
69
|
+
if block_given?
|
|
70
|
+
@output_validator = build_input_validator(block)
|
|
71
|
+
elsif schema_or_validator
|
|
72
|
+
@output_validator = build_input_validator(schema_or_validator)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def async(async = true)
|
|
77
|
+
@async = async
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def retries(max_attempts: 3, backoff: :exponential, base_delay: 1)
|
|
81
|
+
@retry_config = {
|
|
82
|
+
max_attempts: max_attempts,
|
|
83
|
+
backoff: backoff,
|
|
84
|
+
base_delay: base_delay
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build
|
|
89
|
+
step_config = {
|
|
90
|
+
name: @name,
|
|
91
|
+
impl: @impl,
|
|
92
|
+
arguments: @arguments,
|
|
93
|
+
run_block: @run_block,
|
|
94
|
+
compensate_block: @compensate_block,
|
|
95
|
+
undo_block: @undo_block,
|
|
96
|
+
conditions: @conditions,
|
|
97
|
+
guards: @guards,
|
|
98
|
+
dependencies: @dependencies,
|
|
99
|
+
args_validator: @args_validator,
|
|
100
|
+
output_validator: @output_validator,
|
|
101
|
+
async: @async,
|
|
102
|
+
retry_config: @retry_config.empty? ? (@reactor&.retry_defaults || {}) : @retry_config
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RubyReactor::Dsl::StepConfig.new(step_config)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def build_input_validator(schema_or_block)
|
|
111
|
+
check_dry_validation_available!
|
|
112
|
+
|
|
113
|
+
schema = case schema_or_block
|
|
114
|
+
when Proc
|
|
115
|
+
build_validation_schema(&schema_or_block)
|
|
116
|
+
else
|
|
117
|
+
schema_or_block
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
RubyReactor::Validation::InputValidator.new(schema)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_validation_schema(&block)
|
|
124
|
+
RubyReactor::Validation::SchemaBuilder.build_from_block(&block)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def check_dry_validation_available!
|
|
128
|
+
return if defined?(Dry::Schema)
|
|
129
|
+
|
|
130
|
+
raise LoadError,
|
|
131
|
+
"dry-validation gem is required for validation features. Add 'gem \"dry-validation\"' to your Gemfile."
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class StepConfig
|
|
136
|
+
attr_reader :name, :impl, :arguments, :run_block, :compensate_block, :undo_block, :conditions, :guards,
|
|
137
|
+
:dependencies, :args_validator, :output_validator, :async, :retry_config
|
|
138
|
+
|
|
139
|
+
def initialize(config)
|
|
140
|
+
@name = config[:name]
|
|
141
|
+
@impl = config[:impl]
|
|
142
|
+
@arguments = config[:arguments] || {}
|
|
143
|
+
@run_block = config[:run_block]
|
|
144
|
+
@compensate_block = config[:compensate_block]
|
|
145
|
+
@undo_block = config[:undo_block]
|
|
146
|
+
@conditions = config[:conditions] || []
|
|
147
|
+
@guards = config[:guards] || []
|
|
148
|
+
@dependencies = config[:dependencies] || []
|
|
149
|
+
@args_validator = config[:args_validator]
|
|
150
|
+
@output_validator = config[:output_validator]
|
|
151
|
+
@async = config[:async] || false
|
|
152
|
+
@retry_config = { max_attempts: 1 }.merge(config[:retry_config] || {})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def has_impl?
|
|
156
|
+
!@impl.nil?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def has_run_block?
|
|
160
|
+
!@run_block.nil?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def async?
|
|
164
|
+
@async
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def retryable?
|
|
168
|
+
(retry_config[:max_attempts] || 0) > 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def should_run?(context)
|
|
172
|
+
@conditions.all? { |condition| condition.call(context) } &&
|
|
173
|
+
@guards.all? { |guard| guard.call(context) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
module TemplateHelpers
|
|
6
|
+
def input(name, path = nil)
|
|
7
|
+
RubyReactor::Template::Input.new(name, path)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def result(step_name, path = nil)
|
|
11
|
+
RubyReactor::Template::Result.new(step_name, path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def value(val)
|
|
15
|
+
RubyReactor::Template::Value.new(val)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def element(map_name, path = nil)
|
|
19
|
+
RubyReactor::Template::Element.new(map_name, path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Make Success and Failure available in DSL contexts
|
|
23
|
+
# rubocop:disable Naming/MethodName
|
|
24
|
+
def Success(value = nil)
|
|
25
|
+
# rubocop:enable Naming/MethodName
|
|
26
|
+
RubyReactor.Success(value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# rubocop:disable Naming/MethodName
|
|
30
|
+
def Failure(error)
|
|
31
|
+
# rubocop:enable Naming/MethodName
|
|
32
|
+
RubyReactor.Failure(error)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
module ValidationHelpers
|
|
6
|
+
# Validation helper methods
|
|
7
|
+
def build_validation_schema(&block)
|
|
8
|
+
check_dry_validation_available!
|
|
9
|
+
RubyReactor::Validation::SchemaBuilder.build_from_block(&block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create_input_validator(schema_or_block)
|
|
13
|
+
check_dry_validation_available!
|
|
14
|
+
|
|
15
|
+
schema = case schema_or_block
|
|
16
|
+
when Proc
|
|
17
|
+
build_validation_schema(&schema_or_block)
|
|
18
|
+
else
|
|
19
|
+
schema_or_block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
RubyReactor::Validation::InputValidator.new(schema)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_dry_validation_available!
|
|
28
|
+
return if defined?(Dry::Schema)
|
|
29
|
+
|
|
30
|
+
raise LoadError,
|
|
31
|
+
"dry-validation gem is required for validation features. Add 'gem \"dry-validation\"' to your Gemfile."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Error
|
|
5
|
+
class Base < StandardError
|
|
6
|
+
attr_reader :step, :context, :original_error
|
|
7
|
+
|
|
8
|
+
def initialize(message, step: nil, context: nil, original_error: nil)
|
|
9
|
+
super(message)
|
|
10
|
+
@step = step
|
|
11
|
+
@context = context
|
|
12
|
+
@original_error = original_error
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Error
|
|
5
|
+
class InputValidationError < Base
|
|
6
|
+
attr_reader :field_errors
|
|
7
|
+
|
|
8
|
+
def initialize(field_errors)
|
|
9
|
+
@field_errors = field_errors
|
|
10
|
+
@message = build_message
|
|
11
|
+
super(@message)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build_message
|
|
15
|
+
return "Input validation failed" if field_errors.empty?
|
|
16
|
+
|
|
17
|
+
error_messages = field_errors.map do |field, errors|
|
|
18
|
+
"#{field} #{errors}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
"Input validation failed: #{error_messages.join(", ")}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
@message || build_message
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Error
|
|
5
|
+
class StepFailureError < Base
|
|
6
|
+
attr_reader :step_arguments
|
|
7
|
+
|
|
8
|
+
def initialize(message, step: nil, context: nil, original_error: nil, step_arguments: {})
|
|
9
|
+
super(message, step: step, context: context, original_error: original_error)
|
|
10
|
+
@step_arguments = step_arguments
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def retryable?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
class Executor
|
|
5
|
+
class CompensationManager
|
|
6
|
+
def initialize(context)
|
|
7
|
+
@context = context
|
|
8
|
+
@undo_stack = []
|
|
9
|
+
@undo_trace = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :undo_stack, :undo_trace
|
|
13
|
+
|
|
14
|
+
def add_to_undo_stack(step_info)
|
|
15
|
+
@undo_stack << step_info
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def handle_step_failure(step_config, error, arguments)
|
|
19
|
+
# Try compensation
|
|
20
|
+
compensation_result = compensate_step(step_config, error, arguments)
|
|
21
|
+
case compensation_result
|
|
22
|
+
when RubyReactor::Success
|
|
23
|
+
# Compensation succeeded, continue with rollback
|
|
24
|
+
rollback_completed_steps
|
|
25
|
+
RubyReactor.Failure("Step '#{step_config.name}' failed: #{error}")
|
|
26
|
+
when RubyReactor::Failure
|
|
27
|
+
# Compensation failed, this is more serious
|
|
28
|
+
rollback_completed_steps
|
|
29
|
+
raise Error::CompensationError.new(
|
|
30
|
+
"Compensation for step '#{step_config.name}' failed: #{compensation_result.error}",
|
|
31
|
+
step: step_config.name,
|
|
32
|
+
context: @context,
|
|
33
|
+
original_error: error
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rollback_completed_steps
|
|
39
|
+
@undo_stack.reverse_each do |step_info|
|
|
40
|
+
result = undo_step(step_info[:step], step_info[:result], step_info[:arguments])
|
|
41
|
+
@undo_trace << { type: :undo, step: step_info[:step].name, result: result,
|
|
42
|
+
arguments: step_info[:arguments] }
|
|
43
|
+
end
|
|
44
|
+
@undo_stack.clear
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def compensate_step(step_config, error, arguments)
|
|
50
|
+
if step_config.compensate_block
|
|
51
|
+
@context.execution_trace << { type: :compensate, step: step_config.name, timestamp: Time.now, error: error,
|
|
52
|
+
arguments: arguments }
|
|
53
|
+
@undo_trace << { type: :compensation, step: step_config.name, error: error, arguments: arguments }
|
|
54
|
+
step_config.compensate_block.call(error, arguments, @context)
|
|
55
|
+
elsif step_config.has_impl?
|
|
56
|
+
@context.execution_trace << { type: :compensate, step: step_config.name, timestamp: Time.now, error: error,
|
|
57
|
+
arguments: arguments }
|
|
58
|
+
@undo_trace << { type: :compensation, step: step_config.name, error: error, arguments: arguments }
|
|
59
|
+
step_config.impl.compensate(error, arguments, @context)
|
|
60
|
+
else
|
|
61
|
+
RubyReactor.Success() # Default compensation
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def undo_step(step_config, result, arguments)
|
|
66
|
+
@context.execution_trace << { type: :undo, step: step_config.name, timestamp: Time.now, result: result.value,
|
|
67
|
+
arguments: arguments }
|
|
68
|
+
if step_config.undo_block
|
|
69
|
+
step_config.undo_block.call(result.value, arguments, @context)
|
|
70
|
+
elsif step_config.has_impl?
|
|
71
|
+
step_config.impl.undo(result.value, arguments, @context)
|
|
72
|
+
end
|
|
73
|
+
rescue StandardError
|
|
74
|
+
# Log undo failure but don't halt the rollback process
|
|
75
|
+
# In a real implementation, this would use a logger
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
class Executor
|
|
5
|
+
class GraphManager
|
|
6
|
+
def initialize(reactor_class, dependency_graph, context)
|
|
7
|
+
@reactor_class = reactor_class
|
|
8
|
+
@dependency_graph = dependency_graph
|
|
9
|
+
@context = context
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_and_validate!
|
|
13
|
+
build_dependency_graph
|
|
14
|
+
validate_graph!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mark_completed_steps_from_context
|
|
18
|
+
@context.intermediate_results.each_key do |step_name|
|
|
19
|
+
@dependency_graph.complete_step(step_name)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def build_dependency_graph
|
|
26
|
+
@reactor_class.steps.each_value do |step_config|
|
|
27
|
+
@dependency_graph.add_step(step_config)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_graph!
|
|
32
|
+
return unless @dependency_graph.has_cycles?
|
|
33
|
+
|
|
34
|
+
raise Error::DependencyError.new(
|
|
35
|
+
"Dependency graph contains cycles",
|
|
36
|
+
context: @context
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|