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
|
@@ -4,18 +4,22 @@ 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
|
|
16
20
|
def self.perform_map_element_async(map_id:, element_id:, index:, serialized_inputs:, reactor_class_info:,
|
|
17
21
|
strict_ordering:, parent_context_id:, parent_reactor_class_name:, step_name:,
|
|
18
|
-
batch_size: nil, serialized_context: nil)
|
|
22
|
+
batch_size: nil, serialized_context: nil, fail_fast: nil)
|
|
19
23
|
RubyReactor::SidekiqWorkers::MapElementWorker.perform_async(
|
|
20
24
|
{
|
|
21
25
|
"map_id" => map_id,
|
|
@@ -28,7 +32,8 @@ module RubyReactor
|
|
|
28
32
|
"parent_reactor_class_name" => parent_reactor_class_name,
|
|
29
33
|
"step_name" => step_name,
|
|
30
34
|
"batch_size" => batch_size,
|
|
31
|
-
"serialized_context" => serialized_context
|
|
35
|
+
"serialized_context" => serialized_context,
|
|
36
|
+
"fail_fast" => fail_fast
|
|
32
37
|
}
|
|
33
38
|
)
|
|
34
39
|
end
|
|
@@ -71,9 +76,8 @@ module RubyReactor
|
|
|
71
76
|
end
|
|
72
77
|
# rubocop:enable Metrics/ParameterLists
|
|
73
78
|
|
|
74
|
-
# rubocop:disable Metrics/ParameterLists
|
|
75
79
|
def self.perform_map_execution_async(map_id:, serialized_inputs:, reactor_class_info:, strict_ordering:,
|
|
76
|
-
parent_context_id:, parent_reactor_class_name:, step_name:)
|
|
80
|
+
parent_context_id:, parent_reactor_class_name:, step_name:, fail_fast: nil)
|
|
77
81
|
RubyReactor::SidekiqWorkers::MapExecutionWorker.perform_async(
|
|
78
82
|
{
|
|
79
83
|
"map_id" => map_id,
|
|
@@ -82,10 +86,10 @@ module RubyReactor
|
|
|
82
86
|
"strict_ordering" => strict_ordering,
|
|
83
87
|
"parent_context_id" => parent_context_id,
|
|
84
88
|
"parent_reactor_class_name" => parent_reactor_class_name,
|
|
85
|
-
"step_name" => step_name
|
|
89
|
+
"step_name" => step_name,
|
|
90
|
+
"fail_fast" => fail_fast
|
|
86
91
|
}
|
|
87
92
|
)
|
|
88
93
|
end
|
|
89
|
-
# rubocop:enable Metrics/ParameterLists
|
|
90
94
|
end
|
|
91
95
|
end
|
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)
|
|
@@ -68,6 +68,14 @@ module RubyReactor
|
|
|
68
68
|
{ "_type" => "Template::Value", "value" => serialize_value(value.instance_variable_get(:@value)) }
|
|
69
69
|
when RubyReactor::Template::Result
|
|
70
70
|
{ "_type" => "Template::Result", "step_name" => value.step_name.to_s, "path" => value.path }
|
|
71
|
+
when RubyReactor::Map::ResultEnumerator
|
|
72
|
+
{
|
|
73
|
+
"_type" => "Map::ResultEnumerator",
|
|
74
|
+
"map_id" => value.map_id,
|
|
75
|
+
"reactor_class_name" => value.reactor_class_name,
|
|
76
|
+
"strict_ordering" => value.strict_ordering,
|
|
77
|
+
"batch_size" => value.batch_size
|
|
78
|
+
}
|
|
71
79
|
when Hash
|
|
72
80
|
value.transform_keys(&:to_s).transform_values { |v| serialize_value(v) }
|
|
73
81
|
when Array
|
|
@@ -115,6 +123,13 @@ module RubyReactor
|
|
|
115
123
|
RubyReactor::Template::Value.new(deserialize_value(value["value"]))
|
|
116
124
|
when "Template::Result"
|
|
117
125
|
RubyReactor::Template::Result.new(value["step_name"], value["path"])
|
|
126
|
+
when "Map::ResultEnumerator"
|
|
127
|
+
RubyReactor::Map::ResultEnumerator.new(
|
|
128
|
+
value["map_id"],
|
|
129
|
+
value["reactor_class_name"],
|
|
130
|
+
strict_ordering: value["strict_ordering"],
|
|
131
|
+
batch_size: value["batch_size"]
|
|
132
|
+
)
|
|
118
133
|
else
|
|
119
134
|
value
|
|
120
135
|
end
|
|
@@ -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
|
|
@@ -24,8 +32,12 @@ module RubyReactor
|
|
|
24
32
|
@argument_mappings[mapped_input_name] = source
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
def source(enumerable)
|
|
28
|
-
@source_enumerable =
|
|
35
|
+
def source(enumerable = nil, &block)
|
|
36
|
+
@source_enumerable = if block
|
|
37
|
+
RubyReactor::Template::DynamicSource.new(@argument_mappings, &block)
|
|
38
|
+
else
|
|
39
|
+
enumerable
|
|
40
|
+
end
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
def async(async = true, batch_size: nil)
|
|
@@ -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
|