ruby_reactor 0.3.0 → 0.3.1
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 +4 -4
- data/README.md +31 -4
- data/documentation/testing.md +812 -0
- data/lib/ruby_reactor/configuration.rb +1 -1
- data/lib/ruby_reactor/context.rb +13 -5
- data/lib/ruby_reactor/context_serializer.rb +55 -4
- data/lib/ruby_reactor/dsl/reactor.rb +3 -2
- data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
- data/lib/ruby_reactor/executor/result_handler.rb +8 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
- data/lib/ruby_reactor/executor/step_executor.rb +24 -99
- data/lib/ruby_reactor/executor.rb +3 -13
- data/lib/ruby_reactor/map/collector.rb +16 -15
- data/lib/ruby_reactor/map/element_executor.rb +90 -104
- data/lib/ruby_reactor/map/execution.rb +2 -1
- data/lib/ruby_reactor/map/helpers.rb +2 -1
- data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
- data/lib/ruby_reactor/reactor.rb +174 -16
- data/lib/ruby_reactor/rspec/helpers.rb +17 -0
- data/lib/ruby_reactor/rspec/matchers.rb +256 -0
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
- data/lib/ruby_reactor/rspec.rb +18 -0
- data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -3
- data/lib/ruby_reactor/step/compose_step.rb +0 -1
- data/lib/ruby_reactor/step/map_step.rb +11 -18
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +32 -24
- data/lib/ruby_reactor.rb +70 -10
- metadata +9 -3
data/lib/ruby_reactor/context.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RubyReactor
|
|
4
4
|
class Context
|
|
5
5
|
attr_accessor :inputs, :intermediate_results, :private_data, :current_step, :retry_count, :concurrency_key,
|
|
6
|
-
:retry_context, :reactor_class, :execution_trace, :inline_async_execution, :undo_stack,
|
|
6
|
+
:retry_context, :reactor_class, :execution_trace, :inline_async_execution, :undo_stack,
|
|
7
7
|
:parent_context, :root_context, :composed_contexts, :context_id, :map_operations, :map_metadata,
|
|
8
8
|
:cancelled, :cancellation_reason, :parent_context_id, :status, :failure_reason
|
|
9
9
|
|
|
@@ -23,7 +23,6 @@ module RubyReactor
|
|
|
23
23
|
@execution_trace = []
|
|
24
24
|
@inline_async_execution = false # Flag to prevent nested async calls
|
|
25
25
|
@undo_stack = [] # Initialize the undo stack
|
|
26
|
-
@test_mode = false
|
|
27
26
|
@cancelled = false
|
|
28
27
|
@cancellation_reason = nil
|
|
29
28
|
@status = "pending"
|
|
@@ -33,6 +32,14 @@ module RubyReactor
|
|
|
33
32
|
@root_context = nil
|
|
34
33
|
end
|
|
35
34
|
|
|
35
|
+
def finished?
|
|
36
|
+
%w[completed failed cancelled].include?(@status.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failed?
|
|
40
|
+
@status.to_s == "failed"
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def get_input(name, path = nil)
|
|
37
44
|
value = @inputs[name.to_sym] || @inputs[name.to_s]
|
|
38
45
|
return nil if value.nil?
|
|
@@ -57,6 +64,10 @@ module RubyReactor
|
|
|
57
64
|
end
|
|
58
65
|
alias result get_result
|
|
59
66
|
|
|
67
|
+
def has_result?(step_name)
|
|
68
|
+
@intermediate_results.key?(step_name.to_sym) || @intermediate_results.key?(step_name.to_s)
|
|
69
|
+
end
|
|
70
|
+
|
|
60
71
|
def set_result(step_name, value)
|
|
61
72
|
@intermediate_results[step_name.to_sym] = value
|
|
62
73
|
end
|
|
@@ -81,7 +92,6 @@ module RubyReactor
|
|
|
81
92
|
retry_context: @retry_context,
|
|
82
93
|
reactor_class: @reactor_class,
|
|
83
94
|
execution_trace: @execution_trace,
|
|
84
|
-
test_mode: @test_mode,
|
|
85
95
|
status: @status,
|
|
86
96
|
failure_reason: @failure_reason
|
|
87
97
|
}
|
|
@@ -105,7 +115,6 @@ module RubyReactor
|
|
|
105
115
|
retry_context: @retry_context.serialize_for_retry,
|
|
106
116
|
execution_trace: ContextSerializer.serialize_value(@execution_trace),
|
|
107
117
|
undo_stack: serialize_undo_stack,
|
|
108
|
-
test_mode: @test_mode,
|
|
109
118
|
cancelled: @cancelled,
|
|
110
119
|
cancellation_reason: @cancellation_reason,
|
|
111
120
|
status: @status,
|
|
@@ -130,7 +139,6 @@ module RubyReactor
|
|
|
130
139
|
context.retry_context = RetryContext.deserialize_from_retry(data["retry_context"] || {})
|
|
131
140
|
context.execution_trace = ContextSerializer.deserialize_value(data["execution_trace"]) || []
|
|
132
141
|
context.undo_stack = deserialize_undo_stack(data["undo_stack"] || [], context.reactor_class)
|
|
133
|
-
context.test_mode = data["test_mode"] || false
|
|
134
142
|
context.cancelled = data["cancelled"] || false
|
|
135
143
|
context.cancellation_reason = data["cancellation_reason"]
|
|
136
144
|
context.status = data["status"] || "pending"
|
|
@@ -34,9 +34,25 @@ module RubyReactor
|
|
|
34
34
|
when RubyReactor::Success
|
|
35
35
|
{ "_type" => "Success", "value" => serialize_value(value.value) }
|
|
36
36
|
when RubyReactor::Failure
|
|
37
|
-
{
|
|
37
|
+
{
|
|
38
|
+
"_type" => "Failure",
|
|
39
|
+
"error" => serialize_value(value.error),
|
|
40
|
+
"retryable" => value.retryable,
|
|
41
|
+
"step_name" => value.step_name,
|
|
42
|
+
"inputs" => serialize_value(value.inputs),
|
|
43
|
+
"backtrace" => value.backtrace,
|
|
44
|
+
"reactor_name" => value.reactor_name,
|
|
45
|
+
"step_arguments" => serialize_value(value.step_arguments),
|
|
46
|
+
"exception_class" => value.exception_class,
|
|
47
|
+
"file_path" => value.file_path,
|
|
48
|
+
"line_number" => value.line_number,
|
|
49
|
+
"code_snippet" => serialize_value(value.code_snippet),
|
|
50
|
+
"validation_errors" => serialize_value(value.validation_errors)
|
|
51
|
+
}
|
|
38
52
|
when RubyReactor::Context
|
|
39
53
|
{ "_type" => "Context", "value" => value.serialize_for_retry }
|
|
54
|
+
when Symbol
|
|
55
|
+
{ "_type" => "Symbol", "value" => value.to_s }
|
|
40
56
|
when Time
|
|
41
57
|
{ "_type" => "Time", "value" => value.iso8601 }
|
|
42
58
|
when BigDecimal
|
|
@@ -94,9 +110,24 @@ module RubyReactor
|
|
|
94
110
|
when "Success"
|
|
95
111
|
RubyReactor::Success(deserialize_value(value["value"]))
|
|
96
112
|
when "Failure"
|
|
97
|
-
RubyReactor::Failure(
|
|
113
|
+
RubyReactor::Failure.new(
|
|
114
|
+
deserialize_value(value["error"]),
|
|
115
|
+
retryable: value["retryable"],
|
|
116
|
+
step_name: value["step_name"],
|
|
117
|
+
inputs: deserialize_value(value["inputs"]),
|
|
118
|
+
backtrace: value["backtrace"],
|
|
119
|
+
reactor_name: value["reactor_name"],
|
|
120
|
+
step_arguments: deserialize_value(value["step_arguments"]),
|
|
121
|
+
exception_class: value["exception_class"],
|
|
122
|
+
file_path: value["file_path"],
|
|
123
|
+
line_number: value["line_number"],
|
|
124
|
+
code_snippet: deserialize_value(value["code_snippet"]),
|
|
125
|
+
validation_errors: deserialize_value(value["validation_errors"])
|
|
126
|
+
)
|
|
98
127
|
when "Context"
|
|
99
128
|
Context.deserialize_from_retry(value["value"])
|
|
129
|
+
when "Symbol"
|
|
130
|
+
value["value"].to_sym
|
|
100
131
|
when "Time"
|
|
101
132
|
Time.iso8601(value["value"])
|
|
102
133
|
when "BigDecimal"
|
|
@@ -130,11 +161,13 @@ module RubyReactor
|
|
|
130
161
|
strict_ordering: value["strict_ordering"],
|
|
131
162
|
batch_size: value["batch_size"]
|
|
132
163
|
)
|
|
164
|
+
|
|
133
165
|
else
|
|
134
|
-
|
|
166
|
+
# Unknown type wrapper, return as is (but deserialize values)
|
|
167
|
+
value.transform_values { |v| deserialize_value(v) }
|
|
135
168
|
end
|
|
136
169
|
else
|
|
137
|
-
# Regular
|
|
170
|
+
# Regular Hash
|
|
138
171
|
value.transform_keys(&:to_sym).transform_values { |v| deserialize_value(v) }
|
|
139
172
|
end
|
|
140
173
|
when Array
|
|
@@ -143,6 +176,24 @@ module RubyReactor
|
|
|
143
176
|
value
|
|
144
177
|
end
|
|
145
178
|
end
|
|
179
|
+
|
|
180
|
+
# Simplifies data for public API usage (removes wrappers, flattens types)
|
|
181
|
+
def simplify_for_api(value)
|
|
182
|
+
case value
|
|
183
|
+
when Hash
|
|
184
|
+
value.each_with_object({}) do |(k, v), hash|
|
|
185
|
+
hash[k.to_s] = simplify_for_api(v)
|
|
186
|
+
end
|
|
187
|
+
when Array
|
|
188
|
+
value.map { |v| simplify_for_api(v) }
|
|
189
|
+
when Success, Failure, Context
|
|
190
|
+
simplify_for_api(value.to_h)
|
|
191
|
+
when Symbol
|
|
192
|
+
value.to_s
|
|
193
|
+
else
|
|
194
|
+
value
|
|
195
|
+
end
|
|
196
|
+
end
|
|
146
197
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
147
198
|
|
|
148
199
|
private
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module RubyReactor
|
|
4
4
|
module Error
|
|
5
5
|
class StepFailureError < Base
|
|
6
|
-
attr_reader :step_arguments
|
|
6
|
+
attr_reader :step_arguments, :exception_class
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# rubocop:disable Metrics/ParameterLists
|
|
9
|
+
def initialize(message, step: nil, context: nil, original_error: nil, step_arguments: {}, exception_class: nil)
|
|
10
|
+
# rubocop:enable Metrics/ParameterLists
|
|
9
11
|
super(message, step: step, context: context, original_error: original_error)
|
|
10
12
|
@step_arguments = step_arguments
|
|
13
|
+
@exception_class = exception_class
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def retryable?
|
|
@@ -31,7 +31,7 @@ module RubyReactor
|
|
|
31
31
|
handle_step_failure_error(error)
|
|
32
32
|
when Error::InputValidationError
|
|
33
33
|
# Preserve validation errors as-is for proper error handling
|
|
34
|
-
RubyReactor.Failure(error)
|
|
34
|
+
RubyReactor.Failure(error, validation_errors: error.field_errors)
|
|
35
35
|
when Error::Base
|
|
36
36
|
# Other errors need rollback
|
|
37
37
|
@compensation_manager.rollback_completed_steps
|
|
@@ -129,7 +129,7 @@ module RubyReactor
|
|
|
129
129
|
|
|
130
130
|
def create_failure_from_error(error, redact_inputs)
|
|
131
131
|
original_error = error.original_error
|
|
132
|
-
exception_class = original_error
|
|
132
|
+
exception_class = resolve_exception_class(original_error, error)
|
|
133
133
|
backtrace = original_error&.backtrace || error.backtrace
|
|
134
134
|
file_path, line_number = extract_location(backtrace)
|
|
135
135
|
code_snippet = RubyReactor::Utils::CodeExtractor.extract(file_path, line_number) if file_path
|
|
@@ -149,6 +149,12 @@ module RubyReactor
|
|
|
149
149
|
)
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
def resolve_exception_class(original_error, error)
|
|
153
|
+
return original_error.class.name if original_error
|
|
154
|
+
|
|
155
|
+
error.respond_to?(:exception_class) ? error.exception_class : nil
|
|
156
|
+
end
|
|
157
|
+
|
|
152
158
|
def validate_step_output(step_config, value, resolved_arguments = {})
|
|
153
159
|
return unless step_config.output_validator
|
|
154
160
|
|
|
@@ -116,7 +116,8 @@ module RubyReactor
|
|
|
116
116
|
@context.root_context&.reactor_class&.async? ||
|
|
117
117
|
@context.inline_async_execution
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
# Always try async retry if configured
|
|
120
|
+
if is_async
|
|
120
121
|
handle_async_retry(step_config, reactor_class, result)
|
|
121
122
|
else
|
|
122
123
|
handle_sync_retry(step_config, reactor_class, result)
|
|
@@ -124,12 +125,19 @@ module RubyReactor
|
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
def handle_async_retry(step_config, reactor_class, result)
|
|
127
|
-
requeue_job_for_step_retry(step_config, result.error, reactor_class)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
requeue_result = requeue_job_for_step_retry(step_config, result.error, reactor_class)
|
|
129
|
+
|
|
130
|
+
# If it returned an AsyncResult, we are truly async.
|
|
131
|
+
# Otherwise, it ran inline and we should return the result of that execution.
|
|
132
|
+
if requeue_result.is_a?(RubyReactor::AsyncResult)
|
|
133
|
+
RetryQueuedResult.new(
|
|
134
|
+
step_config.name,
|
|
135
|
+
@context.retry_context.attempts_for_step(step_config.name),
|
|
136
|
+
@context.retry_context.next_retry_at
|
|
137
|
+
)
|
|
138
|
+
else
|
|
139
|
+
requeue_result
|
|
140
|
+
end
|
|
133
141
|
end
|
|
134
142
|
|
|
135
143
|
def handle_sync_retry(step_config, reactor_class, result)
|
|
@@ -13,7 +13,7 @@ module RubyReactor
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def execute_all_steps
|
|
16
|
-
until @dependency_graph.all_completed?
|
|
16
|
+
until @dependency_graph.all_completed? || @context.finished?
|
|
17
17
|
ready_steps = @dependency_graph.ready_steps
|
|
18
18
|
|
|
19
19
|
if ready_steps.empty?
|
|
@@ -65,53 +65,29 @@ module RubyReactor
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
# Merge the state from the async-executed executor back into ours
|
|
70
|
-
# We need to update our context IN PLACE, not replace the reference,
|
|
71
|
-
# because the Executor also holds a reference to the same context object
|
|
72
|
-
|
|
73
|
-
# Update intermediate results
|
|
74
|
-
other_executor.context.intermediate_results.each do |step_name, value|
|
|
75
|
-
@context.set_result(step_name, value)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Append execution trace from the async execution
|
|
79
|
-
# The Worker's execution will have ALL steps including ones we already executed,
|
|
80
|
-
# but we only want to add the NEW entries (from current_step onwards)
|
|
81
|
-
current_trace_length = @context.execution_trace.length
|
|
82
|
-
new_trace_entries = other_executor.context.execution_trace[current_trace_length..] || []
|
|
83
|
-
|
|
84
|
-
@context.execution_trace.concat(new_trace_entries)
|
|
85
|
-
|
|
86
|
-
# Update retry context
|
|
87
|
-
@context.retry_context = other_executor.context.retry_context
|
|
88
|
-
|
|
89
|
-
# Update current_step:
|
|
90
|
-
# If the other executor has a current_step, it means it paused/interrupted there. We should adopt it.
|
|
91
|
-
# If it's nil, it means it completed successfully, so we clear our current_step (which was the async step).
|
|
92
|
-
@context.current_step = other_executor.context.current_step
|
|
93
|
-
|
|
94
|
-
# Update our dependency graph to reflect completed steps
|
|
95
|
-
other_executor.context.intermediate_results.each_key do |step_name|
|
|
96
|
-
@dependency_graph.complete_step(step_name)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Also mark the current_step as completed if it exists (for failed steps that don't have results)
|
|
100
|
-
@dependency_graph.complete_step(other_executor.context.current_step) if other_executor.context.current_step
|
|
101
|
-
|
|
102
|
-
# Merge any undo stack items
|
|
103
|
-
other_executor.undo_stack.each do |item|
|
|
104
|
-
# Avoid duplicates by checking if this step is already in the undo stack
|
|
105
|
-
# Use string comparison for step names to avoid symbol/string mismatch issues
|
|
106
|
-
unless @compensation_manager.undo_stack.any? { |existing| existing[:step].name.to_s == item[:step].name.to_s }
|
|
107
|
-
@compensation_manager.add_to_undo_stack(item)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
68
|
+
private
|
|
110
69
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
70
|
+
def reconstruct_failure(data)
|
|
71
|
+
return data if data.is_a?(RubyReactor::Failure)
|
|
72
|
+
return nil unless data.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
# Helper for hash access with string/symbol keys
|
|
75
|
+
get = ->(key) { data[key] || data[key.to_s] }
|
|
76
|
+
|
|
77
|
+
RubyReactor::Failure.new(
|
|
78
|
+
get.call(:message),
|
|
79
|
+
step_name: get.call(:step_name),
|
|
80
|
+
inputs: get.call(:inputs),
|
|
81
|
+
redact_inputs: get.call(:redact_inputs) || [],
|
|
82
|
+
backtrace: get.call(:backtrace),
|
|
83
|
+
reactor_name: get.call(:reactor_name),
|
|
84
|
+
step_arguments: get.call(:step_arguments),
|
|
85
|
+
exception_class: get.call(:exception_class),
|
|
86
|
+
file_path: get.call(:file_path),
|
|
87
|
+
line_number: get.call(:line_number),
|
|
88
|
+
code_snippet: get.call(:code_snippet),
|
|
89
|
+
validation_errors: get.call(:validation_errors)
|
|
90
|
+
)
|
|
115
91
|
end
|
|
116
92
|
|
|
117
93
|
def execute_step_with_retry(step_config)
|
|
@@ -190,8 +166,6 @@ module RubyReactor
|
|
|
190
166
|
end
|
|
191
167
|
end
|
|
192
168
|
|
|
193
|
-
private
|
|
194
|
-
|
|
195
169
|
def handle_async_step(step_config)
|
|
196
170
|
# Step-level async: hand off execution to worker
|
|
197
171
|
|
|
@@ -204,60 +178,11 @@ module RubyReactor
|
|
|
204
178
|
|
|
205
179
|
serialized_context = ContextSerializer.serialize(context_to_serialize)
|
|
206
180
|
|
|
207
|
-
|
|
181
|
+
configuration.async_router.perform_async(
|
|
208
182
|
serialized_context,
|
|
209
183
|
reactor_class_name,
|
|
210
184
|
intermediate_results: @context.intermediate_results
|
|
211
185
|
)
|
|
212
|
-
|
|
213
|
-
# Handle different result types from async router
|
|
214
|
-
case result
|
|
215
|
-
when RubyReactor::AsyncResult
|
|
216
|
-
# Production behavior: return async result to caller
|
|
217
|
-
|
|
218
|
-
result
|
|
219
|
-
when Executor
|
|
220
|
-
handle_inline_executor_result(result)
|
|
221
|
-
else
|
|
222
|
-
# Unexpected result type, treat as error
|
|
223
|
-
raise Error::ValidationError.new(
|
|
224
|
-
"Unexpected result type from async router: #{result.class}",
|
|
225
|
-
context: @context
|
|
226
|
-
)
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def handle_inline_executor_result(result)
|
|
231
|
-
# Worker executed inline and returned an executor.
|
|
232
|
-
# This happens when running in test mode or when perform_async returns an executor.
|
|
233
|
-
# We need to merge the state back into our current executor.
|
|
234
|
-
#
|
|
235
|
-
# If we are a child reactor, the worker executed the root reactor, so the result
|
|
236
|
-
# will be a Root executor. We handle this mismatch below by finding our
|
|
237
|
-
# corresponding child context within the root result.
|
|
238
|
-
if @context.root_context && (result.context.reactor_class != @reactor_class)
|
|
239
|
-
# We are a child, and result is root.
|
|
240
|
-
# We need to find ourselves in the root result using context_id.
|
|
241
|
-
matching_context = find_context_by_id(result.context, @context.context_id)
|
|
242
|
-
|
|
243
|
-
if matching_context
|
|
244
|
-
# Replace the result's context with the matching child context
|
|
245
|
-
# so merge_executor_state works correctly
|
|
246
|
-
result.instance_variable_set(:@context, matching_context)
|
|
247
|
-
else
|
|
248
|
-
# Fallback: if we can't find it (shouldn't happen), we might be in trouble.
|
|
249
|
-
# But let's try to proceed, maybe it's not nested?
|
|
250
|
-
# For now, raise an error to be explicit
|
|
251
|
-
raise Error::ValidationError.new(
|
|
252
|
-
"Could not find child context with ID #{@context.context_id} in root result",
|
|
253
|
-
context: @context
|
|
254
|
-
)
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
merge_executor_state(result)
|
|
259
|
-
|
|
260
|
-
result.result
|
|
261
186
|
end
|
|
262
187
|
|
|
263
188
|
def handle_interrupt_step(step_config)
|
|
@@ -40,6 +40,7 @@ module RubyReactor
|
|
|
40
40
|
input_validator = InputValidator.new(@reactor_class, @context)
|
|
41
41
|
input_validator.validate!
|
|
42
42
|
|
|
43
|
+
@context.status = :running
|
|
43
44
|
save_context
|
|
44
45
|
|
|
45
46
|
graph_manager = GraphManager.new(@reactor_class, @dependency_graph, @context)
|
|
@@ -59,6 +60,7 @@ module RubyReactor
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def resume_execution
|
|
63
|
+
@context.status = :running
|
|
62
64
|
prepare_for_resume
|
|
63
65
|
save_context
|
|
64
66
|
|
|
@@ -118,19 +120,7 @@ module RubyReactor
|
|
|
118
120
|
@context.status = :completed
|
|
119
121
|
when RubyReactor::Failure
|
|
120
122
|
@context.status = :failed
|
|
121
|
-
@context.failure_reason =
|
|
122
|
-
message: result.error.is_a?(Exception) ? result.error.message : result.error.to_s,
|
|
123
|
-
step_name: result.step_name,
|
|
124
|
-
inputs: result.inputs,
|
|
125
|
-
backtrace: result.backtrace,
|
|
126
|
-
reactor_name: result.reactor_name,
|
|
127
|
-
step_arguments: result.step_arguments,
|
|
128
|
-
exception_class: result.exception_class,
|
|
129
|
-
file_path: result.file_path,
|
|
130
|
-
line_number: result.line_number,
|
|
131
|
-
code_snippet: result.code_snippet,
|
|
132
|
-
validation_errors: result.validation_errors
|
|
133
|
-
}
|
|
123
|
+
@context.failure_reason = result
|
|
134
124
|
when RubyReactor::InterruptResult
|
|
135
125
|
@context.status = :paused
|
|
136
126
|
end
|
|
@@ -29,21 +29,9 @@ module RubyReactor
|
|
|
29
29
|
# Since map_offset tracks dispatching progress and might exceed count due to batching reservation,
|
|
30
30
|
# we must strictly check against the total count of elements.
|
|
31
31
|
# Check for fail_fast failure FIRST
|
|
32
|
-
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
reactor_class = resolve_reactor_class(metadata["reactor_class_info"])
|
|
36
|
-
|
|
37
|
-
failed_context_data = storage.retrieve_context(failed_context_id, reactor_class.name)
|
|
38
|
-
|
|
39
|
-
if failed_context_data
|
|
40
|
-
failed_context = RubyReactor::Context.deserialize_from_retry(failed_context_data)
|
|
41
|
-
|
|
42
|
-
# Resume parent execution (which marks step as failed)
|
|
43
|
-
resume_parent_execution(parent_context, step_name, RubyReactor::Failure(failed_context.failure_reason),
|
|
44
|
-
storage)
|
|
45
|
-
return
|
|
46
|
-
end
|
|
32
|
+
if (failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name))
|
|
33
|
+
handle_failure(failed_context_id, metadata, storage, parent_context, step_name)
|
|
34
|
+
return
|
|
47
35
|
end
|
|
48
36
|
|
|
49
37
|
return if results_count < total_count
|
|
@@ -96,6 +84,19 @@ module RubyReactor
|
|
|
96
84
|
RubyReactor::Success(results)
|
|
97
85
|
end
|
|
98
86
|
end
|
|
87
|
+
|
|
88
|
+
def self.handle_failure(failed_context_id, metadata, storage, parent_context, step_name)
|
|
89
|
+
# Resolve the class of the mapped reactor to retrieve its context
|
|
90
|
+
reactor_class = resolve_reactor_class(metadata["reactor_class_info"])
|
|
91
|
+
failed_context_data = storage.retrieve_context(failed_context_id, reactor_class.name)
|
|
92
|
+
|
|
93
|
+
return unless failed_context_data
|
|
94
|
+
|
|
95
|
+
failed_context = RubyReactor::Context.deserialize_from_retry(failed_context_data)
|
|
96
|
+
reason = failed_context.failure_reason
|
|
97
|
+
result = reason.is_a?(RubyReactor::Failure) ? reason : RubyReactor::Failure(reason)
|
|
98
|
+
resume_parent_execution(parent_context, step_name, result, storage)
|
|
99
|
+
end
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
end
|