ruby_reactor 0.1.0 → 0.3.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 +4 -4
- data/.rubocop.yml +10 -2
- data/README.md +177 -3
- data/Rakefile +25 -0
- data/documentation/data_pipelines.md +90 -84
- data/documentation/images/failed_order_processing.png +0 -0
- data/documentation/images/payment_workflow.png +0 -0
- data/documentation/interrupts.md +161 -0
- data/gui/.gitignore +24 -0
- data/gui/README.md +73 -0
- data/gui/eslint.config.js +23 -0
- data/gui/index.html +13 -0
- data/gui/package-lock.json +5925 -0
- data/gui/package.json +46 -0
- data/gui/postcss.config.js +6 -0
- data/gui/public/vite.svg +1 -0
- data/gui/src/App.css +42 -0
- data/gui/src/App.tsx +51 -0
- data/gui/src/assets/react.svg +1 -0
- data/gui/src/components/DagVisualizer.tsx +424 -0
- data/gui/src/components/Dashboard.tsx +163 -0
- data/gui/src/components/ErrorBoundary.tsx +47 -0
- data/gui/src/components/ReactorDetail.tsx +135 -0
- data/gui/src/components/StepInspector.tsx +492 -0
- data/gui/src/components/__tests__/DagVisualizer.test.tsx +140 -0
- data/gui/src/components/__tests__/ReactorDetail.test.tsx +111 -0
- data/gui/src/components/__tests__/StepInspector.test.tsx +408 -0
- data/gui/src/globals.d.ts +7 -0
- data/gui/src/index.css +14 -0
- data/gui/src/lib/utils.ts +13 -0
- data/gui/src/main.tsx +14 -0
- data/gui/src/test/setup.ts +11 -0
- data/gui/tailwind.config.js +11 -0
- data/gui/tsconfig.app.json +28 -0
- data/gui/tsconfig.json +7 -0
- data/gui/tsconfig.node.json +26 -0
- data/gui/vite.config.ts +8 -0
- data/gui/vitest.config.ts +13 -0
- data/lib/ruby_reactor/async_router.rb +12 -8
- data/lib/ruby_reactor/context.rb +35 -9
- data/lib/ruby_reactor/context_serializer.rb +15 -0
- data/lib/ruby_reactor/dependency_graph.rb +2 -0
- data/lib/ruby_reactor/dsl/compose_builder.rb +8 -0
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +48 -0
- data/lib/ruby_reactor/dsl/interrupt_step_config.rb +21 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +14 -2
- data/lib/ruby_reactor/dsl/reactor.rb +12 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +4 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +60 -27
- data/lib/ruby_reactor/executor/graph_manager.rb +2 -0
- data/lib/ruby_reactor/executor/result_handler.rb +118 -39
- data/lib/ruby_reactor/executor/retry_manager.rb +12 -1
- data/lib/ruby_reactor/executor/step_executor.rb +38 -4
- data/lib/ruby_reactor/executor.rb +86 -13
- data/lib/ruby_reactor/interrupt_result.rb +20 -0
- data/lib/ruby_reactor/map/collector.rb +71 -35
- data/lib/ruby_reactor/map/dispatcher.rb +162 -0
- data/lib/ruby_reactor/map/element_executor.rb +62 -56
- data/lib/ruby_reactor/map/execution.rb +44 -4
- data/lib/ruby_reactor/map/helpers.rb +44 -6
- data/lib/ruby_reactor/map/result_enumerator.rb +105 -0
- data/lib/ruby_reactor/reactor.rb +187 -1
- data/lib/ruby_reactor/registry.rb +25 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -1
- data/lib/ruby_reactor/step/compose_step.rb +22 -6
- data/lib/ruby_reactor/step/map_step.rb +78 -19
- data/lib/ruby_reactor/storage/adapter.rb +32 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +213 -11
- data/lib/ruby_reactor/template/dynamic_source.rb +32 -0
- data/lib/ruby_reactor/utils/code_extractor.rb +31 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +206 -0
- data/lib/ruby_reactor/web/application.rb +53 -0
- data/lib/ruby_reactor/web/config.ru +5 -0
- data/lib/ruby_reactor/web/public/assets/index-VdeLgH9k.js +19 -0
- data/lib/ruby_reactor/web/public/assets/index-_z-6BvuM.css +1 -0
- data/lib/ruby_reactor/web/public/index.html +14 -0
- data/lib/ruby_reactor/web/public/vite.svg +1 -0
- data/lib/ruby_reactor.rb +94 -28
- data/llms-full.txt +66 -0
- data/llms.txt +7 -0
- metadata +66 -2
|
@@ -15,62 +15,30 @@ module RubyReactor
|
|
|
15
15
|
def handle_step_result(step_config, result, resolved_arguments)
|
|
16
16
|
case result
|
|
17
17
|
when RubyReactor::Success
|
|
18
|
-
|
|
19
|
-
@step_results[step_config.name] = result
|
|
20
|
-
@compensation_manager.add_to_undo_stack({ step: step_config, arguments: resolved_arguments, result: result })
|
|
21
|
-
@context.set_result(step_config.name, result.value)
|
|
22
|
-
@dependency_graph.complete_step(step_config.name)
|
|
18
|
+
handle_success(step_config, result, resolved_arguments)
|
|
23
19
|
when RubyReactor::MaxRetriesExhaustedFailure
|
|
24
|
-
|
|
25
|
-
# The error message from MaxRetriesExhaustedFailure already includes "failed after N attempts"
|
|
26
|
-
@compensation_manager.handle_step_failure(step_config, result.original_error, resolved_arguments)
|
|
27
|
-
# Use the MaxRetriesExhaustedFailure error message for the final error
|
|
28
|
-
raise Error::StepFailureError.new(result.error, step: step_config.name, context: @context,
|
|
29
|
-
step_arguments: resolved_arguments)
|
|
20
|
+
handle_retries_exhausted(step_config, result, resolved_arguments)
|
|
30
21
|
when RubyReactor::Failure
|
|
31
|
-
|
|
32
|
-
raise Error::StepFailureError.new(failure_result.error, step: step_config.name, context: @context,
|
|
33
|
-
step_arguments: resolved_arguments)
|
|
22
|
+
handle_failure(step_config, result, resolved_arguments)
|
|
34
23
|
else
|
|
35
|
-
|
|
36
|
-
validate_step_output(step_config, result, resolved_arguments)
|
|
37
|
-
success_result = RubyReactor.Success(result)
|
|
38
|
-
@step_results[step_config.name] = success_result
|
|
39
|
-
@compensation_manager.add_to_undo_stack({ step: step_config, arguments: resolved_arguments,
|
|
40
|
-
result: success_result })
|
|
41
|
-
@context.set_result(step_config.name, result)
|
|
42
|
-
@dependency_graph.complete_step(step_config.name)
|
|
24
|
+
handle_unknown_result(step_config, result, resolved_arguments)
|
|
43
25
|
end
|
|
44
26
|
end
|
|
45
27
|
|
|
46
28
|
def handle_execution_error(error)
|
|
47
29
|
case error
|
|
48
30
|
when Error::StepFailureError
|
|
49
|
-
|
|
50
|
-
# But we need to rollback all completed steps
|
|
51
|
-
@compensation_manager.rollback_completed_steps
|
|
52
|
-
|
|
53
|
-
redact_inputs = error.context.reactor_class.inputs.select { |_, config| config[:redact] }.keys
|
|
54
|
-
|
|
55
|
-
RubyReactor::Failure(
|
|
56
|
-
error.message,
|
|
57
|
-
step_name: error.step,
|
|
58
|
-
inputs: error.context.inputs,
|
|
59
|
-
redact_inputs: redact_inputs,
|
|
60
|
-
backtrace: error.backtrace,
|
|
61
|
-
reactor_name: error.context.reactor_class.name,
|
|
62
|
-
step_arguments: error.step_arguments
|
|
63
|
-
)
|
|
31
|
+
handle_step_failure_error(error)
|
|
64
32
|
when Error::InputValidationError
|
|
65
33
|
# Preserve validation errors as-is for proper error handling
|
|
66
34
|
RubyReactor.Failure(error)
|
|
67
35
|
when Error::Base
|
|
68
36
|
# Other errors need rollback
|
|
69
37
|
@compensation_manager.rollback_completed_steps
|
|
70
|
-
RubyReactor.Failure("Execution error: #{error.message}")
|
|
38
|
+
RubyReactor.Failure("Execution error: #{error.message}", exception_class: error.class.name)
|
|
71
39
|
else
|
|
72
40
|
# Unknown errors - don't rollback as they may not be reactor-related
|
|
73
|
-
RubyReactor.Failure("Execution failed: #{error.message}")
|
|
41
|
+
RubyReactor.Failure("Execution failed: #{error.message}", exception_class: error.class.name)
|
|
74
42
|
end
|
|
75
43
|
end
|
|
76
44
|
|
|
@@ -85,6 +53,102 @@ module RubyReactor
|
|
|
85
53
|
|
|
86
54
|
private
|
|
87
55
|
|
|
56
|
+
def handle_success(step_config, result, resolved_arguments)
|
|
57
|
+
validate_step_output(step_config, result.value, resolved_arguments)
|
|
58
|
+
@step_results[step_config.name] = result
|
|
59
|
+
@compensation_manager.add_to_undo_stack({ step: step_config, arguments: resolved_arguments, result: result })
|
|
60
|
+
@context.set_result(step_config.name, result.value)
|
|
61
|
+
@dependency_graph.complete_step(step_config.name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def handle_retries_exhausted(step_config, result, resolved_arguments)
|
|
65
|
+
@compensation_manager.handle_step_failure(step_config, result.original_error, resolved_arguments)
|
|
66
|
+
orig_err = result.original_error.is_a?(Exception) ? result.original_error : nil
|
|
67
|
+
error = Error::StepFailureError.new(result.error, step: step_config.name, context: @context,
|
|
68
|
+
original_error: orig_err,
|
|
69
|
+
step_arguments: resolved_arguments)
|
|
70
|
+
if result.respond_to?(:backtrace) && result.backtrace
|
|
71
|
+
error.set_backtrace(result.backtrace)
|
|
72
|
+
elsif orig_err
|
|
73
|
+
error.set_backtrace(orig_err.backtrace)
|
|
74
|
+
end
|
|
75
|
+
raise error
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_failure(step_config, result, resolved_arguments)
|
|
79
|
+
failure_result = @compensation_manager.handle_step_failure(step_config, result.error, resolved_arguments)
|
|
80
|
+
orig_err = result.error.is_a?(Exception) ? result.error : nil
|
|
81
|
+
error = Error::StepFailureError.new(failure_result.error, step: step_config.name, context: @context,
|
|
82
|
+
original_error: orig_err,
|
|
83
|
+
step_arguments: resolved_arguments)
|
|
84
|
+
if result.respond_to?(:backtrace) && result.backtrace
|
|
85
|
+
error.set_backtrace(result.backtrace)
|
|
86
|
+
elsif orig_err
|
|
87
|
+
error.set_backtrace(orig_err.backtrace)
|
|
88
|
+
end
|
|
89
|
+
raise error
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_unknown_result(step_config, result, resolved_arguments)
|
|
93
|
+
validate_step_output(step_config, result, resolved_arguments)
|
|
94
|
+
success_result = RubyReactor.Success(result)
|
|
95
|
+
@step_results[step_config.name] = success_result
|
|
96
|
+
@compensation_manager.add_to_undo_stack({ step: step_config, arguments: resolved_arguments,
|
|
97
|
+
result: success_result })
|
|
98
|
+
@context.set_result(step_config.name, result)
|
|
99
|
+
@dependency_graph.complete_step(step_config.name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_step_failure_error(error)
|
|
103
|
+
current_context = error.context || @context
|
|
104
|
+
current_context.current_step = error.step
|
|
105
|
+
|
|
106
|
+
store_failed_map_context(current_context) if current_context.map_metadata
|
|
107
|
+
|
|
108
|
+
@compensation_manager.rollback_completed_steps
|
|
109
|
+
|
|
110
|
+
redact_inputs = []
|
|
111
|
+
if error.context&.reactor_class
|
|
112
|
+
redact_inputs = error.context.reactor_class.inputs.select { |_, config| config[:redact] }.keys
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
create_failure_from_error(error, redact_inputs)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def store_failed_map_context(context)
|
|
119
|
+
return unless context.map_metadata && context.map_metadata[:map_id]
|
|
120
|
+
return unless context.map_metadata[:fail_fast]
|
|
121
|
+
|
|
122
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
123
|
+
storage.store_map_failed_context_id(
|
|
124
|
+
context.map_metadata[:map_id],
|
|
125
|
+
context.context_id,
|
|
126
|
+
context.map_metadata[:parent_reactor_class_name]
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def create_failure_from_error(error, redact_inputs)
|
|
131
|
+
original_error = error.original_error
|
|
132
|
+
exception_class = original_error&.class&.name
|
|
133
|
+
backtrace = original_error&.backtrace || error.backtrace
|
|
134
|
+
file_path, line_number = extract_location(backtrace)
|
|
135
|
+
code_snippet = RubyReactor::Utils::CodeExtractor.extract(file_path, line_number) if file_path
|
|
136
|
+
|
|
137
|
+
RubyReactor.Failure(
|
|
138
|
+
error.message,
|
|
139
|
+
step_name: error.step,
|
|
140
|
+
inputs: error.context.inputs,
|
|
141
|
+
redact_inputs: redact_inputs,
|
|
142
|
+
backtrace: backtrace,
|
|
143
|
+
reactor_name: error.context.reactor_class.name,
|
|
144
|
+
step_arguments: error.step_arguments,
|
|
145
|
+
exception_class: exception_class,
|
|
146
|
+
file_path: file_path,
|
|
147
|
+
line_number: line_number,
|
|
148
|
+
code_snippet: code_snippet
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
88
152
|
def validate_step_output(step_config, value, resolved_arguments = {})
|
|
89
153
|
return unless step_config.output_validator
|
|
90
154
|
|
|
@@ -98,6 +162,21 @@ module RubyReactor
|
|
|
98
162
|
step_arguments: resolved_arguments
|
|
99
163
|
)
|
|
100
164
|
end
|
|
165
|
+
|
|
166
|
+
def extract_location(backtrace)
|
|
167
|
+
return [nil, nil] unless backtrace && !backtrace.empty?
|
|
168
|
+
|
|
169
|
+
# Filter out internal reactor frames if needed, or just take the first one
|
|
170
|
+
# For now, let's take the first line of the backtrace which should be the error source
|
|
171
|
+
# But we might want to skip our own internal frames if we want to point to user code
|
|
172
|
+
# Let's start with the top frame, assuming backtrace is already correct (from original error)
|
|
173
|
+
|
|
174
|
+
first_line = backtrace.first
|
|
175
|
+
match = first_line.match(/^(.+?):(\d+)(?::in `.*')?$/)
|
|
176
|
+
return [nil, nil] unless match
|
|
177
|
+
|
|
178
|
+
[match[1], match[2].to_i]
|
|
179
|
+
end
|
|
101
180
|
end
|
|
102
181
|
end
|
|
103
182
|
end
|
|
@@ -34,11 +34,22 @@ module RubyReactor
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def requeue_job_for_step_retry(step_config, error, reactor_class)
|
|
37
|
+
@context.current_step = step_config.name
|
|
37
38
|
delay = calculate_backoff_delay(step_config, error, reactor_class)
|
|
38
39
|
|
|
39
40
|
# Serialize context and requeue the job
|
|
40
41
|
# Use root context if available to ensure we serialize the full tree
|
|
41
|
-
|
|
42
|
+
# BUT for map elements (which have map_metadata), we must serialize the element context itself
|
|
43
|
+
|
|
44
|
+
context_to_serialize = if @context.map_metadata
|
|
45
|
+
@context
|
|
46
|
+
else
|
|
47
|
+
@context.root_context || @context
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts "SERIALIZING CONTEXT: #{context_to_serialize.reactor_class.name}"
|
|
51
|
+
puts "INPUTS KEYS: #{context_to_serialize.inputs.keys}" if context_to_serialize.respond_to?(:inputs)
|
|
52
|
+
|
|
42
53
|
reactor_class_name = context_to_serialize.reactor_class.name
|
|
43
54
|
|
|
44
55
|
serialized_context = ContextSerializer.serialize(context_to_serialize)
|
|
@@ -36,6 +36,9 @@ module RubyReactor
|
|
|
36
36
|
# If a step returns Failure, we need to stop execution and return it
|
|
37
37
|
return result if result.is_a?(RubyReactor::Failure)
|
|
38
38
|
|
|
39
|
+
# If a step returns InterruptResult, we need to stop execution and return it
|
|
40
|
+
return result if result.is_a?(RubyReactor::InterruptResult)
|
|
41
|
+
|
|
39
42
|
# If result is nil, it means async was executed inline (test mode), continue
|
|
40
43
|
next if result.nil?
|
|
41
44
|
end
|
|
@@ -49,7 +52,13 @@ module RubyReactor
|
|
|
49
52
|
# If we're already in inline async execution mode (inside Worker),
|
|
50
53
|
# treat async steps as sync to avoid infinite recursion
|
|
51
54
|
|
|
52
|
-
if
|
|
55
|
+
if @dependency_graph.completed.include?(step_config.name)
|
|
56
|
+
return RubyReactor.Success(@context.get_result(step_config.name))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if step_config.interrupt?
|
|
60
|
+
handle_interrupt_step(step_config)
|
|
61
|
+
elsif step_config.async? && !@context.inline_async_execution
|
|
53
62
|
handle_async_step(step_config)
|
|
54
63
|
else
|
|
55
64
|
execute_step_with_retry(step_config)
|
|
@@ -77,8 +86,10 @@ module RubyReactor
|
|
|
77
86
|
# Update retry context
|
|
78
87
|
@context.retry_context = other_executor.context.retry_context
|
|
79
88
|
|
|
80
|
-
#
|
|
81
|
-
|
|
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
|
|
82
93
|
|
|
83
94
|
# Update our dependency graph to reflect completed steps
|
|
84
95
|
other_executor.context.intermediate_results.each_key do |step_name|
|
|
@@ -91,7 +102,8 @@ module RubyReactor
|
|
|
91
102
|
# Merge any undo stack items
|
|
92
103
|
other_executor.undo_stack.each do |item|
|
|
93
104
|
# Avoid duplicates by checking if this step is already in the undo stack
|
|
94
|
-
|
|
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 }
|
|
95
107
|
@compensation_manager.add_to_undo_stack(item)
|
|
96
108
|
end
|
|
97
109
|
end
|
|
@@ -248,6 +260,28 @@ module RubyReactor
|
|
|
248
260
|
result.result
|
|
249
261
|
end
|
|
250
262
|
|
|
263
|
+
def handle_interrupt_step(step_config)
|
|
264
|
+
# Check if we have a result for this step (resuming)
|
|
265
|
+
if @context.intermediate_results.key?(step_config.name)
|
|
266
|
+
# We are resuming
|
|
267
|
+
result = @context.get_result(step_config.name)
|
|
268
|
+
return RubyReactor.Success(result)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# We are pausing
|
|
272
|
+
correlation_id = nil
|
|
273
|
+
correlation_id = step_config.correlation_id_block.call(@context) if step_config.correlation_id_block
|
|
274
|
+
|
|
275
|
+
# Store current step as the one we are paused at
|
|
276
|
+
@context.current_step = step_config.name
|
|
277
|
+
|
|
278
|
+
RubyReactor::InterruptResult.new(
|
|
279
|
+
execution_id: @context.context_id,
|
|
280
|
+
correlation_id: correlation_id,
|
|
281
|
+
intermediate_results: @context.intermediate_results
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
|
|
251
285
|
def configuration
|
|
252
286
|
RubyReactor::Configuration.instance
|
|
253
287
|
end
|
|
@@ -40,24 +40,49 @@ module RubyReactor
|
|
|
40
40
|
input_validator = InputValidator.new(@reactor_class, @context)
|
|
41
41
|
input_validator.validate!
|
|
42
42
|
|
|
43
|
+
save_context
|
|
44
|
+
|
|
43
45
|
graph_manager = GraphManager.new(@reactor_class, @dependency_graph, @context)
|
|
44
46
|
graph_manager.build_and_validate!
|
|
47
|
+
graph_manager.mark_completed_steps_from_context
|
|
45
48
|
|
|
46
49
|
@result = @step_executor.execute_all_steps
|
|
50
|
+
update_context_status(@result)
|
|
51
|
+
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
52
|
+
@result
|
|
47
53
|
rescue StandardError => e
|
|
48
54
|
@result = @result_handler.handle_execution_error(e)
|
|
55
|
+
update_context_status(@result)
|
|
56
|
+
@result
|
|
57
|
+
ensure
|
|
58
|
+
save_context
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
def resume_execution
|
|
52
62
|
prepare_for_resume
|
|
63
|
+
save_context
|
|
53
64
|
|
|
54
|
-
if @context.current_step
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
@result = if @context.current_step
|
|
66
|
+
execute_current_step_and_continue
|
|
67
|
+
else
|
|
68
|
+
execute_remaining_steps
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
update_context_status(@result)
|
|
72
|
+
|
|
73
|
+
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
74
|
+
|
|
75
|
+
@result
|
|
59
76
|
rescue StandardError => e
|
|
60
77
|
handle_resume_error(e)
|
|
78
|
+
update_context_status(@result)
|
|
79
|
+
@result
|
|
80
|
+
ensure
|
|
81
|
+
save_context
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def undo_all
|
|
85
|
+
@compensation_manager.rollback_completed_steps
|
|
61
86
|
end
|
|
62
87
|
|
|
63
88
|
def undo_stack
|
|
@@ -72,8 +97,45 @@ module RubyReactor
|
|
|
72
97
|
@context.execution_trace
|
|
73
98
|
end
|
|
74
99
|
|
|
100
|
+
def save_context
|
|
101
|
+
storage = RubyReactor::Configuration.instance.storage_adapter
|
|
102
|
+
reactor_class_name = @reactor_class.name || "AnonymousReactor-#{@reactor_class.object_id}"
|
|
103
|
+
|
|
104
|
+
# Serialize context
|
|
105
|
+
serialized_context = ContextSerializer.serialize(@context)
|
|
106
|
+
storage.store_context(@context.context_id, serialized_context, reactor_class_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
75
109
|
private
|
|
76
110
|
|
|
111
|
+
def update_context_status(result)
|
|
112
|
+
return unless result
|
|
113
|
+
|
|
114
|
+
case result
|
|
115
|
+
when RubyReactor::AsyncResult
|
|
116
|
+
@context.status = :running
|
|
117
|
+
when RubyReactor::Success
|
|
118
|
+
@context.status = :completed
|
|
119
|
+
when RubyReactor::Failure
|
|
120
|
+
@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
|
+
}
|
|
134
|
+
when RubyReactor::InterruptResult
|
|
135
|
+
@context.status = :paused
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
77
139
|
def prepare_for_resume
|
|
78
140
|
# Build dependency graph and mark completed steps
|
|
79
141
|
graph_manager = GraphManager.new(@reactor_class, @dependency_graph, @context)
|
|
@@ -85,6 +147,9 @@ module RubyReactor
|
|
|
85
147
|
step_config = @reactor_class.steps[@context.current_step]
|
|
86
148
|
return RubyReactor::Failure("Step '#{@context.current_step}' not found in reactor") unless step_config
|
|
87
149
|
|
|
150
|
+
# If current step is already in intermediate_results, skip directly to execute_all_steps
|
|
151
|
+
return @step_executor.execute_all_steps if @context.intermediate_results.key?(@context.current_step.to_sym)
|
|
152
|
+
|
|
88
153
|
# Use execute_step (not execute_step_with_retry) so that async steps can be handled properly in inline mode
|
|
89
154
|
result = @step_executor.execute_step(step_config)
|
|
90
155
|
|
|
@@ -93,7 +158,7 @@ module RubyReactor
|
|
|
93
158
|
@result = @step_executor.execute_all_steps
|
|
94
159
|
else
|
|
95
160
|
case result
|
|
96
|
-
when RetryQueuedResult, RubyReactor::Failure, RubyReactor::AsyncResult
|
|
161
|
+
when RetryQueuedResult, RubyReactor::Failure, RubyReactor::AsyncResult, RubyReactor::InterruptResult
|
|
97
162
|
# Step was requeued, failed, or handed off to async - return the result
|
|
98
163
|
@result = result
|
|
99
164
|
when RubyReactor::Success
|
|
@@ -110,14 +175,22 @@ module RubyReactor
|
|
|
110
175
|
end
|
|
111
176
|
|
|
112
177
|
def handle_resume_error(error)
|
|
113
|
-
|
|
114
|
-
# StepFailureError means compensation already happened, just convert to Failure
|
|
115
|
-
@result = if error.is_a?(Error::StepFailureError)
|
|
116
|
-
RubyReactor.Failure(error.message)
|
|
117
|
-
else
|
|
118
|
-
@result_handler.handle_execution_error(error)
|
|
119
|
-
end
|
|
178
|
+
@result = @result_handler.handle_execution_error(error)
|
|
120
179
|
@result
|
|
121
180
|
end
|
|
181
|
+
|
|
182
|
+
def handle_interrupt(interrupt_result)
|
|
183
|
+
save_context
|
|
184
|
+
|
|
185
|
+
# Store correlation ID mapping if present
|
|
186
|
+
return unless interrupt_result.correlation_id
|
|
187
|
+
|
|
188
|
+
storage = RubyReactor::Configuration.instance.storage_adapter
|
|
189
|
+
storage.store_correlation_id(
|
|
190
|
+
interrupt_result.correlation_id,
|
|
191
|
+
@context.context_id,
|
|
192
|
+
@reactor_class.name
|
|
193
|
+
)
|
|
194
|
+
end
|
|
122
195
|
end
|
|
123
196
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
class InterruptResult
|
|
5
|
+
attr_reader :execution_id, :correlation_id, :status, :timeout_at, :intermediate_results, :error
|
|
6
|
+
|
|
7
|
+
def initialize(execution_id:, correlation_id: nil, timeout_at: nil, intermediate_results: {}, error: nil)
|
|
8
|
+
@execution_id = execution_id
|
|
9
|
+
@correlation_id = correlation_id
|
|
10
|
+
@status = :paused
|
|
11
|
+
@timeout_at = timeout_at
|
|
12
|
+
@intermediate_results = intermediate_results
|
|
13
|
+
@error = error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def paused?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,61 +5,97 @@ module RubyReactor
|
|
|
5
5
|
class Collector
|
|
6
6
|
extend Helpers
|
|
7
7
|
|
|
8
|
-
# rubocop:disable Metrics/MethodLength
|
|
9
8
|
def self.perform(arguments)
|
|
10
9
|
arguments = arguments.transform_keys(&:to_sym)
|
|
11
|
-
parent_context_id = arguments[:parent_context_id]
|
|
12
10
|
map_id = arguments[:map_id]
|
|
11
|
+
parent_context_id = arguments[:parent_context_id]
|
|
13
12
|
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
14
13
|
step_name = arguments[:step_name]
|
|
15
14
|
strict_ordering = arguments[:strict_ordering]
|
|
15
|
+
# timeout = arguments[:timeout]
|
|
16
16
|
|
|
17
17
|
storage = RubyReactor.configuration.storage_adapter
|
|
18
|
+
parent_context_data = storage.retrieve_context(parent_context_id, parent_reactor_class_name)
|
|
19
|
+
parent_context = RubyReactor::Context.deserialize_from_retry(parent_context_data)
|
|
20
|
+
|
|
21
|
+
# Check if all tasks are completed
|
|
22
|
+
metadata = storage.retrieve_map_metadata(map_id, parent_reactor_class_name)
|
|
23
|
+
total_count = metadata ? metadata["count"].to_i : 0
|
|
24
|
+
|
|
25
|
+
results_count = storage.count_map_results(map_id, parent_reactor_class_name)
|
|
26
|
+
|
|
27
|
+
# Not done yet, requeue or wait?
|
|
28
|
+
# Actually Collector currently assumes we only call it when we expect completion or check progress
|
|
29
|
+
# Since map_offset tracks dispatching progress and might exceed count due to batching reservation,
|
|
30
|
+
# we must strictly check against the total count of elements.
|
|
31
|
+
# Check for fail_fast failure FIRST
|
|
32
|
+
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
33
|
+
if failed_context_id
|
|
34
|
+
# Resolve the class of the mapped reactor to retrieve its context
|
|
35
|
+
reactor_class = resolve_reactor_class(metadata["reactor_class_info"])
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return if results_count < total_count
|
|
50
|
+
|
|
51
|
+
# Retrieve results lazily
|
|
52
|
+
results = RubyReactor::Map::ResultEnumerator.new(
|
|
53
|
+
map_id,
|
|
22
54
|
parent_reactor_class_name,
|
|
23
|
-
|
|
55
|
+
strict_ordering: strict_ordering
|
|
24
56
|
)
|
|
25
57
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
strict_ordering: strict_ordering)
|
|
58
|
+
# Apply collect block (or default collection)
|
|
59
|
+
step_config = parent_context.reactor_class.steps[step_name.to_sym]
|
|
29
60
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
begin
|
|
62
|
+
final_result = apply_collect_block(results, step_config)
|
|
63
|
+
|
|
64
|
+
if final_result.failure?
|
|
65
|
+
# Optionally log failure internally or just rely on context status update
|
|
35
66
|
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
final_result = RubyReactor::Failure(e)
|
|
36
69
|
end
|
|
37
70
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
# Resume parent execution
|
|
72
|
+
resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
puts "COLLECTOR CRASH: #{e.message}"
|
|
75
|
+
puts e.backtrace
|
|
76
|
+
raise e
|
|
77
|
+
end
|
|
41
78
|
|
|
42
|
-
|
|
79
|
+
def self.apply_collect_block(results, step_config)
|
|
80
|
+
collect_block = step_config.arguments[:collect_block][:source].value if step_config.arguments[:collect_block]
|
|
43
81
|
# TODO: Check allow_partial_failure option
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
83
|
+
if collect_block
|
|
84
|
+
begin
|
|
85
|
+
# Pass Enumerator to collect block
|
|
86
|
+
collected = collect_block.call(results)
|
|
87
|
+
RubyReactor::Success(collected)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
puts "COLLECTOR INNER EXCEPTION: #{e.message}"
|
|
90
|
+
puts e.backtrace
|
|
91
|
+
RubyReactor::Failure(e)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
# Default behavior: Return Success(Enumerator).
|
|
95
|
+
# Logic for checking failures is deferred to the consumer of the enumerator.
|
|
96
|
+
RubyReactor::Success(results)
|
|
97
|
+
end
|
|
61
98
|
end
|
|
62
|
-
# rubocop:enable Metrics/MethodLength
|
|
63
99
|
end
|
|
64
100
|
end
|
|
65
101
|
end
|