ruby_reactor 0.4.1 → 0.5.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/.release-please-manifest.json +1 -1
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +8 -2
- data/lib/ruby_reactor/configuration.rb +6 -1
- data/lib/ruby_reactor/context.rb +2 -1
- data/lib/ruby_reactor/context_serializer.rb +1 -3
- data/lib/ruby_reactor/dsl/reactor.rb +6 -2
- data/lib/ruby_reactor/executor/compensation_manager.rb +75 -47
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -5
- data/lib/ruby_reactor/executor/step_executor.rb +36 -18
- data/lib/ruby_reactor/executor.rb +112 -36
- data/lib/ruby_reactor/map/collector.rb +4 -4
- data/lib/ruby_reactor/map/element_executor.rb +15 -1
- data/lib/ruby_reactor/map/helpers.rb +17 -4
- data/lib/ruby_reactor/middleware.rb +13 -0
- data/lib/ruby_reactor/middleware_runner.rb +29 -0
- data/lib/ruby_reactor/open_telemetry.rb +647 -0
- data/lib/ruby_reactor/reactor.rb +1 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +0 -1
- data/lib/ruby_reactor/sidekiq_adapter.rb +7 -21
- data/lib/ruby_reactor/step/map_step.rb +25 -33
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/coordination_serializer.rb +12 -18
- data/teley/Dockerfile +60 -0
- metadata +5 -3
- data/lib/ruby_reactor/map/execution.rb +0 -101
- data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +0 -15
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require_relative "executor/input_validator"
|
|
4
5
|
require_relative "executor/graph_manager"
|
|
5
6
|
require_relative "executor/retry_manager"
|
|
@@ -8,16 +9,19 @@ require_relative "executor/result_handler"
|
|
|
8
9
|
require_relative "executor/step_executor"
|
|
9
10
|
|
|
10
11
|
module RubyReactor
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
|
11
13
|
class Executor
|
|
12
14
|
attr_reader :reactor_class, :context, :dependency_graph, :compensation_manager, :retry_manager, :result_handler,
|
|
13
|
-
:step_executor, :result
|
|
15
|
+
:step_executor, :result, :middlewares
|
|
14
16
|
|
|
15
17
|
def initialize(reactor_class, inputs = {}, context = nil)
|
|
16
18
|
@reactor_class = reactor_class
|
|
17
19
|
@context = context || Context.new(inputs, reactor_class)
|
|
20
|
+
@middlewares = Executor.middlewares_for(reactor_class)
|
|
21
|
+
@context.middlewares = @middlewares
|
|
18
22
|
@dependency_graph = DependencyGraph.new
|
|
19
23
|
@compensation_manager = CompensationManager.new(@context)
|
|
20
|
-
@retry_manager = RetryManager.new(@context)
|
|
24
|
+
@retry_manager = RetryManager.new(@context, @middlewares)
|
|
21
25
|
@result_handler = ResultHandler.new(
|
|
22
26
|
context: @context,
|
|
23
27
|
compensation_manager: @compensation_manager,
|
|
@@ -30,7 +34,8 @@ module RubyReactor
|
|
|
30
34
|
managers: {
|
|
31
35
|
retry_manager: @retry_manager,
|
|
32
36
|
result_handler: @result_handler,
|
|
33
|
-
compensation_manager: @compensation_manager
|
|
37
|
+
compensation_manager: @compensation_manager,
|
|
38
|
+
middlewares: @middlewares
|
|
34
39
|
}
|
|
35
40
|
)
|
|
36
41
|
@result = nil
|
|
@@ -38,17 +43,44 @@ module RubyReactor
|
|
|
38
43
|
@acquired_semaphore = nil
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
def
|
|
46
|
+
def self.resolve_middlewares(reactor_class)
|
|
47
|
+
global_list = Array(RubyReactor.configuration.middlewares)
|
|
48
|
+
reactor_list = if reactor_class.respond_to?(:middlewares)
|
|
49
|
+
Array(reactor_class.middlewares)
|
|
50
|
+
else
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
(global_list + reactor_list).map do |mw|
|
|
55
|
+
if mw.is_a?(Class)
|
|
56
|
+
mw.new
|
|
57
|
+
elsif mw.is_a?(Array) && mw.first.is_a?(Class)
|
|
58
|
+
klass, opts = mw
|
|
59
|
+
klass.new(**(opts || {}))
|
|
60
|
+
else
|
|
61
|
+
mw
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.middlewares_for(reactor_class)
|
|
67
|
+
RubyReactor::MiddlewareRunner.new(resolve_middlewares(reactor_class))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute # rubocop:disable Metrics/MethodLength
|
|
71
|
+
middlewares.on(:start_reactor, reactor_class.name, context.inputs, @context)
|
|
72
|
+
completed = false
|
|
73
|
+
|
|
42
74
|
skipped = check_period_gate
|
|
43
75
|
if skipped
|
|
44
76
|
@result = skipped
|
|
45
77
|
update_context_status(@result)
|
|
46
78
|
save_context
|
|
79
|
+
completed = true
|
|
47
80
|
return @result
|
|
48
81
|
end
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
acquire_locks
|
|
83
|
+
acquire_locks_with_telemetry
|
|
52
84
|
|
|
53
85
|
input_validator = InputValidator.new(@reactor_class, @context)
|
|
54
86
|
input_validator.validate!
|
|
@@ -64,6 +96,7 @@ module RubyReactor
|
|
|
64
96
|
update_context_status(@result)
|
|
65
97
|
mark_period_on_success(@result)
|
|
66
98
|
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
99
|
+
completed = true
|
|
67
100
|
@result
|
|
68
101
|
rescue RubyReactor::Lock::AcquisitionError,
|
|
69
102
|
RubyReactor::Semaphore::AcquisitionError,
|
|
@@ -72,41 +105,60 @@ module RubyReactor
|
|
|
72
105
|
rescue StandardError => e
|
|
73
106
|
@result = @result_handler.handle_execution_error(e)
|
|
74
107
|
update_context_status(@result)
|
|
108
|
+
completed = true
|
|
75
109
|
@result
|
|
76
110
|
ensure
|
|
77
111
|
release_locks
|
|
78
112
|
save_context if persist_context?
|
|
113
|
+
|
|
114
|
+
if completed
|
|
115
|
+
middlewares.on(:complete_reactor, reactor_class.name, @result, @context)
|
|
116
|
+
else
|
|
117
|
+
middlewares.on(:failed_reactor, reactor_class.name, $ERROR_INFO, @context)
|
|
118
|
+
end
|
|
79
119
|
end
|
|
80
120
|
|
|
81
121
|
def resume_execution
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
execute_remaining_steps
|
|
91
|
-
end
|
|
122
|
+
middlewares.on(:start_reactor, reactor_class.name, context.inputs, @context)
|
|
123
|
+
completed = false
|
|
124
|
+
begin
|
|
125
|
+
@context.status = :running
|
|
126
|
+
acquire_exclusive_lock if @reactor_class.respond_to?(:lock_config) && @reactor_class.lock_config
|
|
127
|
+
acquire_semaphore if @reactor_class.respond_to?(:semaphore_config) && @reactor_class.semaphore_config
|
|
128
|
+
prepare_for_resume
|
|
129
|
+
save_context
|
|
92
130
|
|
|
93
|
-
|
|
94
|
-
|
|
131
|
+
@result = if @context.current_step
|
|
132
|
+
execute_current_step_and_continue
|
|
133
|
+
else
|
|
134
|
+
execute_remaining_steps
|
|
135
|
+
end
|
|
95
136
|
|
|
96
|
-
|
|
137
|
+
update_context_status(@result)
|
|
138
|
+
mark_period_on_success(@result)
|
|
139
|
+
|
|
140
|
+
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
141
|
+
completed = true
|
|
142
|
+
@result
|
|
143
|
+
rescue RubyReactor::Lock::AcquisitionError,
|
|
144
|
+
RubyReactor::Semaphore::AcquisitionError,
|
|
145
|
+
RubyReactor::RateLimit::ExceededError
|
|
146
|
+
raise
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
handle_resume_error(e)
|
|
149
|
+
update_context_status(@result)
|
|
150
|
+
completed = true
|
|
151
|
+
@result
|
|
152
|
+
ensure
|
|
153
|
+
release_locks
|
|
154
|
+
save_context
|
|
97
155
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
handle_resume_error(e)
|
|
105
|
-
update_context_status(@result)
|
|
106
|
-
@result
|
|
107
|
-
ensure
|
|
108
|
-
release_locks
|
|
109
|
-
save_context
|
|
156
|
+
if completed
|
|
157
|
+
middlewares.on(:complete_reactor, reactor_class.name, @result, @context)
|
|
158
|
+
else
|
|
159
|
+
middlewares.on(:failed_reactor, reactor_class.name, $ERROR_INFO, @context)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
110
162
|
end
|
|
111
163
|
|
|
112
164
|
def undo_all
|
|
@@ -143,10 +195,15 @@ module RubyReactor
|
|
|
143
195
|
private
|
|
144
196
|
|
|
145
197
|
def acquire_locks
|
|
198
|
+
check_rate_limit
|
|
146
199
|
acquire_exclusive_lock if @reactor_class.respond_to?(:lock_config) && @reactor_class.lock_config
|
|
147
200
|
acquire_semaphore if @reactor_class.respond_to?(:semaphore_config) && @reactor_class.semaphore_config
|
|
148
201
|
end
|
|
149
202
|
|
|
203
|
+
def acquire_locks_with_telemetry
|
|
204
|
+
acquire_locks
|
|
205
|
+
end
|
|
206
|
+
|
|
150
207
|
# Consume one slot from each configured rate-limit window. Raises
|
|
151
208
|
# `RubyReactor::RateLimit::ExceededError` (carrying a `retry_after_seconds`
|
|
152
209
|
# hint) if any window is full. Only consulted on initial `execute`; resumes
|
|
@@ -202,8 +259,14 @@ module RubyReactor
|
|
|
202
259
|
wait: contention_wait(config[:wait]),
|
|
203
260
|
auto_extend: config.fetch(:auto_extend, true)
|
|
204
261
|
)
|
|
205
|
-
|
|
206
|
-
|
|
262
|
+
begin
|
|
263
|
+
lock.acquire
|
|
264
|
+
@acquired_lock = lock
|
|
265
|
+
middlewares.on(:lock_acquired, key, @context)
|
|
266
|
+
rescue RubyReactor::Lock::AcquisitionError => e
|
|
267
|
+
middlewares.on(:lock_failed, key, e, @context)
|
|
268
|
+
raise
|
|
269
|
+
end
|
|
207
270
|
end
|
|
208
271
|
|
|
209
272
|
def acquire_semaphore
|
|
@@ -212,8 +275,14 @@ module RubyReactor
|
|
|
212
275
|
limit = config[:limit]
|
|
213
276
|
|
|
214
277
|
semaphore = RubyReactor::Semaphore.new(key, limit: limit, wait: contention_wait(config[:wait]))
|
|
215
|
-
|
|
216
|
-
|
|
278
|
+
begin
|
|
279
|
+
semaphore.acquire
|
|
280
|
+
@acquired_semaphore = semaphore
|
|
281
|
+
middlewares.on(:semaphore_acquired, key, limit, @context)
|
|
282
|
+
rescue RubyReactor::Semaphore::AcquisitionError => e
|
|
283
|
+
middlewares.on(:semaphore_failed, key, limit, e, @context)
|
|
284
|
+
raise
|
|
285
|
+
end
|
|
217
286
|
end
|
|
218
287
|
|
|
219
288
|
# Inside a Sidekiq worker we'd rather snooze the job via perform_in than
|
|
@@ -226,13 +295,19 @@ module RubyReactor
|
|
|
226
295
|
end
|
|
227
296
|
|
|
228
297
|
def release_locks
|
|
229
|
-
|
|
298
|
+
if @acquired_semaphore
|
|
299
|
+
key = @acquired_semaphore.key
|
|
300
|
+
release_one("semaphore", @acquired_semaphore)
|
|
301
|
+
middlewares.on(:semaphore_released, key, @context)
|
|
302
|
+
end
|
|
230
303
|
@acquired_semaphore = nil
|
|
231
304
|
|
|
232
305
|
return unless @acquired_lock
|
|
233
306
|
|
|
307
|
+
key = @acquired_lock.key
|
|
234
308
|
release_one("lock", @acquired_lock)
|
|
235
309
|
@acquired_lock = nil
|
|
310
|
+
middlewares.on(:lock_released, key, @context)
|
|
236
311
|
end
|
|
237
312
|
|
|
238
313
|
def release_one(kind, primitive)
|
|
@@ -332,4 +407,5 @@ module RubyReactor
|
|
|
332
407
|
)
|
|
333
408
|
end
|
|
334
409
|
end
|
|
410
|
+
# rubocop:enable Metrics/ClassLength
|
|
335
411
|
end
|
|
@@ -59,8 +59,8 @@ module RubyReactor
|
|
|
59
59
|
# Resume parent execution
|
|
60
60
|
resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
61
61
|
rescue StandardError => e
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
RubyReactor.configuration.logger.error("Map collector crashed: #{e.message}")
|
|
63
|
+
RubyReactor.configuration.logger.error(e.backtrace.join("\n")) if e.backtrace
|
|
64
64
|
raise e
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -74,8 +74,8 @@ module RubyReactor
|
|
|
74
74
|
collected = collect_block.call(results)
|
|
75
75
|
RubyReactor::Success(collected)
|
|
76
76
|
rescue StandardError => e
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
RubyReactor.configuration.logger.error("Map collect block raised: #{e.message}")
|
|
78
|
+
RubyReactor.configuration.logger.error(e.backtrace.join("\n")) if e.backtrace
|
|
79
79
|
RubyReactor::Failure(e)
|
|
80
80
|
end
|
|
81
81
|
else
|
|
@@ -9,6 +9,12 @@ module RubyReactor
|
|
|
9
9
|
arguments = arguments.transform_keys(&:to_sym)
|
|
10
10
|
|
|
11
11
|
context = hydrate_or_create_context(arguments)
|
|
12
|
+
# The element already runs inside its own background worker, so any async
|
|
13
|
+
# steps (and async retries) must execute inline here rather than handing
|
|
14
|
+
# off to a detached Worker that would escape map result/counter tracking.
|
|
15
|
+
# This mirrors SidekiqWorkers::Worker, which sets the same flag.
|
|
16
|
+
context.inline_async_execution = true
|
|
17
|
+
|
|
12
18
|
storage = RubyReactor.configuration.storage_adapter
|
|
13
19
|
storage.store_map_element_context_id(arguments[:map_id], context.context_id,
|
|
14
20
|
arguments[:parent_reactor_class_name])
|
|
@@ -18,7 +24,15 @@ module RubyReactor
|
|
|
18
24
|
executor = Executor.new(context.reactor_class, {}, context)
|
|
19
25
|
arguments[:serialized_context] ? executor.resume_execution : executor.execute
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
result = executor.result
|
|
28
|
+
|
|
29
|
+
# An async retry requeued this element as a fresh MapElementWorker job, so
|
|
30
|
+
# it is not finished yet. Do not store a result, decrement the completion
|
|
31
|
+
# counter, or trigger the next batch — the requeued job will do that when
|
|
32
|
+
# the element ultimately succeeds or exhausts its retries.
|
|
33
|
+
return if result.is_a?(RetryQueuedResult)
|
|
34
|
+
|
|
35
|
+
handle_result(result, arguments, context, storage, executor)
|
|
22
36
|
finalize_execution(arguments, storage)
|
|
23
37
|
end
|
|
24
38
|
|
|
@@ -52,7 +52,7 @@ module RubyReactor
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Resumes parent reactor execution after map completion
|
|
55
|
-
def resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
55
|
+
def resume_parent_execution(parent_context, step_name, final_result, storage) # rubocop:disable Metrics/MethodLength
|
|
56
56
|
executor = RubyReactor::Executor.new(parent_context.reactor_class, {}, parent_context)
|
|
57
57
|
step_name_sym = step_name.to_sym
|
|
58
58
|
|
|
@@ -74,9 +74,22 @@ module RubyReactor
|
|
|
74
74
|
error.set_backtrace(final_result.error.backtrace)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
77
|
+
# Bracket the rollback with reactor lifecycle events so that
|
|
78
|
+
# compensation/undo spans nest under a reactor span (and stay attached
|
|
79
|
+
# to the originating trace), mirroring the success path's
|
|
80
|
+
# resume_execution. Without this, rollback runs in the collector worker
|
|
81
|
+
# with no active reactor span and the undo/compensation spans orphan.
|
|
82
|
+
executor.middlewares.on(:start_reactor, parent_context.reactor_class.name, parent_context.inputs,
|
|
83
|
+
parent_context)
|
|
84
|
+
failure_response = nil
|
|
85
|
+
begin
|
|
86
|
+
failure_response = executor.result_handler.handle_execution_error(error)
|
|
87
|
+
# Manually update context status since we're not running executor loop
|
|
88
|
+
executor.send(:update_context_status, failure_response)
|
|
89
|
+
ensure
|
|
90
|
+
executor.middlewares.on(:failed_reactor, parent_context.reactor_class.name, failure_response,
|
|
91
|
+
parent_context)
|
|
92
|
+
end
|
|
80
93
|
else
|
|
81
94
|
parent_context.set_result(step_name_sym, final_result.value)
|
|
82
95
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
# Base class for all middlewares in RubyReactor.
|
|
5
|
+
# Middlewares allow hooking into the execution lifecycle of reactors and steps.
|
|
6
|
+
class Middleware
|
|
7
|
+
attr_reader :options
|
|
8
|
+
|
|
9
|
+
def initialize(**options)
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
# MiddlewareRunner executes event hooks on a collection of configured middlewares.
|
|
5
|
+
class MiddlewareRunner
|
|
6
|
+
def initialize(middlewares)
|
|
7
|
+
@middlewares = middlewares || []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Dispatches the given lifecycle event to all configured middlewares.
|
|
11
|
+
# Invokes the specific event hook method if defined, e.g., `on_start_reactor`.
|
|
12
|
+
# Fallback to the generic `on` method if implemented on the middleware.
|
|
13
|
+
# StandardErrors are swallowed and logged to prevent middleware failure from halting execution.
|
|
14
|
+
def on(event, *args)
|
|
15
|
+
@middlewares.each do |middleware|
|
|
16
|
+
method_name = "on_#{event}"
|
|
17
|
+
if middleware.respond_to?(method_name)
|
|
18
|
+
middleware.send(method_name, *args)
|
|
19
|
+
elsif middleware.respond_to?(:on)
|
|
20
|
+
middleware.on(event, *args)
|
|
21
|
+
end
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
RubyReactor.configuration.logger.warn(
|
|
24
|
+
"RubyReactor middleware error in #{middleware.class} during #{event}: #{e.message}"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|