ruby_reactor 0.8.4 → 0.8.5
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/.specify/feature.json +1 -1
- data/CHANGELOG.md +196 -0
- data/CLAUDE.md +1 -1
- data/README.md +47 -11
- data/lib/ruby_reactor/dsl/async_macros.rb +30 -1
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/compose_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +1 -3
- data/lib/ruby_reactor/dsl/map_builder.rb +0 -2
- data/lib/ruby_reactor/dsl/step_builder.rb +91 -19
- data/lib/ruby_reactor/error/argument_resolution_error.rb +19 -0
- data/lib/ruby_reactor/error/rescuable.rb +28 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +30 -26
- data/lib/ruby_reactor/executor/result_handler.rb +18 -16
- data/lib/ruby_reactor/executor/step_coordination.rb +11 -8
- data/lib/ruby_reactor/executor/step_executor.rb +59 -49
- data/lib/ruby_reactor/executor.rb +38 -4
- data/lib/ruby_reactor/map/collector.rb +21 -11
- data/lib/ruby_reactor/map/dispatcher.rb +29 -3
- data/lib/ruby_reactor/map/element_executor.rb +9 -3
- data/lib/ruby_reactor/map/helpers.rb +32 -2
- data/lib/ruby_reactor/map/result_enumerator.rb +18 -12
- data/lib/ruby_reactor/reactor.rb +24 -0
- data/lib/ruby_reactor/rspec/matchers.rb +19 -3
- data/lib/ruby_reactor/step/compose_step.rb +7 -1
- data/lib/ruby_reactor/step/map_step.rb +109 -4
- data/lib/ruby_reactor/step.rb +7 -0
- data/lib/ruby_reactor/step_worker.rb +46 -22
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +9 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +1 -1
- data/lib/ruby_reactor/web/public/assets/{index-CeZU-ESu.js → index-CQbgHtd0.js} +10 -10
- data/lib/ruby_reactor/web/public/index.html +1 -1
- data/lib/ruby_reactor/worker.rb +3 -1
- data/lib/ruby_reactor.rb +17 -6
- data/specs/007-execution-flow-analysis/analysis/README.md +147 -0
- data/specs/007-execution-flow-analysis/analysis/execution-order.md +359 -0
- data/specs/007-execution-flow-analysis/analysis/findings-and-options.md +502 -0
- data/specs/007-execution-flow-analysis/analysis/invariants.md +109 -0
- data/specs/007-execution-flow-analysis/checklists/requirements.md +39 -0
- data/specs/007-execution-flow-analysis/contracts/report-structure.md +71 -0
- data/specs/007-execution-flow-analysis/data-model.md +83 -0
- data/specs/007-execution-flow-analysis/evidence/harness.rb +229 -0
- data/specs/007-execution-flow-analysis/evidence/output.txt +333 -0
- data/specs/007-execution-flow-analysis/evidence/probes/01_plain.rb +122 -0
- data/specs/007-execution-flow-analysis/evidence/probes/02_compose.rb +182 -0
- data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +232 -0
- data/specs/007-execution-flow-analysis/evidence/probes/04_async.rb +132 -0
- data/specs/007-execution-flow-analysis/evidence/probes/05_background.rb +58 -0
- data/specs/007-execution-flow-analysis/evidence/probes/06_coordination.rb +158 -0
- data/specs/007-execution-flow-analysis/evidence/probes/07_interrupts_manual.rb +185 -0
- data/specs/007-execution-flow-analysis/evidence/run.rb +15 -0
- data/specs/007-execution-flow-analysis/plan.md +127 -0
- data/specs/007-execution-flow-analysis/quickstart.md +51 -0
- data/specs/007-execution-flow-analysis/research.md +202 -0
- data/specs/007-execution-flow-analysis/spec.md +270 -0
- data/specs/007-execution-flow-analysis/tasks.md +257 -0
- data/specs/008-rollback-reliability/checklists/requirements.md +43 -0
- data/specs/008-rollback-reliability/contracts/api-surface.md +126 -0
- data/specs/008-rollback-reliability/contracts/rollback-semantics.md +76 -0
- data/specs/008-rollback-reliability/data-model.md +139 -0
- data/specs/008-rollback-reliability/plan.md +233 -0
- data/specs/008-rollback-reliability/quickstart.md +105 -0
- data/specs/008-rollback-reliability/research.md +653 -0
- data/specs/008-rollback-reliability/spec.md +561 -0
- data/specs/008-rollback-reliability/tasks.md +1110 -0
- data/specs/future_improvements.md +48 -0
- metadata +35 -2
|
@@ -13,7 +13,7 @@ module RubyReactor
|
|
|
13
13
|
period_config: "with_period", ordered_lock_config: "with_ordered_lock"
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
|
-
attr_accessor :name, :impl, :arguments, :run_block, :compensate_block, :undo_block,
|
|
16
|
+
attr_accessor :name, :impl, :arguments, :run_block, :compensate_block, :undo_block,
|
|
17
17
|
:dependencies, :args_validator, :output_validator
|
|
18
18
|
|
|
19
19
|
def initialize(name, impl = nil, reactor = nil)
|
|
@@ -24,8 +24,6 @@ module RubyReactor
|
|
|
24
24
|
@run_block = nil
|
|
25
25
|
@compensate_block = nil
|
|
26
26
|
@undo_block = nil
|
|
27
|
-
@conditions = []
|
|
28
|
-
@guards = []
|
|
29
27
|
@dependencies = []
|
|
30
28
|
@arg_validations = []
|
|
31
29
|
@validate_args_input = nil
|
|
@@ -78,12 +76,15 @@ module RubyReactor
|
|
|
78
76
|
@undo_block = block
|
|
79
77
|
end
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
# `where`/`guard` were a second, pre-body skip mechanism with failure
|
|
80
|
+
# rules of their own (008 R-15). A step decides to skip itself from its
|
|
81
|
+
# body instead, so both stay only to name the replacement.
|
|
82
|
+
def where(*)
|
|
83
|
+
raise_removed_condition!(:where)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
def guard(
|
|
86
|
-
|
|
86
|
+
def guard(*)
|
|
87
|
+
raise_removed_condition!(:guard)
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def wait_for(*step_names)
|
|
@@ -148,8 +149,6 @@ module RubyReactor
|
|
|
148
149
|
run_block: @run_block,
|
|
149
150
|
compensate_block: @compensate_block,
|
|
150
151
|
undo_block: @undo_block,
|
|
151
|
-
conditions: @conditions,
|
|
152
|
-
guards: @guards,
|
|
153
152
|
dependencies: @dependencies,
|
|
154
153
|
args_validator: @args_validator || build_args_validator(@arg_validations, @validate_args_input),
|
|
155
154
|
output_validator: @output_validator,
|
|
@@ -167,6 +166,15 @@ module RubyReactor
|
|
|
167
166
|
|
|
168
167
|
private
|
|
169
168
|
|
|
169
|
+
def raise_removed_condition!(keyword)
|
|
170
|
+
raise RubyReactor::Error::DeprecatedDslError.new(
|
|
171
|
+
"`#{keyword}` inside a step block has been removed (`where`/`guard` are gone). To skip " \
|
|
172
|
+
":#{@name}, return `Skipped(value)` (or call `skip!(value)`) from its `run` body; the " \
|
|
173
|
+
"reactor continues with that value, exactly as for a Success.",
|
|
174
|
+
step: @name
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
170
178
|
# The same primitive declared BOTH inline and on the step class is
|
|
171
179
|
# ambiguous — two keys for one slot of the fixed acquisition order, and
|
|
172
180
|
# `StepConfig`'s readers would silently let the inline one win. Refuse it
|
|
@@ -237,11 +245,15 @@ module RubyReactor
|
|
|
237
245
|
end
|
|
238
246
|
|
|
239
247
|
def warn_deprecation(site, message)
|
|
248
|
+
warn_definition(site, "DEPRECATION:", "#{message} Removal no earlier than the next MAJOR.")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# A definition-time warning, printed once per declaration site.
|
|
252
|
+
def warn_definition(site, prefix, message)
|
|
240
253
|
location = "#{site.path}:#{site.lineno}"
|
|
241
254
|
return unless StepBuilder.deprecation_sites.add?(location)
|
|
242
255
|
|
|
243
|
-
warn "[RubyReactor]
|
|
244
|
-
"Removal no earlier than the next MAJOR."
|
|
256
|
+
warn ["[RubyReactor]", prefix, location, reactor_label, message].compact.join(" ")
|
|
245
257
|
end
|
|
246
258
|
|
|
247
259
|
def owned_contract
|
|
@@ -256,7 +268,7 @@ module RubyReactor
|
|
|
256
268
|
class StepConfig
|
|
257
269
|
NO_RETRIES = { max_attempts: 1, backoff: :exponential, base_delay: 1 }.freeze
|
|
258
270
|
|
|
259
|
-
attr_reader :name, :impl, :arguments, :run_block, :compensate_block, :undo_block,
|
|
271
|
+
attr_reader :name, :impl, :arguments, :run_block, :compensate_block, :undo_block,
|
|
260
272
|
:dependencies, :args_validator, :output_validator, :async_dispatch,
|
|
261
273
|
:inline_contract
|
|
262
274
|
|
|
@@ -268,8 +280,6 @@ module RubyReactor
|
|
|
268
280
|
@run_block = config[:run_block]
|
|
269
281
|
@compensate_block = config[:compensate_block]
|
|
270
282
|
@undo_block = config[:undo_block]
|
|
271
|
-
@conditions = config[:conditions] || []
|
|
272
|
-
@guards = config[:guards] || []
|
|
273
283
|
@dependencies = config[:dependencies] || []
|
|
274
284
|
@args_validator = config[:args_validator]
|
|
275
285
|
@output_validator = config[:output_validator]
|
|
@@ -358,6 +368,51 @@ module RubyReactor
|
|
|
358
368
|
input_contract ? input_contract.enforce!(args) : args
|
|
359
369
|
end
|
|
360
370
|
|
|
371
|
+
# The step's `argument` wiring resolved against `context`: each source,
|
|
372
|
+
# then its `transform`. The one copy every process uses (StepExecutor,
|
|
373
|
+
# StepWorker). Any raise becomes an `ArgumentResolutionError`, a
|
|
374
|
+
# never-started failure attributed to this step (008 R-06) — except a
|
|
375
|
+
# park signal (a worker waiting on an async result), which is not a failure.
|
|
376
|
+
def resolve_arguments(context)
|
|
377
|
+
arguments.to_h do |arg_name, arg_config|
|
|
378
|
+
value = arg_config[:source].resolve(context)
|
|
379
|
+
value = arg_config[:transform].call(value) if arg_config[:transform]
|
|
380
|
+
[arg_name, value]
|
|
381
|
+
end
|
|
382
|
+
rescue Error::ExecutionParked
|
|
383
|
+
raise
|
|
384
|
+
rescue Error::Rescuable => e
|
|
385
|
+
raise never_started(Error::ArgumentResolutionError, "could not resolve its arguments", e)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Rollback dispatch, the same wherever the step rolls back
|
|
389
|
+
# (CompensationManager, StepWorker): the inline block, else the class
|
|
390
|
+
# step, else Skipped — nothing defined, rollback continues. Coordination,
|
|
391
|
+
# trace and middleware stay with the caller.
|
|
392
|
+
def call_compensate(error, arguments, context)
|
|
393
|
+
catch(StepSignals::TAG) do
|
|
394
|
+
if compensate_block
|
|
395
|
+
compensate_block.call(error, wrap_inputs(arguments), context)
|
|
396
|
+
elsif has_impl?
|
|
397
|
+
impl.compensate(error, arguments, context)
|
|
398
|
+
else
|
|
399
|
+
RubyReactor.Skipped()
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def call_undo(result_value, arguments, context)
|
|
405
|
+
catch(StepSignals::TAG) do
|
|
406
|
+
if undo_block
|
|
407
|
+
undo_block.call(result_value, wrap_inputs(arguments), context)
|
|
408
|
+
elsif has_impl?
|
|
409
|
+
impl.undo(result_value, arguments, context)
|
|
410
|
+
else
|
|
411
|
+
RubyReactor.Skipped()
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
361
416
|
# The step's work as a reactor runs it. Coordination is NOT taken here:
|
|
362
417
|
# the caller (StepExecutor / StepWorker) takes this config's effective
|
|
363
418
|
# declarations — inline and class alike — in one fixed order around it.
|
|
@@ -381,6 +436,20 @@ module RubyReactor
|
|
|
381
436
|
!@async_dispatch.nil?
|
|
382
437
|
end
|
|
383
438
|
|
|
439
|
+
# Whether a success of this step is recorded for undo. `async_step` /
|
|
440
|
+
# `async_reactor` dispatches are independent units of work with their
|
|
441
|
+
# own compensation flows: the parent rolling back must not "undo" a
|
|
442
|
+
# dispatch whose unit runs (and may still succeed) elsewhere (008 R-10).
|
|
443
|
+
def rollback_tracked?
|
|
444
|
+
!async_dispatch?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Whether a run of this step cut short by an interruption is still
|
|
448
|
+
# tracked for undo: the construct's own definition decides (008 R-16).
|
|
449
|
+
def undoes_partial_run?
|
|
450
|
+
rollback_tracked? && has_impl? && impl.respond_to?(:undoes_partial_run?) && impl.undoes_partial_run?
|
|
451
|
+
end
|
|
452
|
+
|
|
384
453
|
def has_impl?
|
|
385
454
|
!@impl.nil?
|
|
386
455
|
end
|
|
@@ -403,14 +472,17 @@ module RubyReactor
|
|
|
403
472
|
(retry_config[:max_attempts] || 0) > 1
|
|
404
473
|
end
|
|
405
474
|
|
|
406
|
-
def should_run?(context)
|
|
407
|
-
@conditions.all? { |condition| condition.call(context) } &&
|
|
408
|
-
@guards.all? { |guard| guard.call(context) }
|
|
409
|
-
end
|
|
410
|
-
|
|
411
475
|
def interrupt?
|
|
412
476
|
false
|
|
413
477
|
end
|
|
478
|
+
|
|
479
|
+
private
|
|
480
|
+
|
|
481
|
+
def never_started(error_class, what, cause)
|
|
482
|
+
error = error_class.new("Step '#{name}' #{what}: #{cause.message}", step: name, original_error: cause)
|
|
483
|
+
error.set_backtrace(cause.backtrace)
|
|
484
|
+
error
|
|
485
|
+
end
|
|
414
486
|
end
|
|
415
487
|
end
|
|
416
488
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Error
|
|
5
|
+
# A step's `argument` source, `transform` or result path raised. The step's
|
|
6
|
+
# body never started, so it is never compensated; completed steps are
|
|
7
|
+
# undone (008 R-06). The same inputs fail the same way, so never retried.
|
|
8
|
+
class ArgumentResolutionError < Base
|
|
9
|
+
attr_reader :exception_class
|
|
10
|
+
|
|
11
|
+
def initialize(message, step:, original_error:, context: nil)
|
|
12
|
+
super(message, step: step, context: context, original_error: original_error)
|
|
13
|
+
@exception_class = original_error.class.name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def retryable? = false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Error
|
|
5
|
+
# `rescue Error::Rescuable` catches every exception reactor code can raise —
|
|
6
|
+
# standard or not (`NotImplementedError`, `SystemStackError`, a custom
|
|
7
|
+
# `Exception` subclass) — so it fails its step and rolls back (008 R-16).
|
|
8
|
+
#
|
|
9
|
+
# It lets through only interruptions: exceptions raised INTO running code
|
|
10
|
+
# from outside it. A signal (incl. `Interrupt`, `Sidekiq::Shutdown`), an
|
|
11
|
+
# exit, running out of memory, and the one an enclosing `Timeout.timeout`
|
|
12
|
+
# raises — rescuing that would stop the caller's timeout from ever firing.
|
|
13
|
+
# Running rollback code during any of them is unsafe; the executor marks
|
|
14
|
+
# the run `aborted` for a manual undo instead.
|
|
15
|
+
module Rescuable
|
|
16
|
+
INTERRUPTIONS = [SignalException, SystemExit, NoMemoryError].freeze
|
|
17
|
+
|
|
18
|
+
def self.===(exception)
|
|
19
|
+
exception.is_a?(Exception) && !interruption?(exception)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.interruption?(exception)
|
|
23
|
+
INTERRUPTIONS.any? { |klass| exception.is_a?(klass) } ||
|
|
24
|
+
(defined?(::Timeout::ExitException) && exception.is_a?(::Timeout::ExitException))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
module RubyReactor
|
|
4
4
|
class Executor
|
|
5
5
|
class CompensationManager
|
|
6
|
-
# Raised ONLY
|
|
7
|
-
#
|
|
6
|
+
# Raised ONLY before the step's body: by its own coordination, or while
|
|
7
|
+
# resolving its arguments (008 R-06).
|
|
8
|
+
# A coordination error raised from inside a body (a nested direct
|
|
8
9
|
# `Step.run`) arrives as `StepCoordination::NestedCoordinationError`, and
|
|
9
10
|
# a bare `Lock::AcquisitionError` etc. may come from a nested
|
|
10
11
|
# `Reactor.run` — both mean the body ran, so neither is listed here.
|
|
11
12
|
NEVER_STARTED_ERROR_CLASSES = [
|
|
12
13
|
RubyReactor::Executor::StepCoordination::Contended,
|
|
13
14
|
RubyReactor::Executor::StepCoordination::KeyError,
|
|
14
|
-
RubyReactor::Executor::StepCoordination::DispatchRefused
|
|
15
|
+
RubyReactor::Executor::StepCoordination::DispatchRefused,
|
|
16
|
+
RubyReactor::Error::ArgumentResolutionError
|
|
15
17
|
].freeze
|
|
16
18
|
|
|
17
19
|
def initialize(context)
|
|
@@ -65,15 +67,26 @@ module RubyReactor
|
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
# A unit that compensates itself outside the executor loop (StepWorker's
|
|
71
|
+
# `async_step`): the same coordination re-take, trace, middleware events
|
|
72
|
+
# and `rollback_failures` as a step compensated here.
|
|
73
|
+
def compensate(step_config, error, arguments)
|
|
74
|
+
compensate_step(step_config, error, arguments)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Newest first. Each entry leaves the stack only once its undo returned,
|
|
78
|
+
# so an interruption mid-rollback leaves exactly the entries still to
|
|
79
|
+
# undo — the interrupted one included — for a manual undo (008 R-16).
|
|
68
80
|
def rollback_completed_steps
|
|
69
|
-
undo_stack.
|
|
81
|
+
until undo_stack.empty?
|
|
82
|
+
step_info = undo_stack.last
|
|
70
83
|
result = @context.with_step(step_info[:step].name) do
|
|
71
84
|
undo_step(step_info[:step], step_info[:result], step_info[:arguments])
|
|
72
85
|
end
|
|
86
|
+
undo_stack.pop
|
|
73
87
|
@undo_trace << { type: :undo, step: step_info[:step].name, result: result,
|
|
74
88
|
arguments: step_info[:arguments] }
|
|
75
89
|
end
|
|
76
|
-
undo_stack.clear
|
|
77
90
|
end
|
|
78
91
|
|
|
79
92
|
private
|
|
@@ -117,19 +130,18 @@ module RubyReactor
|
|
|
117
130
|
).around_rollback(&block)
|
|
118
131
|
end
|
|
119
132
|
|
|
133
|
+
# Under `with_step`, as `rollback_completed_steps` runs each undo: a
|
|
134
|
+
# construct (compose, map) reads `context.current_step` to find its own
|
|
135
|
+
# state during either rollback moment.
|
|
120
136
|
def compensate_step(step_config, error, arguments)
|
|
137
|
+
@context.with_step(step_config.name) { compensate_step_body(step_config, error, arguments) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def compensate_step_body(step_config, error, arguments)
|
|
121
141
|
middlewares.on(:start_compensation, step_config.name, error, arguments, @context)
|
|
122
142
|
begin
|
|
123
143
|
compensate_result = coordinated_rollback(step_config, arguments) do
|
|
124
|
-
|
|
125
|
-
if step_config.compensate_block
|
|
126
|
-
step_config.compensate_block.call(error, step_config.wrap_inputs(arguments), @context)
|
|
127
|
-
elsif step_config.has_impl?
|
|
128
|
-
step_config.impl.compensate(error, arguments, @context)
|
|
129
|
-
else
|
|
130
|
-
RubyReactor.Skipped() # Default: nothing defined, rollback continues
|
|
131
|
-
end
|
|
132
|
-
end
|
|
144
|
+
step_config.call_compensate(error, arguments, @context)
|
|
133
145
|
end
|
|
134
146
|
|
|
135
147
|
@context.append_execution_trace(
|
|
@@ -152,7 +164,7 @@ module RubyReactor
|
|
|
152
164
|
end
|
|
153
165
|
|
|
154
166
|
compensate_result
|
|
155
|
-
rescue
|
|
167
|
+
rescue Error::Rescuable => e
|
|
156
168
|
record_rollback_failure(step_config.name, :compensate, e)
|
|
157
169
|
middlewares.on(:failed_compensation, step_config.name, e, @context)
|
|
158
170
|
# A raise is a compensation failure like a returned Failure: the
|
|
@@ -162,19 +174,11 @@ module RubyReactor
|
|
|
162
174
|
end
|
|
163
175
|
end
|
|
164
176
|
|
|
165
|
-
def undo_step(step_config, result, arguments)
|
|
177
|
+
def undo_step(step_config, result, arguments)
|
|
166
178
|
middlewares.on(:start_undo, step_config.name, result, arguments, @context)
|
|
167
179
|
begin
|
|
168
180
|
undo_result = coordinated_rollback(step_config, arguments) do
|
|
169
|
-
|
|
170
|
-
if step_config.undo_block
|
|
171
|
-
step_config.undo_block.call(result.value, step_config.wrap_inputs(arguments), @context)
|
|
172
|
-
elsif step_config.has_impl?
|
|
173
|
-
step_config.impl.undo(result.value, arguments, @context)
|
|
174
|
-
else
|
|
175
|
-
RubyReactor.Skipped() # Default: nothing defined, rollback continues
|
|
176
|
-
end
|
|
177
|
-
end
|
|
181
|
+
step_config.call_undo(result.value, arguments, @context)
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
@context.append_execution_trace(
|
|
@@ -196,7 +200,7 @@ module RubyReactor
|
|
|
196
200
|
end
|
|
197
201
|
|
|
198
202
|
undo_result
|
|
199
|
-
rescue
|
|
203
|
+
rescue Error::Rescuable => e
|
|
200
204
|
record_rollback_failure(step_config.name, :undo, e)
|
|
201
205
|
middlewares.on(:failed_undo, step_config.name, e, @context)
|
|
202
206
|
# Log undo failure but don't halt the rollback process
|
|
@@ -62,12 +62,20 @@ module RubyReactor
|
|
|
62
62
|
@compensation_manager.rollback_completed_steps
|
|
63
63
|
build_validation_failure(error)
|
|
64
64
|
when Error::Base
|
|
65
|
-
# Other errors need rollback
|
|
65
|
+
# Other errors need rollback. A `CompensationError` names the step
|
|
66
|
+
# whose compensation failed.
|
|
66
67
|
@compensation_manager.rollback_completed_steps
|
|
67
|
-
RubyReactor.Failure("Execution error: #{error.message}", exception_class: error.class.name
|
|
68
|
+
RubyReactor.Failure("Execution error: #{error.message}", exception_class: error.class.name,
|
|
69
|
+
step_name: error.step || @context.current_step,
|
|
70
|
+
reactor_name: @context.reactor_class&.name)
|
|
68
71
|
else
|
|
69
|
-
#
|
|
70
|
-
|
|
72
|
+
# Any other StandardError after completed work (a checkpoint write,
|
|
73
|
+
# a hook) still leaves a partial saga: roll it back like any failure
|
|
74
|
+
# (Constitution II, 008 R-07) and name the step that was executing.
|
|
75
|
+
@compensation_manager.rollback_completed_steps
|
|
76
|
+
RubyReactor.Failure("Execution failed: #{error.message}", exception_class: error.class.name,
|
|
77
|
+
step_name: @context.current_step,
|
|
78
|
+
reactor_name: @context.reactor_class&.name)
|
|
71
79
|
end
|
|
72
80
|
end
|
|
73
81
|
|
|
@@ -108,9 +116,9 @@ module RubyReactor
|
|
|
108
116
|
result
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
# A step returned `RubyReactor.Skipped(...)
|
|
112
|
-
#
|
|
113
|
-
#
|
|
119
|
+
# A step returned `RubyReactor.Skipped(...)`: the run continues exactly as
|
|
120
|
+
# for a Success (value, hand-off, period mark), but the step is NOT pushed
|
|
121
|
+
# for undo — it had nothing to do, so there is nothing to revert (008 R-19).
|
|
114
122
|
def handle_skipped(step_config, result, resolved_arguments)
|
|
115
123
|
validate_step_output(step_config, result.value, resolved_arguments)
|
|
116
124
|
@step_results[step_config.name] = result
|
|
@@ -130,11 +138,7 @@ module RubyReactor
|
|
|
130
138
|
def handle_success(step_config, result, resolved_arguments)
|
|
131
139
|
validate_step_output(step_config, result.value, resolved_arguments)
|
|
132
140
|
@step_results[step_config.name] = result
|
|
133
|
-
|
|
134
|
-
# work with their own compensation flows — the parent rolling back must
|
|
135
|
-
# not "undo" a dispatch whose unit runs (and may still succeed)
|
|
136
|
-
# elsewhere. They never enter the parent's undo stack.
|
|
137
|
-
unless async_unit?(step_config)
|
|
141
|
+
if step_config.rollback_tracked?
|
|
138
142
|
@compensation_manager.add_to_undo_stack({ step: step_config, arguments: resolved_arguments,
|
|
139
143
|
result: result })
|
|
140
144
|
end
|
|
@@ -142,10 +146,6 @@ module RubyReactor
|
|
|
142
146
|
@dependency_graph.complete_step(step_config.name)
|
|
143
147
|
end
|
|
144
148
|
|
|
145
|
-
def async_unit?(step_config)
|
|
146
|
-
step_config.respond_to?(:async_dispatch?) && step_config.async_dispatch?
|
|
147
|
-
end
|
|
148
|
-
|
|
149
149
|
# A composed child's Failure carries the child's own rollback failures;
|
|
150
150
|
# fold them in BEFORE this level rolls back, so they come first.
|
|
151
151
|
def adopt_rollback_failures(result)
|
|
@@ -254,6 +254,8 @@ module RubyReactor
|
|
|
254
254
|
def resolve_exception_class(original_error, error)
|
|
255
255
|
# A step's own contention is reported by its cause (Lock::AcquisitionError, ...).
|
|
256
256
|
original_error = original_error.original if original_error.is_a?(StepCoordination::Contended)
|
|
257
|
+
# Argument failures report their cause's class (008 R-06).
|
|
258
|
+
return original_error.exception_class if original_error.is_a?(Error::ArgumentResolutionError)
|
|
257
259
|
return original_error.class.name if original_error
|
|
258
260
|
|
|
259
261
|
error.respond_to?(:exception_class) ? error.exception_class : nil
|
|
@@ -99,7 +99,7 @@ module RubyReactor
|
|
|
99
99
|
key
|
|
100
100
|
rescue KeyError
|
|
101
101
|
raise
|
|
102
|
-
rescue
|
|
102
|
+
rescue RubyReactor::Error::Rescuable => e
|
|
103
103
|
raise KeyError.new(
|
|
104
104
|
"#{step_name}: coordination key proc raised #{e.class}: #{e.message}",
|
|
105
105
|
step: step_name, original_error: e
|
|
@@ -188,7 +188,7 @@ module RubyReactor
|
|
|
188
188
|
# counter and marker must survive the park.
|
|
189
189
|
rescue Contended, Error::ExecutionParked
|
|
190
190
|
raise
|
|
191
|
-
rescue
|
|
191
|
+
rescue Error::Rescuable
|
|
192
192
|
clear_contention_state
|
|
193
193
|
raise
|
|
194
194
|
end
|
|
@@ -498,7 +498,7 @@ module RubyReactor
|
|
|
498
498
|
rescue Error::ExecutionParked
|
|
499
499
|
outcome = :parked
|
|
500
500
|
raise
|
|
501
|
-
rescue
|
|
501
|
+
rescue Error::Rescuable
|
|
502
502
|
outcome = :failed
|
|
503
503
|
raise
|
|
504
504
|
ensure
|
|
@@ -513,8 +513,8 @@ module RubyReactor
|
|
|
513
513
|
# step again and must keep its place: the stash survives, so the next
|
|
514
514
|
# attempt re-reads the SAME nonce. The heartbeat is stopped across the
|
|
515
515
|
# gap; `poison_pill_timeout` bounds it.
|
|
516
|
-
# - :abandoned — an
|
|
517
|
-
# `
|
|
516
|
+
# - :abandoned — an interruption (`Sidekiq::Shutdown`, `NoMemoryError`,
|
|
517
|
+
# ...; `Error::Rescuable`, 008 R-16). Not advanced: `Sidekiq::Shutdown` pushes the
|
|
518
518
|
# job back to run again, which must keep this place, and `failed: true`
|
|
519
519
|
# would poison successors for work that may yet complete. With the
|
|
520
520
|
# heartbeat stopped, the poison pill releases the position within
|
|
@@ -644,7 +644,7 @@ module RubyReactor
|
|
|
644
644
|
return skipped_for_period if storage_adapter.period_seen?(key)
|
|
645
645
|
|
|
646
646
|
result = yield
|
|
647
|
-
if
|
|
647
|
+
if continuing_success?(result) && !chain_failed?(result)
|
|
648
648
|
storage_adapter.period_mark(key, RubyReactor::Period.ttl_seconds(config[:every]))
|
|
649
649
|
end
|
|
650
650
|
result
|
|
@@ -654,8 +654,11 @@ module RubyReactor
|
|
|
654
654
|
RubyReactor.Skipped(nil, reason: :period, step_name: step_name)
|
|
655
655
|
end
|
|
656
656
|
|
|
657
|
-
|
|
658
|
-
|
|
657
|
+
# A body's `Skipped` marks the bucket like a Success: it is only an
|
|
658
|
+
# instrumentation mark. The library's own skips (period, ordered lock)
|
|
659
|
+
# are returned by the gates around this one, never through it.
|
|
660
|
+
def continuing_success?(result)
|
|
661
|
+
result.is_a?(RubyReactor::Success) && !result.is_a?(RubyReactor::Halt)
|
|
659
662
|
end
|
|
660
663
|
|
|
661
664
|
def period_key(config)
|