ruby_reactor 0.1.0 → 0.2.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 +72 -3
- data/Rakefile +27 -2
- 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 +6 -2
- data/lib/ruby_reactor/context.rb +35 -9
- 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 +8 -0
- 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 +117 -39
- data/lib/ruby_reactor/executor/retry_manager.rb +1 -0
- 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 +0 -2
- data/lib/ruby_reactor/map/element_executor.rb +3 -0
- data/lib/ruby_reactor/map/execution.rb +28 -1
- data/lib/ruby_reactor/map/helpers.rb +44 -6
- 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 +30 -3
- data/lib/ruby_reactor/storage/adapter.rb +32 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +154 -11
- 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 +63 -2
|
@@ -4,12 +4,16 @@ module RubyReactor
|
|
|
4
4
|
class AsyncRouter
|
|
5
5
|
def self.perform_async(serialized_context, reactor_class_name = nil, intermediate_results: {})
|
|
6
6
|
job_id = SidekiqWorkers::Worker.perform_async(serialized_context, reactor_class_name)
|
|
7
|
-
|
|
7
|
+
context = ContextSerializer.deserialize(serialized_context)
|
|
8
|
+
RubyReactor::AsyncResult.new(job_id: job_id, intermediate_results: intermediate_results,
|
|
9
|
+
execution_id: context.context_id)
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def self.perform_in(delay, serialized_context, reactor_class_name = nil, intermediate_results: {})
|
|
11
13
|
job_id = SidekiqWorkers::Worker.perform_in(delay, serialized_context, reactor_class_name)
|
|
12
|
-
|
|
14
|
+
context = ContextSerializer.deserialize(serialized_context)
|
|
15
|
+
RubyReactor::AsyncResult.new(job_id: job_id, intermediate_results: intermediate_results,
|
|
16
|
+
execution_id: context.context_id)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
# rubocop:disable Metrics/ParameterLists
|
data/lib/ruby_reactor/context.rb
CHANGED
|
@@ -4,7 +4,8 @@ module RubyReactor
|
|
|
4
4
|
class Context
|
|
5
5
|
attr_accessor :inputs, :intermediate_results, :private_data, :current_step, :retry_count, :concurrency_key,
|
|
6
6
|
:retry_context, :reactor_class, :execution_trace, :inline_async_execution, :undo_stack, :test_mode,
|
|
7
|
-
:parent_context, :root_context, :composed_contexts, :context_id, :map_operations, :map_metadata
|
|
7
|
+
:parent_context, :root_context, :composed_contexts, :context_id, :map_operations, :map_metadata,
|
|
8
|
+
:cancelled, :cancellation_reason, :parent_context_id, :status, :failure_reason
|
|
8
9
|
|
|
9
10
|
def initialize(inputs = {}, reactor_class = nil)
|
|
10
11
|
@context_id = SecureRandom.uuid
|
|
@@ -23,7 +24,12 @@ module RubyReactor
|
|
|
23
24
|
@inline_async_execution = false # Flag to prevent nested async calls
|
|
24
25
|
@undo_stack = [] # Initialize the undo stack
|
|
25
26
|
@test_mode = false
|
|
27
|
+
@cancelled = false
|
|
28
|
+
@cancellation_reason = nil
|
|
29
|
+
@status = "pending"
|
|
30
|
+
@failure_reason = nil
|
|
26
31
|
@parent_context = nil
|
|
32
|
+
@parent_context_id = nil
|
|
27
33
|
@root_context = nil
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -37,6 +43,7 @@ module RubyReactor
|
|
|
37
43
|
value
|
|
38
44
|
end
|
|
39
45
|
end
|
|
46
|
+
alias input get_input
|
|
40
47
|
|
|
41
48
|
def get_result(step_name, path = nil)
|
|
42
49
|
value = @intermediate_results[step_name.to_sym] || @intermediate_results[step_name.to_s]
|
|
@@ -48,6 +55,7 @@ module RubyReactor
|
|
|
48
55
|
value
|
|
49
56
|
end
|
|
50
57
|
end
|
|
58
|
+
alias result get_result
|
|
51
59
|
|
|
52
60
|
def set_result(step_name, value)
|
|
53
61
|
@intermediate_results[step_name.to_sym] = value
|
|
@@ -73,7 +81,9 @@ module RubyReactor
|
|
|
73
81
|
retry_context: @retry_context,
|
|
74
82
|
reactor_class: @reactor_class,
|
|
75
83
|
execution_trace: @execution_trace,
|
|
76
|
-
test_mode: @test_mode
|
|
84
|
+
test_mode: @test_mode,
|
|
85
|
+
status: @status,
|
|
86
|
+
failure_reason: @failure_reason
|
|
77
87
|
}
|
|
78
88
|
end
|
|
79
89
|
|
|
@@ -95,14 +105,19 @@ module RubyReactor
|
|
|
95
105
|
retry_context: @retry_context.serialize_for_retry,
|
|
96
106
|
execution_trace: ContextSerializer.serialize_value(@execution_trace),
|
|
97
107
|
undo_stack: serialize_undo_stack,
|
|
98
|
-
test_mode: @test_mode
|
|
108
|
+
test_mode: @test_mode,
|
|
109
|
+
cancelled: @cancelled,
|
|
110
|
+
cancellation_reason: @cancellation_reason,
|
|
111
|
+
status: @status,
|
|
112
|
+
failure_reason: ContextSerializer.serialize_value(@failure_reason),
|
|
113
|
+
parent_context_id: @parent_context&.context_id || @parent_context_id
|
|
99
114
|
}
|
|
100
115
|
end
|
|
101
116
|
|
|
102
117
|
def self.deserialize_from_retry(data)
|
|
103
118
|
context = new
|
|
104
119
|
context.context_id = data["context_id"] if data["context_id"]
|
|
105
|
-
context.reactor_class =
|
|
120
|
+
context.reactor_class = resolve_reactor_class(data["reactor_class"])
|
|
106
121
|
context.inputs = ContextSerializer.deserialize_value(data["inputs"]) || {}
|
|
107
122
|
context.intermediate_results = ContextSerializer.deserialize_value(data["intermediate_results"]) || {}
|
|
108
123
|
context.private_data = ContextSerializer.deserialize_value(data["private_data"]) || {}
|
|
@@ -116,15 +131,26 @@ module RubyReactor
|
|
|
116
131
|
context.execution_trace = ContextSerializer.deserialize_value(data["execution_trace"]) || []
|
|
117
132
|
context.undo_stack = deserialize_undo_stack(data["undo_stack"] || [], context.reactor_class)
|
|
118
133
|
context.test_mode = data["test_mode"] || false
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
context.cancelled = data["cancelled"] || false
|
|
135
|
+
context.cancellation_reason = data["cancellation_reason"]
|
|
136
|
+
context.status = data["status"] || "pending"
|
|
137
|
+
context.failure_reason = ContextSerializer.deserialize_value(data["failure_reason"])
|
|
138
|
+
context.parent_context_id = data["parent_context_id"]
|
|
124
139
|
|
|
125
140
|
context
|
|
126
141
|
end
|
|
127
142
|
|
|
143
|
+
def self.resolve_reactor_class(name)
|
|
144
|
+
return nil unless name
|
|
145
|
+
|
|
146
|
+
begin
|
|
147
|
+
Object.const_get(name)
|
|
148
|
+
rescue NameError
|
|
149
|
+
# Try finding in registry for anonymous classes
|
|
150
|
+
RubyReactor::Registry.find(name)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
128
154
|
private
|
|
129
155
|
|
|
130
156
|
def extract_path(value, path)
|
|
@@ -10,6 +10,14 @@ module RubyReactor
|
|
|
10
10
|
def initialize(name, composed_reactor_class = nil, reactor = nil, &block)
|
|
11
11
|
@name = name
|
|
12
12
|
@composed_reactor_class = composed_reactor_class || (block ? Class.new(RubyReactor::Reactor) : nil)
|
|
13
|
+
if @composed_reactor_class && @composed_reactor_class.name.nil? && reactor
|
|
14
|
+
step_name_camel = name.to_s.split("_").map(&:capitalize).join
|
|
15
|
+
parent_name = reactor.name
|
|
16
|
+
|
|
17
|
+
full_name = "#{parent_name}::#{step_name_camel}"
|
|
18
|
+
@composed_reactor_class.define_singleton_method(:name) { full_name }
|
|
19
|
+
RubyReactor::Registry.register(full_name, @composed_reactor_class)
|
|
20
|
+
end
|
|
13
21
|
@reactor = reactor
|
|
14
22
|
@argument_mappings = {}
|
|
15
23
|
@async = false
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
class InterruptBuilder < StepBuilder
|
|
6
|
+
def initialize(name, reactor = nil)
|
|
7
|
+
super(name, nil, reactor)
|
|
8
|
+
@correlation_id_block = nil
|
|
9
|
+
@timeout_config = nil
|
|
10
|
+
@validation_schema = nil
|
|
11
|
+
@max_attempts = 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def max_attempts(count)
|
|
15
|
+
@max_attempts = count
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def correlation_id(&block)
|
|
19
|
+
@correlation_id_block = block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def timeout(seconds, strategy: :lazy)
|
|
23
|
+
@timeout_config = { duration: seconds, strategy: strategy }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate(&block)
|
|
27
|
+
check_dry_validation_available!
|
|
28
|
+
@validation_schema = build_validation_schema(&block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build
|
|
32
|
+
step_config = {
|
|
33
|
+
name: @name,
|
|
34
|
+
correlation_id_block: @correlation_id_block,
|
|
35
|
+
timeout_config: @timeout_config,
|
|
36
|
+
validation_schema: @validation_schema,
|
|
37
|
+
max_attempts: @max_attempts,
|
|
38
|
+
dependencies: @dependencies,
|
|
39
|
+
async: false, # Interrupts are effectively boundaries, not async jobs themselves (until resumed)
|
|
40
|
+
conditions: @conditions,
|
|
41
|
+
guards: @guards
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
RubyReactor::Dsl::InterruptStepConfig.new(step_config)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Dsl
|
|
5
|
+
class InterruptStepConfig < StepConfig
|
|
6
|
+
attr_reader :correlation_id_block, :timeout_config, :validation_schema, :strategy, :max_attempts
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
super
|
|
10
|
+
@correlation_id_block = config[:correlation_id_block]
|
|
11
|
+
@timeout_config = config[:timeout_config]
|
|
12
|
+
@validation_schema = config[:validation_schema]
|
|
13
|
+
@max_attempts = config[:max_attempts] || 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def interrupt?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -10,6 +10,14 @@ module RubyReactor
|
|
|
10
10
|
def initialize(name, mapped_reactor_class = nil, reactor = nil, &block)
|
|
11
11
|
@name = name
|
|
12
12
|
@mapped_reactor_class = mapped_reactor_class || (block ? Class.new(RubyReactor::Reactor) : nil)
|
|
13
|
+
if @mapped_reactor_class && @mapped_reactor_class.name.nil? && reactor
|
|
14
|
+
map_name_camel = name.to_s.split("_").map(&:capitalize).join
|
|
15
|
+
parent_name = reactor.name # Assuming reactor has a name method or is a class with a name.
|
|
16
|
+
|
|
17
|
+
full_name = "#{parent_name}::#{map_name_camel}"
|
|
18
|
+
@mapped_reactor_class.define_singleton_method(:name) { full_name }
|
|
19
|
+
RubyReactor::Registry.register(full_name, @mapped_reactor_class)
|
|
20
|
+
end
|
|
13
21
|
@reactor = reactor
|
|
14
22
|
@argument_mappings = {}
|
|
15
23
|
@async = false
|
|
@@ -18,6 +18,9 @@ module RubyReactor
|
|
|
18
18
|
include RubyReactor::Dsl::TemplateHelpers
|
|
19
19
|
include RubyReactor::Dsl::ValidationHelpers
|
|
20
20
|
|
|
21
|
+
require_relative "interrupt_builder"
|
|
22
|
+
require_relative "interrupt_step_config"
|
|
23
|
+
|
|
21
24
|
def inputs
|
|
22
25
|
@inputs ||= {}
|
|
23
26
|
end
|
|
@@ -106,6 +109,15 @@ module RubyReactor
|
|
|
106
109
|
step_config
|
|
107
110
|
end
|
|
108
111
|
|
|
112
|
+
def interrupt(name, &block)
|
|
113
|
+
builder = RubyReactor::Dsl::InterruptBuilder.new(name, self)
|
|
114
|
+
builder.instance_eval(&block) if block_given?
|
|
115
|
+
|
|
116
|
+
step_config = builder.build
|
|
117
|
+
steps[name] = step_config
|
|
118
|
+
step_config
|
|
119
|
+
end
|
|
120
|
+
|
|
109
121
|
def returns(step_name)
|
|
110
122
|
@return_step = step_name
|
|
111
123
|
end
|
|
@@ -5,14 +5,17 @@ module RubyReactor
|
|
|
5
5
|
class CompensationManager
|
|
6
6
|
def initialize(context)
|
|
7
7
|
@context = context
|
|
8
|
-
@undo_stack = []
|
|
9
8
|
@undo_trace = []
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
def undo_stack
|
|
12
|
+
@context.undo_stack
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :undo_trace
|
|
13
16
|
|
|
14
17
|
def add_to_undo_stack(step_info)
|
|
15
|
-
@undo_stack << step_info
|
|
18
|
+
@context.undo_stack << step_info
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def handle_step_failure(step_config, error, arguments)
|
|
@@ -36,43 +39,73 @@ module RubyReactor
|
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def rollback_completed_steps
|
|
39
|
-
|
|
40
|
-
result =
|
|
42
|
+
undo_stack.reverse_each do |step_info|
|
|
43
|
+
result = @context.with_step(step_info[:step].name) do
|
|
44
|
+
undo_step(step_info[:step], step_info[:result], step_info[:arguments])
|
|
45
|
+
end
|
|
41
46
|
@undo_trace << { type: :undo, step: step_info[:step].name, result: result,
|
|
42
47
|
arguments: step_info[:arguments] }
|
|
43
48
|
end
|
|
44
|
-
|
|
49
|
+
undo_stack.clear
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
private
|
|
48
53
|
|
|
49
54
|
def compensate_step(step_config, error, arguments)
|
|
50
|
-
if step_config.compensate_block
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
compensate_result = if step_config.compensate_block
|
|
56
|
+
step_config.compensate_block.call(error, arguments, @context)
|
|
57
|
+
elsif step_config.has_impl?
|
|
58
|
+
step_config.impl.compensate(error, arguments, @context)
|
|
59
|
+
else
|
|
60
|
+
RubyReactor.Success() # Default compensation
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ensure we have a value to log
|
|
64
|
+
logged_result = if compensate_result.respond_to?(:value)
|
|
65
|
+
compensate_result.value
|
|
66
|
+
elsif compensate_result.respond_to?(:error)
|
|
67
|
+
compensate_result.error
|
|
68
|
+
else
|
|
69
|
+
compensate_result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@context.execution_trace << {
|
|
73
|
+
type: :compensate,
|
|
74
|
+
step: step_config.name,
|
|
75
|
+
timestamp: Time.now,
|
|
76
|
+
result: logged_result,
|
|
77
|
+
arguments: arguments
|
|
78
|
+
}
|
|
79
|
+
@undo_trace << { type: :compensation, step: step_config.name, error: error, arguments: arguments }
|
|
80
|
+
compensate_result
|
|
63
81
|
end
|
|
64
82
|
|
|
65
83
|
def undo_step(step_config, result, arguments)
|
|
66
|
-
|
|
84
|
+
undo_result = if step_config.undo_block
|
|
85
|
+
step_config.undo_block.call(result.value, arguments, @context)
|
|
86
|
+
elsif step_config.has_impl?
|
|
87
|
+
step_config.impl.undo(result.value, arguments, @context)
|
|
88
|
+
else
|
|
89
|
+
RubyReactor.Success()
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Ensure we have a value to log (if it's a Success/Failure object, get the value or error)
|
|
93
|
+
logged_result = if undo_result.respond_to?(:value)
|
|
94
|
+
undo_result.value
|
|
95
|
+
elsif undo_result.respond_to?(:error)
|
|
96
|
+
undo_result.error
|
|
97
|
+
else
|
|
98
|
+
undo_result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
@context.execution_trace << { type: :undo, step: step_config.name, timestamp: Time.now, result: logged_result,
|
|
67
102
|
arguments: arguments }
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
elsif step_config.has_impl?
|
|
71
|
-
step_config.impl.undo(result.value, arguments, @context)
|
|
72
|
-
end
|
|
73
|
-
rescue StandardError
|
|
103
|
+
undo_result
|
|
104
|
+
rescue StandardError => e
|
|
74
105
|
# Log undo failure but don't halt the rollback process
|
|
75
|
-
|
|
106
|
+
@context.execution_trace << { type: :undo_failure, step: step_config.name, timestamp: Time.now,
|
|
107
|
+
error: e.message }
|
|
108
|
+
RubyReactor.Failure(e)
|
|
76
109
|
end
|
|
77
110
|
end
|
|
78
111
|
end
|
|
@@ -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,101 @@ 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
|
+
|
|
121
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
122
|
+
storage.store_map_failed_context_id(
|
|
123
|
+
context.map_metadata[:map_id],
|
|
124
|
+
context.context_id,
|
|
125
|
+
context.map_metadata[:parent_reactor_class_name]
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def create_failure_from_error(error, redact_inputs)
|
|
130
|
+
original_error = error.original_error
|
|
131
|
+
exception_class = original_error&.class&.name
|
|
132
|
+
backtrace = original_error&.backtrace || error.backtrace
|
|
133
|
+
file_path, line_number = extract_location(backtrace)
|
|
134
|
+
code_snippet = RubyReactor::Utils::CodeExtractor.extract(file_path, line_number) if file_path
|
|
135
|
+
|
|
136
|
+
RubyReactor.Failure(
|
|
137
|
+
error.message,
|
|
138
|
+
step_name: error.step,
|
|
139
|
+
inputs: error.context.inputs,
|
|
140
|
+
redact_inputs: redact_inputs,
|
|
141
|
+
backtrace: backtrace,
|
|
142
|
+
reactor_name: error.context.reactor_class.name,
|
|
143
|
+
step_arguments: error.step_arguments,
|
|
144
|
+
exception_class: exception_class,
|
|
145
|
+
file_path: file_path,
|
|
146
|
+
line_number: line_number,
|
|
147
|
+
code_snippet: code_snippet
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
88
151
|
def validate_step_output(step_config, value, resolved_arguments = {})
|
|
89
152
|
return unless step_config.output_validator
|
|
90
153
|
|
|
@@ -98,6 +161,21 @@ module RubyReactor
|
|
|
98
161
|
step_arguments: resolved_arguments
|
|
99
162
|
)
|
|
100
163
|
end
|
|
164
|
+
|
|
165
|
+
def extract_location(backtrace)
|
|
166
|
+
return [nil, nil] unless backtrace && !backtrace.empty?
|
|
167
|
+
|
|
168
|
+
# Filter out internal reactor frames if needed, or just take the first one
|
|
169
|
+
# For now, let's take the first line of the backtrace which should be the error source
|
|
170
|
+
# But we might want to skip our own internal frames if we want to point to user code
|
|
171
|
+
# Let's start with the top frame, assuming backtrace is already correct (from original error)
|
|
172
|
+
|
|
173
|
+
first_line = backtrace.first
|
|
174
|
+
match = first_line.match(/^(.+?):(\d+)(?::in `.*')?$/)
|
|
175
|
+
return [nil, nil] unless match
|
|
176
|
+
|
|
177
|
+
[match[1], match[2].to_i]
|
|
178
|
+
end
|
|
101
179
|
end
|
|
102
180
|
end
|
|
103
181
|
end
|