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
data/lib/ruby_reactor.rb
CHANGED
|
@@ -4,6 +4,11 @@ require "zeitwerk"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require_relative "ruby_reactor/registry"
|
|
6
6
|
require_relative "ruby_reactor/utils/code_extractor"
|
|
7
|
+
require_relative "ruby_reactor/dsl/lockable" # Add this
|
|
8
|
+
require_relative "ruby_reactor/lock"
|
|
9
|
+
require_relative "ruby_reactor/semaphore"
|
|
10
|
+
require_relative "ruby_reactor/period"
|
|
11
|
+
require_relative "ruby_reactor/rate_limit"
|
|
7
12
|
|
|
8
13
|
# Load dry-validation if available (for validation features)
|
|
9
14
|
begin
|
|
@@ -20,7 +25,7 @@ rescue LoadError
|
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
loader = Zeitwerk::Loader.for_gem
|
|
23
|
-
loader.inflector.inflect("api" => "API")
|
|
28
|
+
loader.inflector.inflect("api" => "API", "rspec" => "RSpec")
|
|
24
29
|
loader.setup
|
|
25
30
|
|
|
26
31
|
module RubyReactor
|
|
@@ -39,20 +44,76 @@ module RubyReactor
|
|
|
39
44
|
def failure?
|
|
40
45
|
false
|
|
41
46
|
end
|
|
47
|
+
|
|
48
|
+
def skipped?
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_h
|
|
53
|
+
{ success: true, value: @value }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A "clean halt" signal. Two ways to produce one:
|
|
58
|
+
#
|
|
59
|
+
# 1. Implicitly, when a reactor's `with_period` gate finds the bucket has
|
|
60
|
+
# already been claimed. The executor short-circuits before any step
|
|
61
|
+
# runs.
|
|
62
|
+
#
|
|
63
|
+
# 2. Explicitly, by returning `RubyReactor.Skipped(reason: "...")` from a
|
|
64
|
+
# step's `run` block. The reactor halts immediately — no further steps,
|
|
65
|
+
# and crucially **no compensation** of already-completed steps. Use this
|
|
66
|
+
# when a step discovers that the rest of the workflow is not needed
|
|
67
|
+
# (e.g. "user already opted out", "nothing to do this round") and the
|
|
68
|
+
# partial progress is still correct to keep.
|
|
69
|
+
#
|
|
70
|
+
# Subclass of Success so callers that only check `success?` continue to work;
|
|
71
|
+
# `skipped?` distinguishes it.
|
|
72
|
+
class Skipped < Success
|
|
73
|
+
attr_reader :reason, :period_key, :step_name
|
|
74
|
+
|
|
75
|
+
def initialize(reason: nil, period_key: nil, step_name: nil)
|
|
76
|
+
super(nil)
|
|
77
|
+
@reason = reason
|
|
78
|
+
@period_key = period_key
|
|
79
|
+
@step_name = step_name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def skipped?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
42
85
|
end
|
|
43
86
|
|
|
44
87
|
class Failure
|
|
45
88
|
attr_reader :error, :retryable, :step_name, :inputs, :backtrace, :reactor_name, :step_arguments, :exception_class,
|
|
46
89
|
:file_path, :line_number, :code_snippet, :validation_errors
|
|
47
90
|
|
|
48
|
-
# rubocop:disable Metrics/ParameterLists
|
|
91
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
49
92
|
def initialize(error, retryable: nil, step_name: nil, inputs: {}, backtrace: nil, redact_inputs: [],
|
|
50
93
|
reactor_name: nil, step_arguments: {}, exception_class: nil,
|
|
51
94
|
file_path: nil, line_number: nil, code_snippet: nil, invalid_payload: false, validation_errors: nil)
|
|
52
|
-
# rubocop:enable Metrics/ParameterLists
|
|
95
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
53
96
|
@error = error
|
|
97
|
+
|
|
98
|
+
# Handle case where error is a serialized hash (e.g. from async failure propagation)
|
|
99
|
+
if @error.is_a?(Hash)
|
|
100
|
+
attributes = extract_attributes_from_hash(@error)
|
|
101
|
+
@error = attributes[:error]
|
|
102
|
+
retryable = attributes[:retryable] if retryable.nil?
|
|
103
|
+
step_name ||= attributes[:step_name]
|
|
104
|
+
reactor_name ||= attributes[:reactor_name]
|
|
105
|
+
inputs = attributes[:inputs] if inputs.empty?
|
|
106
|
+
step_arguments = attributes[:step_arguments] if step_arguments.empty?
|
|
107
|
+
raw_backtrace ||= attributes[:backtrace] || backtrace
|
|
108
|
+
exception_class ||= attributes[:exception_class]
|
|
109
|
+
file_path ||= attributes[:file_path]
|
|
110
|
+
line_number ||= attributes[:line_number]
|
|
111
|
+
code_snippet ||= attributes[:code_snippet]
|
|
112
|
+
validation_errors ||= attributes[:validation_errors]
|
|
113
|
+
end
|
|
114
|
+
|
|
54
115
|
@retryable = if retryable.nil?
|
|
55
|
-
error.respond_to?(:retryable?) ? error.retryable? : true
|
|
116
|
+
@error.respond_to?(:retryable?) ? @error.retryable? : true
|
|
56
117
|
else
|
|
57
118
|
retryable
|
|
58
119
|
end
|
|
@@ -60,10 +121,10 @@ module RubyReactor
|
|
|
60
121
|
@reactor_name = reactor_name
|
|
61
122
|
@inputs = inputs
|
|
62
123
|
@step_arguments = step_arguments
|
|
63
|
-
raw_backtrace
|
|
124
|
+
raw_backtrace ||= backtrace || (@error.respond_to?(:backtrace) ? @error.backtrace : caller)
|
|
64
125
|
@backtrace = filter_backtrace(raw_backtrace)
|
|
65
126
|
@redact_inputs = redact_inputs
|
|
66
|
-
@exception_class = exception_class || (error.is_a?(Exception) ? error.class.name : nil)
|
|
127
|
+
@exception_class = exception_class || (@error.is_a?(Exception) ? @error.class.name : nil)
|
|
67
128
|
@file_path = file_path
|
|
68
129
|
@line_number = line_number
|
|
69
130
|
@code_snippet = code_snippet
|
|
@@ -83,6 +144,10 @@ module RubyReactor
|
|
|
83
144
|
@retryable
|
|
84
145
|
end
|
|
85
146
|
|
|
147
|
+
def skipped?
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
86
151
|
def invalid_payload?
|
|
87
152
|
@invalid_payload
|
|
88
153
|
end
|
|
@@ -99,6 +164,28 @@ module RubyReactor
|
|
|
99
164
|
msg.join("\n")
|
|
100
165
|
end
|
|
101
166
|
|
|
167
|
+
def to_s
|
|
168
|
+
message
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def to_h
|
|
172
|
+
{
|
|
173
|
+
success: false,
|
|
174
|
+
error: error_message,
|
|
175
|
+
step_name: @step_name,
|
|
176
|
+
inputs: @inputs,
|
|
177
|
+
redact_inputs: @redact_inputs,
|
|
178
|
+
reactor_name: @reactor_name,
|
|
179
|
+
step_arguments: @step_arguments,
|
|
180
|
+
exception_class: @exception_class,
|
|
181
|
+
file_path: @file_path,
|
|
182
|
+
line_number: @line_number,
|
|
183
|
+
code_snippet: @code_snippet,
|
|
184
|
+
validation_errors: @validation_errors,
|
|
185
|
+
backtrace: @backtrace
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
102
189
|
private
|
|
103
190
|
|
|
104
191
|
def build_header
|
|
@@ -147,10 +234,6 @@ module RubyReactor
|
|
|
147
234
|
msg << backtrace.take(10).map { |line| " #{line}" }.join("\n")
|
|
148
235
|
end
|
|
149
236
|
|
|
150
|
-
def to_s
|
|
151
|
-
message
|
|
152
|
-
end
|
|
153
|
-
|
|
154
237
|
def filter_backtrace(backtrace)
|
|
155
238
|
return backtrace if ENV["RUBY_REACTOR_DEBUG"] == "true"
|
|
156
239
|
return backtrace if backtrace.nil? || backtrace.empty?
|
|
@@ -177,6 +260,26 @@ module RubyReactor
|
|
|
177
260
|
def error_message
|
|
178
261
|
@error.respond_to?(:message) ? @error.message : @error.to_s
|
|
179
262
|
end
|
|
263
|
+
|
|
264
|
+
def extract_attributes_from_hash(error_hash)
|
|
265
|
+
# Ensure indifferent access
|
|
266
|
+
err = ->(k) { error_hash[k.to_s] || error_hash[k.to_sym] }
|
|
267
|
+
|
|
268
|
+
{
|
|
269
|
+
error: err[:message] || err[:error] || error_hash,
|
|
270
|
+
retryable: err[:retryable],
|
|
271
|
+
step_name: err[:step_name],
|
|
272
|
+
reactor_name: err[:reactor_name],
|
|
273
|
+
inputs: err[:inputs] || {},
|
|
274
|
+
step_arguments: err[:step_arguments] || {},
|
|
275
|
+
backtrace: err[:backtrace],
|
|
276
|
+
exception_class: err[:exception_class],
|
|
277
|
+
file_path: err[:file_path],
|
|
278
|
+
line_number: err[:line_number],
|
|
279
|
+
code_snippet: err[:code_snippet],
|
|
280
|
+
validation_errors: err[:validation_errors]
|
|
281
|
+
}
|
|
282
|
+
end
|
|
180
283
|
end
|
|
181
284
|
|
|
182
285
|
# Async result for background job execution
|
|
@@ -211,6 +314,12 @@ module RubyReactor
|
|
|
211
314
|
Failure.new(error, **kwargs)
|
|
212
315
|
end
|
|
213
316
|
|
|
317
|
+
# Build a `Skipped` result. Return one from a step's `run` block to halt the
|
|
318
|
+
# reactor cleanly without triggering compensation of previous steps.
|
|
319
|
+
def self.Skipped(reason: nil, **kwargs)
|
|
320
|
+
Skipped.new(reason: reason, **kwargs)
|
|
321
|
+
end
|
|
322
|
+
|
|
214
323
|
def self.configure
|
|
215
324
|
yield(Configuration.instance) if block_given?
|
|
216
325
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_reactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-validation
|
|
@@ -107,7 +107,9 @@ files:
|
|
|
107
107
|
- documentation/images/failed_order_processing.png
|
|
108
108
|
- documentation/images/payment_workflow.png
|
|
109
109
|
- documentation/interrupts.md
|
|
110
|
+
- documentation/locks_and_semaphores.md
|
|
110
111
|
- documentation/retry_configuration.md
|
|
112
|
+
- documentation/testing.md
|
|
111
113
|
- gui/.gitignore
|
|
112
114
|
- gui/README.md
|
|
113
115
|
- gui/eslint.config.js
|
|
@@ -139,7 +141,6 @@ files:
|
|
|
139
141
|
- gui/vite.config.ts
|
|
140
142
|
- gui/vitest.config.ts
|
|
141
143
|
- lib/ruby_reactor.rb
|
|
142
|
-
- lib/ruby_reactor/async_router.rb
|
|
143
144
|
- lib/ruby_reactor/configuration.rb
|
|
144
145
|
- lib/ruby_reactor/context.rb
|
|
145
146
|
- lib/ruby_reactor/context_serializer.rb
|
|
@@ -147,6 +148,7 @@ files:
|
|
|
147
148
|
- lib/ruby_reactor/dsl/compose_builder.rb
|
|
148
149
|
- lib/ruby_reactor/dsl/interrupt_builder.rb
|
|
149
150
|
- lib/ruby_reactor/dsl/interrupt_step_config.rb
|
|
151
|
+
- lib/ruby_reactor/dsl/lockable.rb
|
|
150
152
|
- lib/ruby_reactor/dsl/map_builder.rb
|
|
151
153
|
- lib/ruby_reactor/dsl/reactor.rb
|
|
152
154
|
- lib/ruby_reactor/dsl/step_builder.rb
|
|
@@ -170,6 +172,7 @@ files:
|
|
|
170
172
|
- lib/ruby_reactor/executor/retry_manager.rb
|
|
171
173
|
- lib/ruby_reactor/executor/step_executor.rb
|
|
172
174
|
- lib/ruby_reactor/interrupt_result.rb
|
|
175
|
+
- lib/ruby_reactor/lock.rb
|
|
173
176
|
- lib/ruby_reactor/map/collector.rb
|
|
174
177
|
- lib/ruby_reactor/map/dispatcher.rb
|
|
175
178
|
- lib/ruby_reactor/map/element_executor.rb
|
|
@@ -177,10 +180,19 @@ files:
|
|
|
177
180
|
- lib/ruby_reactor/map/helpers.rb
|
|
178
181
|
- lib/ruby_reactor/map/result_enumerator.rb
|
|
179
182
|
- lib/ruby_reactor/max_retries_exhausted_failure.rb
|
|
183
|
+
- lib/ruby_reactor/period.rb
|
|
184
|
+
- lib/ruby_reactor/rate_limit.rb
|
|
180
185
|
- lib/ruby_reactor/reactor.rb
|
|
181
186
|
- lib/ruby_reactor/registry.rb
|
|
182
187
|
- lib/ruby_reactor/retry_context.rb
|
|
183
188
|
- lib/ruby_reactor/retry_queued_result.rb
|
|
189
|
+
- lib/ruby_reactor/rspec.rb
|
|
190
|
+
- lib/ruby_reactor/rspec/helpers.rb
|
|
191
|
+
- lib/ruby_reactor/rspec/matchers.rb
|
|
192
|
+
- lib/ruby_reactor/rspec/step_executor_patch.rb
|
|
193
|
+
- lib/ruby_reactor/rspec/test_subject.rb
|
|
194
|
+
- lib/ruby_reactor/semaphore.rb
|
|
195
|
+
- lib/ruby_reactor/sidekiq_adapter.rb
|
|
184
196
|
- lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
|
|
185
197
|
- lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
|
|
186
198
|
- lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
|
|
@@ -191,6 +203,7 @@ files:
|
|
|
191
203
|
- lib/ruby_reactor/storage/adapter.rb
|
|
192
204
|
- lib/ruby_reactor/storage/configuration.rb
|
|
193
205
|
- lib/ruby_reactor/storage/redis_adapter.rb
|
|
206
|
+
- lib/ruby_reactor/storage/redis_locking.rb
|
|
194
207
|
- lib/ruby_reactor/template/base.rb
|
|
195
208
|
- lib/ruby_reactor/template/dynamic_source.rb
|
|
196
209
|
- lib/ruby_reactor/template/element.rb
|