ruby_reactor 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +145 -9
- data/documentation/README.md +20 -8
- data/documentation/async_reactors.md +46 -34
- data/documentation/core_concepts.md +75 -61
- data/documentation/examples/inventory_management.md +2 -3
- data/documentation/examples/order_processing.md +92 -77
- data/documentation/examples/payment_processing.md +28 -117
- data/documentation/getting_started.md +112 -94
- data/documentation/interrupts.md +9 -7
- data/documentation/locks_and_semaphores.md +459 -0
- data/documentation/retry_configuration.md +19 -14
- data/documentation/testing.md +994 -0
- data/lib/ruby_reactor/configuration.rb +19 -2
- data/lib/ruby_reactor/context.rb +13 -5
- data/lib/ruby_reactor/context_serializer.rb +55 -4
- data/lib/ruby_reactor/dsl/lockable.rb +130 -0
- data/lib/ruby_reactor/dsl/reactor.rb +3 -2
- data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
- data/lib/ruby_reactor/executor/result_handler.rb +27 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
- data/lib/ruby_reactor/executor/step_executor.rb +29 -99
- data/lib/ruby_reactor/executor.rb +148 -15
- data/lib/ruby_reactor/lock.rb +92 -0
- data/lib/ruby_reactor/map/collector.rb +16 -15
- data/lib/ruby_reactor/map/element_executor.rb +90 -104
- data/lib/ruby_reactor/map/execution.rb +2 -1
- data/lib/ruby_reactor/map/helpers.rb +2 -1
- data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
- data/lib/ruby_reactor/period.rb +67 -0
- data/lib/ruby_reactor/rate_limit.rb +74 -0
- data/lib/ruby_reactor/reactor.rb +175 -16
- data/lib/ruby_reactor/rspec/helpers.rb +17 -0
- data/lib/ruby_reactor/rspec/matchers.rb +423 -0
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
- data/lib/ruby_reactor/rspec.rb +18 -0
- data/lib/ruby_reactor/semaphore.rb +58 -0
- data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +69 -9
- data/lib/ruby_reactor/step/compose_step.rb +0 -1
- data/lib/ruby_reactor/step/map_step.rb +11 -18
- data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
- data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +32 -24
- data/lib/ruby_reactor.rb +119 -10
- metadata +16 -3
|
@@ -34,12 +34,26 @@ module RubyReactor
|
|
|
34
34
|
}
|
|
35
35
|
)
|
|
36
36
|
@result = nil
|
|
37
|
+
@acquired_lock = nil
|
|
38
|
+
@acquired_semaphore = nil
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def execute
|
|
42
|
+
skipped = check_period_gate
|
|
43
|
+
if skipped
|
|
44
|
+
@result = skipped
|
|
45
|
+
update_context_status(@result)
|
|
46
|
+
save_context
|
|
47
|
+
return @result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
check_rate_limit
|
|
51
|
+
acquire_locks
|
|
52
|
+
|
|
40
53
|
input_validator = InputValidator.new(@reactor_class, @context)
|
|
41
54
|
input_validator.validate!
|
|
42
55
|
|
|
56
|
+
@context.status = :running
|
|
43
57
|
save_context
|
|
44
58
|
|
|
45
59
|
graph_manager = GraphManager.new(@reactor_class, @dependency_graph, @context)
|
|
@@ -48,17 +62,25 @@ module RubyReactor
|
|
|
48
62
|
|
|
49
63
|
@result = @step_executor.execute_all_steps
|
|
50
64
|
update_context_status(@result)
|
|
65
|
+
mark_period_on_success(@result)
|
|
51
66
|
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
52
67
|
@result
|
|
68
|
+
rescue RubyReactor::Lock::AcquisitionError,
|
|
69
|
+
RubyReactor::Semaphore::AcquisitionError,
|
|
70
|
+
RubyReactor::RateLimit::ExceededError => e
|
|
71
|
+
raise e
|
|
53
72
|
rescue StandardError => e
|
|
54
73
|
@result = @result_handler.handle_execution_error(e)
|
|
55
74
|
update_context_status(@result)
|
|
56
75
|
@result
|
|
57
76
|
ensure
|
|
77
|
+
release_locks
|
|
58
78
|
save_context
|
|
59
79
|
end
|
|
60
80
|
|
|
61
81
|
def resume_execution
|
|
82
|
+
@context.status = :running
|
|
83
|
+
acquire_locks
|
|
62
84
|
prepare_for_resume
|
|
63
85
|
save_context
|
|
64
86
|
|
|
@@ -69,15 +91,21 @@ module RubyReactor
|
|
|
69
91
|
end
|
|
70
92
|
|
|
71
93
|
update_context_status(@result)
|
|
94
|
+
mark_period_on_success(@result)
|
|
72
95
|
|
|
73
96
|
handle_interrupt(@result) if @result.is_a?(RubyReactor::InterruptResult)
|
|
74
97
|
|
|
75
98
|
@result
|
|
99
|
+
rescue RubyReactor::Lock::AcquisitionError,
|
|
100
|
+
RubyReactor::Semaphore::AcquisitionError,
|
|
101
|
+
RubyReactor::RateLimit::ExceededError => e
|
|
102
|
+
raise e
|
|
76
103
|
rescue StandardError => e
|
|
77
104
|
handle_resume_error(e)
|
|
78
105
|
update_context_status(@result)
|
|
79
106
|
@result
|
|
80
107
|
ensure
|
|
108
|
+
release_locks
|
|
81
109
|
save_context
|
|
82
110
|
end
|
|
83
111
|
|
|
@@ -108,29 +136,127 @@ module RubyReactor
|
|
|
108
136
|
|
|
109
137
|
private
|
|
110
138
|
|
|
139
|
+
def acquire_locks
|
|
140
|
+
acquire_exclusive_lock if @reactor_class.respond_to?(:lock_config) && @reactor_class.lock_config
|
|
141
|
+
acquire_semaphore if @reactor_class.respond_to?(:semaphore_config) && @reactor_class.semaphore_config
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Consume one slot from each configured rate-limit window. Raises
|
|
145
|
+
# `RubyReactor::RateLimit::ExceededError` (carrying a `retry_after_seconds`
|
|
146
|
+
# hint) if any window is full. Only consulted on initial `execute`; resumes
|
|
147
|
+
# never re-check (a paused reactor must not block itself on resume).
|
|
148
|
+
def check_rate_limit
|
|
149
|
+
return unless @reactor_class.respond_to?(:rate_limit_config) && @reactor_class.rate_limit_config
|
|
150
|
+
|
|
151
|
+
config = @reactor_class.rate_limit_config
|
|
152
|
+
key_base = config[:key_proc].call(@context.inputs)
|
|
153
|
+
|
|
154
|
+
RubyReactor::RateLimit.new(key_base, limits: config[:limits]).check_and_increment!
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns a Skipped result if the period bucket is already marked, else nil.
|
|
158
|
+
# Only consulted on initial `execute`; resumes never re-check (a paused run
|
|
159
|
+
# must not skip itself when its own marker eventually appears).
|
|
160
|
+
def check_period_gate
|
|
161
|
+
return nil unless @reactor_class.respond_to?(:period_config) && @reactor_class.period_config
|
|
162
|
+
|
|
163
|
+
config = @reactor_class.period_config
|
|
164
|
+
key = period_key(config)
|
|
165
|
+
return nil unless RubyReactor.configuration.storage_adapter.period_seen?(key)
|
|
166
|
+
|
|
167
|
+
RubyReactor::Skipped.new(reason: :period, period_key: key)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def mark_period_on_success(result)
|
|
171
|
+
return unless @reactor_class.respond_to?(:period_config) && @reactor_class.period_config
|
|
172
|
+
return unless result.is_a?(RubyReactor::Success)
|
|
173
|
+
return if result.is_a?(RubyReactor::Skipped)
|
|
174
|
+
|
|
175
|
+
config = @reactor_class.period_config
|
|
176
|
+
ttl = RubyReactor::Period.ttl_seconds(config[:every])
|
|
177
|
+
RubyReactor.configuration.storage_adapter.period_mark(period_key(config), ttl)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def period_key(config)
|
|
181
|
+
base = config[:key_proc].call(@context.inputs)
|
|
182
|
+
RubyReactor::Period.key(base, config[:every])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def acquire_exclusive_lock
|
|
186
|
+
config = @reactor_class.lock_config
|
|
187
|
+
key = config[:key_proc].call(@context.inputs)
|
|
188
|
+
|
|
189
|
+
# Use root context ID as owner to allow re-entrancy across nested reactors
|
|
190
|
+
owner = (@context.root_context || @context).context_id
|
|
191
|
+
|
|
192
|
+
lock = RubyReactor::Lock.new(
|
|
193
|
+
key,
|
|
194
|
+
owner: owner,
|
|
195
|
+
ttl: config[:ttl],
|
|
196
|
+
wait: contention_wait(config[:wait]),
|
|
197
|
+
auto_extend: config.fetch(:auto_extend, true)
|
|
198
|
+
)
|
|
199
|
+
lock.acquire
|
|
200
|
+
@acquired_lock = lock
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def acquire_semaphore
|
|
204
|
+
config = @reactor_class.semaphore_config
|
|
205
|
+
key = config[:key_proc].call(@context.inputs)
|
|
206
|
+
limit = config[:limit]
|
|
207
|
+
|
|
208
|
+
semaphore = RubyReactor::Semaphore.new(key, limit: limit, wait: contention_wait(config[:wait]))
|
|
209
|
+
semaphore.acquire
|
|
210
|
+
@acquired_semaphore = semaphore
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Inside a Sidekiq worker we'd rather snooze the job via perform_in than
|
|
214
|
+
# tie up the worker thread on a BLPOP / sleep loop. The non-blocking path
|
|
215
|
+
# fails fast and the Worker rescue branch reschedules.
|
|
216
|
+
def contention_wait(configured_wait)
|
|
217
|
+
return 0 if @context.inline_async_execution
|
|
218
|
+
|
|
219
|
+
configured_wait
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def release_locks
|
|
223
|
+
release_one("semaphore", @acquired_semaphore) if @acquired_semaphore
|
|
224
|
+
@acquired_semaphore = nil
|
|
225
|
+
|
|
226
|
+
return unless @acquired_lock
|
|
227
|
+
|
|
228
|
+
release_one("lock", @acquired_lock)
|
|
229
|
+
@acquired_lock = nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def release_one(kind, primitive)
|
|
233
|
+
released = primitive.release
|
|
234
|
+
return if released
|
|
235
|
+
|
|
236
|
+
RubyReactor.configuration.logger.warn(
|
|
237
|
+
"RubyReactor #{kind} '#{primitive.key}' was not held at release time " \
|
|
238
|
+
"(likely TTL expired or owner changed)"
|
|
239
|
+
)
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
# Never let release break the ensure chain — log and move on.
|
|
242
|
+
RubyReactor.configuration.logger.warn(
|
|
243
|
+
"RubyReactor failed to release #{kind} '#{primitive.key}': #{e.message}"
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
111
247
|
def update_context_status(result)
|
|
112
248
|
return unless result
|
|
113
249
|
|
|
114
250
|
case result
|
|
115
251
|
when RubyReactor::AsyncResult
|
|
116
252
|
@context.status = :running
|
|
253
|
+
when RubyReactor::Skipped
|
|
254
|
+
@context.status = :skipped
|
|
117
255
|
when RubyReactor::Success
|
|
118
256
|
@context.status = :completed
|
|
119
257
|
when RubyReactor::Failure
|
|
120
258
|
@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
|
-
}
|
|
259
|
+
@context.failure_reason = result
|
|
134
260
|
when RubyReactor::InterruptResult
|
|
135
261
|
@context.status = :paused
|
|
136
262
|
end
|
|
@@ -158,8 +284,15 @@ module RubyReactor
|
|
|
158
284
|
@result = @step_executor.execute_all_steps
|
|
159
285
|
else
|
|
160
286
|
case result
|
|
161
|
-
|
|
162
|
-
|
|
287
|
+
# Skipped must be listed before Success (Skipped < Success) so the
|
|
288
|
+
# halt path wins over the "continue with remaining steps" path.
|
|
289
|
+
when RubyReactor::Skipped,
|
|
290
|
+
RetryQueuedResult,
|
|
291
|
+
RubyReactor::Failure,
|
|
292
|
+
RubyReactor::AsyncResult,
|
|
293
|
+
RubyReactor::InterruptResult
|
|
294
|
+
# Terminal: step was skipped, requeued, failed, paused, or handed
|
|
295
|
+
# off to async. Return the result as-is.
|
|
163
296
|
@result = result
|
|
164
297
|
when RubyReactor::Success
|
|
165
298
|
# Step succeeded, continue with remaining steps
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
class Lock
|
|
5
|
+
class AcquisitionError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Minimum interval between auto-extend pings; protects very small TTLs.
|
|
8
|
+
MIN_EXTEND_INTERVAL = 1.0
|
|
9
|
+
|
|
10
|
+
attr_reader :key, :owner, :ttl, :wait, :auto_extend
|
|
11
|
+
|
|
12
|
+
def initialize(key, owner:, ttl: 60, wait: 0, auto_extend: true)
|
|
13
|
+
@key = "lock:#{key}"
|
|
14
|
+
@owner = owner
|
|
15
|
+
@ttl = ttl
|
|
16
|
+
@wait = wait
|
|
17
|
+
@auto_extend = auto_extend
|
|
18
|
+
@extender = nil
|
|
19
|
+
@extender_running = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def acquire
|
|
23
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
if adapter.lock_acquire(@key, @owner, @ttl)
|
|
27
|
+
start_extender if @auto_extend
|
|
28
|
+
return true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if @wait.zero? || (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) >= @wait
|
|
32
|
+
raise AcquisitionError, "Could not acquire lock '#{@key}' for owner '#{@owner}'"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sleep 0.1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def release
|
|
40
|
+
stop_extender
|
|
41
|
+
adapter.lock_release(@key, @owner)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def synchronize
|
|
45
|
+
acquire
|
|
46
|
+
yield
|
|
47
|
+
ensure
|
|
48
|
+
release
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def start_extender
|
|
54
|
+
return if @extender&.alive?
|
|
55
|
+
|
|
56
|
+
interval = [@ttl / 3.0, MIN_EXTEND_INTERVAL].max
|
|
57
|
+
@extender_running = true
|
|
58
|
+
|
|
59
|
+
@extender = Thread.new do
|
|
60
|
+
while @extender_running
|
|
61
|
+
sleep interval
|
|
62
|
+
break unless @extender_running
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
adapter.lock_extend(@key, @owner, @ttl)
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
RubyReactor.configuration.logger.warn(
|
|
68
|
+
"Lock auto-extend failed for '#{@key}': #{e.message}"
|
|
69
|
+
)
|
|
70
|
+
break
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def stop_extender
|
|
77
|
+
@extender_running = false
|
|
78
|
+
thread = @extender
|
|
79
|
+
@extender = nil
|
|
80
|
+
return unless thread
|
|
81
|
+
|
|
82
|
+
thread.wakeup if thread.alive?
|
|
83
|
+
thread.join(0.1)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
# Best-effort shutdown; never let extender teardown break release.
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def adapter
|
|
89
|
+
RubyReactor.configuration.storage_adapter
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -29,21 +29,9 @@ module RubyReactor
|
|
|
29
29
|
# Since map_offset tracks dispatching progress and might exceed count due to batching reservation,
|
|
30
30
|
# we must strictly check against the total count of elements.
|
|
31
31
|
# Check for fail_fast failure FIRST
|
|
32
|
-
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
reactor_class = resolve_reactor_class(metadata["reactor_class_info"])
|
|
36
|
-
|
|
37
|
-
failed_context_data = storage.retrieve_context(failed_context_id, reactor_class.name)
|
|
38
|
-
|
|
39
|
-
if failed_context_data
|
|
40
|
-
failed_context = RubyReactor::Context.deserialize_from_retry(failed_context_data)
|
|
41
|
-
|
|
42
|
-
# Resume parent execution (which marks step as failed)
|
|
43
|
-
resume_parent_execution(parent_context, step_name, RubyReactor::Failure(failed_context.failure_reason),
|
|
44
|
-
storage)
|
|
45
|
-
return
|
|
46
|
-
end
|
|
32
|
+
if (failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name))
|
|
33
|
+
handle_failure(failed_context_id, metadata, storage, parent_context, step_name)
|
|
34
|
+
return
|
|
47
35
|
end
|
|
48
36
|
|
|
49
37
|
return if results_count < total_count
|
|
@@ -96,6 +84,19 @@ module RubyReactor
|
|
|
96
84
|
RubyReactor::Success(results)
|
|
97
85
|
end
|
|
98
86
|
end
|
|
87
|
+
|
|
88
|
+
def self.handle_failure(failed_context_id, metadata, storage, parent_context, step_name)
|
|
89
|
+
# Resolve the class of the mapped reactor to retrieve its context
|
|
90
|
+
reactor_class = resolve_reactor_class(metadata["reactor_class_info"])
|
|
91
|
+
failed_context_data = storage.retrieve_context(failed_context_id, reactor_class.name)
|
|
92
|
+
|
|
93
|
+
return unless failed_context_data
|
|
94
|
+
|
|
95
|
+
failed_context = RubyReactor::Context.deserialize_from_retry(failed_context_data)
|
|
96
|
+
reason = failed_context.failure_reason
|
|
97
|
+
result = reason.is_a?(RubyReactor::Failure) ? reason : RubyReactor::Failure(reason)
|
|
98
|
+
resume_parent_execution(parent_context, step_name, result, storage)
|
|
99
|
+
end
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
end
|
|
@@ -5,151 +5,137 @@ module RubyReactor
|
|
|
5
5
|
class ElementExecutor
|
|
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
|
-
map_id = arguments[:map_id]
|
|
12
|
-
_element_id = arguments[:element_id]
|
|
13
|
-
index = arguments[:index]
|
|
14
|
-
serialized_inputs = arguments[:serialized_inputs]
|
|
15
|
-
reactor_class_info = arguments[:reactor_class_info]
|
|
16
|
-
strict_ordering = arguments[:strict_ordering]
|
|
17
|
-
parent_context_id = arguments[:parent_context_id]
|
|
18
|
-
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
19
|
-
step_name = arguments[:step_name]
|
|
20
|
-
batch_size = arguments[:batch_size]
|
|
21
|
-
# rubocop:enable Metrics/MethodLength
|
|
22
|
-
serialized_context = arguments[:serialized_context]
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
context = hydrate_or_create_context(arguments)
|
|
12
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
13
|
+
storage.store_map_element_context_id(arguments[:map_id], context.context_id,
|
|
14
|
+
arguments[:parent_reactor_class_name])
|
|
15
|
+
|
|
16
|
+
return if check_fail_fast?(arguments, storage)
|
|
17
|
+
|
|
18
|
+
executor = Executor.new(context.reactor_class, {}, context)
|
|
19
|
+
arguments[:serialized_context] ? executor.resume_execution : executor.execute
|
|
20
|
+
|
|
21
|
+
handle_result(executor.result, arguments, context, storage, executor)
|
|
22
|
+
finalize_execution(arguments, storage)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.load_parent_context(arguments, reactor_class_name, storage)
|
|
26
|
+
parent_context_data = storage.retrieve_context(arguments[:parent_context_id], reactor_class_name)
|
|
27
|
+
parent_reactor_class = Object.const_get(reactor_class_name)
|
|
28
|
+
parent_context = Context.new(
|
|
29
|
+
ContextSerializer.deserialize_value(parent_context_data["inputs"]),
|
|
30
|
+
parent_reactor_class
|
|
31
|
+
)
|
|
32
|
+
parent_context.context_id = arguments[:parent_context_id]
|
|
33
|
+
parent_context
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Legacy helpers resolved_next_element, build_serialized_inputs, queue_element_job
|
|
37
|
+
# are REMOVED as they are no longer used for self-queuing.
|
|
38
|
+
|
|
39
|
+
# Basic helper to build inputs for the CURRENT element (still needed for perform)
|
|
40
|
+
# Wait, perform uses `serialized_inputs` passed to it.
|
|
41
|
+
# We don't need `build_element_inputs` here?
|
|
42
|
+
# `perform` uses `params[:serialized_inputs]`.
|
|
43
|
+
# So we can remove input building helpers too?
|
|
44
|
+
# Let's check if they are used elsewhere.
|
|
45
|
+
# `resolve_reactor_class` is used in `perform`.
|
|
46
|
+
# `build_element_inputs` is likely in Helpers or mixed in?
|
|
47
|
+
|
|
48
|
+
# rubocop:disable Style/IdenticalConditionalBranches
|
|
49
|
+
def self.hydrate_or_create_context(arguments)
|
|
50
|
+
if arguments[:serialized_context]
|
|
51
|
+
context = ContextSerializer.deserialize(arguments[:serialized_context])
|
|
26
52
|
context.map_metadata = arguments
|
|
27
|
-
reactor_class = context.reactor_class
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
context.inputs = ContextSerializer.deserialize_value(serialized_inputs)
|
|
54
|
+
if context.inputs.empty? && arguments[:serialized_inputs]
|
|
55
|
+
context.inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
|
|
32
56
|
end
|
|
57
|
+
context
|
|
33
58
|
else
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Resolve reactor class
|
|
38
|
-
reactor_class = resolve_reactor_class(reactor_class_info)
|
|
59
|
+
inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
|
|
60
|
+
reactor_class = resolve_reactor_class(arguments[:reactor_class_info])
|
|
39
61
|
|
|
40
|
-
# Create context
|
|
41
62
|
context = Context.new(inputs, reactor_class)
|
|
42
|
-
context.parent_context_id = parent_context_id
|
|
63
|
+
context.parent_context_id = arguments[:parent_context_id]
|
|
43
64
|
context.map_metadata = arguments
|
|
65
|
+
context
|
|
44
66
|
end
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Style/IdenticalConditionalBranches
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Fail Fast Check
|
|
50
|
-
if arguments[:fail_fast]
|
|
51
|
-
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
52
|
-
if failed_context_id
|
|
53
|
-
# Decrement counter as we are skipping execution
|
|
54
|
-
new_count = storage.decrement_map_counter(map_id, parent_reactor_class_name)
|
|
55
|
-
return unless new_count.zero?
|
|
70
|
+
def self.check_fail_fast?(arguments, storage)
|
|
71
|
+
return false unless arguments[:fail_fast]
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
parent_context_id: parent_context_id,
|
|
60
|
-
map_id: map_id,
|
|
61
|
-
parent_reactor_class_name: parent_reactor_class_name,
|
|
62
|
-
step_name: step_name,
|
|
63
|
-
strict_ordering: strict_ordering,
|
|
64
|
-
timeout: 3600
|
|
65
|
-
)
|
|
66
|
-
return
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Execute
|
|
71
|
-
executor = Executor.new(reactor_class, {}, context)
|
|
73
|
+
map_id = arguments[:map_id]
|
|
74
|
+
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
else
|
|
76
|
-
executor.execute
|
|
77
|
-
end
|
|
76
|
+
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
77
|
+
return false unless failed_context_id
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
# Skip execution
|
|
80
|
+
finalize_execution(arguments, storage)
|
|
81
|
+
true
|
|
82
|
+
end
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
end
|
|
84
|
+
def self.handle_result(result, arguments, context, storage, executor)
|
|
85
|
+
return if result.is_a?(RetryQueuedResult)
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
map_id = arguments[:map_id]
|
|
88
|
+
index = arguments[:index]
|
|
89
|
+
parent_class = arguments[:parent_reactor_class_name] # Using short name for variable
|
|
87
90
|
|
|
88
91
|
if result.success?
|
|
89
|
-
storage.store_map_result(map_id, index,
|
|
90
|
-
|
|
91
|
-
parent_reactor_class_name,
|
|
92
|
-
strict_ordering: strict_ordering)
|
|
92
|
+
storage.store_map_result(map_id, index, ContextSerializer.serialize_value(result.value),
|
|
93
|
+
parent_class, strict_ordering: arguments[:strict_ordering])
|
|
93
94
|
else
|
|
94
|
-
# Trigger Compensation Logic
|
|
95
95
|
executor.undo_all
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
storage.store_map_result(map_id, index, { _error: result.error }, parent_reactor_class_name,
|
|
99
|
-
strict_ordering: strict_ordering)
|
|
96
|
+
storage.store_map_result(map_id, index, { _error: result.error }, parent_class,
|
|
97
|
+
strict_ordering: arguments[:strict_ordering])
|
|
100
98
|
|
|
101
99
|
if arguments[:fail_fast]
|
|
102
|
-
storage.store_map_failed_context_id(map_id, context.context_id,
|
|
100
|
+
storage.store_map_failed_context_id(map_id, context.context_id, parent_class)
|
|
101
|
+
# FAST FAIL: Trigger Collector immediately to cancel/fail the map execution
|
|
102
|
+
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
103
|
+
parent_context_id: arguments[:parent_context_id],
|
|
104
|
+
map_id: map_id,
|
|
105
|
+
parent_reactor_class_name: parent_class,
|
|
106
|
+
step_name: arguments[:step_name],
|
|
107
|
+
strict_ordering: arguments[:strict_ordering],
|
|
108
|
+
timeout: 3600
|
|
109
|
+
)
|
|
103
110
|
end
|
|
104
111
|
end
|
|
112
|
+
end
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
def self.finalize_execution(arguments, storage)
|
|
115
|
+
map_id = arguments[:map_id]
|
|
116
|
+
parent_class = arguments[:parent_reactor_class_name]
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
trigger_next_batch_if_needed(arguments, index, batch_size)
|
|
118
|
+
new_count = storage.decrement_map_counter(map_id, parent_class)
|
|
119
|
+
trigger_next_batch_if_needed(arguments, arguments[:index], arguments[:batch_size])
|
|
111
120
|
|
|
112
121
|
return unless new_count.zero?
|
|
113
122
|
|
|
114
|
-
# Trigger collection
|
|
115
123
|
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
116
|
-
parent_context_id: parent_context_id,
|
|
124
|
+
parent_context_id: arguments[:parent_context_id],
|
|
117
125
|
map_id: map_id,
|
|
118
|
-
parent_reactor_class_name:
|
|
119
|
-
step_name: step_name,
|
|
120
|
-
strict_ordering: strict_ordering,
|
|
126
|
+
parent_reactor_class_name: parent_class,
|
|
127
|
+
step_name: arguments[:step_name],
|
|
128
|
+
strict_ordering: arguments[:strict_ordering],
|
|
121
129
|
timeout: 3600
|
|
122
130
|
)
|
|
123
131
|
end
|
|
124
132
|
|
|
125
|
-
def self.load_parent_context(arguments, reactor_class_name, storage)
|
|
126
|
-
parent_context_data = storage.retrieve_context(arguments[:parent_context_id], reactor_class_name)
|
|
127
|
-
parent_reactor_class = Object.const_get(reactor_class_name)
|
|
128
|
-
parent_context = Context.new(
|
|
129
|
-
ContextSerializer.deserialize_value(parent_context_data["inputs"]),
|
|
130
|
-
parent_reactor_class
|
|
131
|
-
)
|
|
132
|
-
parent_context.context_id = arguments[:parent_context_id]
|
|
133
|
-
parent_context
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Legacy helpers resolved_next_element, build_serialized_inputs, queue_element_job
|
|
137
|
-
# are REMOVED as they are no longer used for self-queuing.
|
|
138
|
-
|
|
139
|
-
# Basic helper to build inputs for the CURRENT element (still needed for perform)
|
|
140
|
-
# Wait, perform uses `serialized_inputs` passed to it.
|
|
141
|
-
# We don't need `build_element_inputs` here?
|
|
142
|
-
# `perform` uses `params[:serialized_inputs]`.
|
|
143
|
-
# So we can remove input building helpers too?
|
|
144
|
-
# Let's check if they are used elsewhere.
|
|
145
|
-
# `resolve_reactor_class` is used in `perform`.
|
|
146
|
-
# `build_element_inputs` is likely in Helpers or mixed in?
|
|
147
|
-
|
|
148
133
|
def self.trigger_next_batch_if_needed(arguments, index, batch_size)
|
|
149
134
|
return unless batch_size && ((index + 1) % batch_size).zero?
|
|
150
135
|
|
|
151
136
|
# Trigger Dispatcher for next batch
|
|
152
137
|
next_batch_args = arguments.dup
|
|
138
|
+
# Ensure we don't carry over temporary execution flags if any
|
|
153
139
|
next_batch_args[:continuation] = true
|
|
154
140
|
RubyReactor::Map::Dispatcher.perform(next_batch_args)
|
|
155
141
|
end
|
|
@@ -31,6 +31,7 @@ module RubyReactor
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.execute_all_elements(source:, mappings:, reactor_class:, parent_context:, storage_options:)
|
|
34
|
+
# rubocop:disable Metrics/BlockLength
|
|
34
35
|
source.map.with_index do |element, index|
|
|
35
36
|
if storage_options[:fail_fast]
|
|
36
37
|
failed_context_id = storage_options[:storage].retrieve_map_failed_context_id(
|
|
@@ -71,12 +72,12 @@ module RubyReactor
|
|
|
71
72
|
|
|
72
73
|
result
|
|
73
74
|
end.compact
|
|
75
|
+
# rubocop:enable Metrics/BlockLength
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
def self.link_contexts(child_context, parent_context)
|
|
77
79
|
child_context.parent_context = parent_context
|
|
78
80
|
child_context.root_context = parent_context.root_context || parent_context
|
|
79
|
-
child_context.test_mode = parent_context.test_mode
|
|
80
81
|
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
81
82
|
end
|
|
82
83
|
|
|
@@ -63,7 +63,8 @@ module RubyReactor
|
|
|
63
63
|
final_result.error,
|
|
64
64
|
step: step_name_sym,
|
|
65
65
|
context: parent_context,
|
|
66
|
-
original_error: final_result.error.is_a?(Exception) ? final_result.error : nil
|
|
66
|
+
original_error: final_result.error.is_a?(Exception) ? final_result.error : nil,
|
|
67
|
+
exception_class: final_result.respond_to?(:exception_class) ? final_result.exception_class : nil
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
# Pass backtrace if available
|