ruby_reactor 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/README.md +570 -0
- data/Rakefile +12 -0
- data/documentation/DAG.md +457 -0
- data/documentation/README.md +123 -0
- data/documentation/async_reactors.md +369 -0
- data/documentation/composition.md +199 -0
- data/documentation/core_concepts.md +662 -0
- data/documentation/data_pipelines.md +224 -0
- data/documentation/examples/inventory_management.md +749 -0
- data/documentation/examples/order_processing.md +365 -0
- data/documentation/examples/payment_processing.md +654 -0
- data/documentation/getting_started.md +224 -0
- data/documentation/retry_configuration.md +357 -0
- data/lib/ruby_reactor/async_router.rb +91 -0
- data/lib/ruby_reactor/configuration.rb +41 -0
- data/lib/ruby_reactor/context.rb +169 -0
- data/lib/ruby_reactor/context_serializer.rb +164 -0
- data/lib/ruby_reactor/dependency_graph.rb +126 -0
- data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
- data/lib/ruby_reactor/dsl/reactor.rb +151 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
- data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
- data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
- data/lib/ruby_reactor/error/base.rb +16 -0
- data/lib/ruby_reactor/error/compensation_error.rb +8 -0
- data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
- data/lib/ruby_reactor/error/dependency_error.rb +8 -0
- data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
- data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
- data/lib/ruby_reactor/error/undo_error.rb +8 -0
- data/lib/ruby_reactor/error/validation_error.rb +8 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
- data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
- data/lib/ruby_reactor/executor/input_validator.rb +39 -0
- data/lib/ruby_reactor/executor/result_handler.rb +103 -0
- data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
- data/lib/ruby_reactor/executor/step_executor.rb +319 -0
- data/lib/ruby_reactor/executor.rb +123 -0
- data/lib/ruby_reactor/map/collector.rb +65 -0
- data/lib/ruby_reactor/map/element_executor.rb +154 -0
- data/lib/ruby_reactor/map/execution.rb +60 -0
- data/lib/ruby_reactor/map/helpers.rb +67 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
- data/lib/ruby_reactor/reactor.rb +75 -0
- data/lib/ruby_reactor/retry_context.rb +92 -0
- data/lib/ruby_reactor/retry_queued_result.rb +26 -0
- data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
- data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
- data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
- data/lib/ruby_reactor/step/compose_step.rb +107 -0
- data/lib/ruby_reactor/step/map_step.rb +234 -0
- data/lib/ruby_reactor/step.rb +33 -0
- data/lib/ruby_reactor/storage/adapter.rb +51 -0
- data/lib/ruby_reactor/storage/configuration.rb +15 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
- data/lib/ruby_reactor/template/base.rb +15 -0
- data/lib/ruby_reactor/template/element.rb +25 -0
- data/lib/ruby_reactor/template/input.rb +48 -0
- data/lib/ruby_reactor/template/result.rb +48 -0
- data/lib/ruby_reactor/template/value.rb +22 -0
- data/lib/ruby_reactor/validation/base.rb +26 -0
- data/lib/ruby_reactor/validation/input_validator.rb +62 -0
- data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
- data/lib/ruby_reactor/version.rb +5 -0
- data/lib/ruby_reactor.rb +159 -0
- data/sig/ruby_reactor.rbs +4 -0
- metadata +178 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# Core Concepts
|
|
2
|
+
|
|
3
|
+
Understanding RubyReactor's core concepts is essential for building reliable sequential business processes.
|
|
4
|
+
|
|
5
|
+
## Reactor
|
|
6
|
+
|
|
7
|
+
A Reactor is the main execution unit that orchestrates steps in a specific order.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class OrderProcessingReactor < RubyReactor::Reactor
|
|
11
|
+
# Reactor definition
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Key Characteristics:**
|
|
16
|
+
- **Sequential Execution**: Steps run one after another in dependency order
|
|
17
|
+
- **Error Handling**: Automatic rollback on failures
|
|
18
|
+
- **Compensation**: Undo operations for failed steps
|
|
19
|
+
- **Result Aggregation**: Collects results from all steps
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
Steps are the individual units of work within a reactor. Each step has a name and implementation.
|
|
24
|
+
|
|
25
|
+
### Inline Step Definition
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
step :validate_order do
|
|
29
|
+
run do |order_id|
|
|
30
|
+
# Step implementation
|
|
31
|
+
order = Order.find(order_id)
|
|
32
|
+
raise "Order not found" unless order
|
|
33
|
+
Success({ order: order })
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Step Classes
|
|
39
|
+
|
|
40
|
+
For complex steps with compensation and undo logic, or for better testability and reusability, you can define steps as separate classes that include the `RubyReactor::Step` module. This is the preferred approach for steps that require sophisticated error handling or have significant business logic.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class ReserveInventoryStep
|
|
44
|
+
include RubyReactor::Step
|
|
45
|
+
|
|
46
|
+
def self.run(arguments, context)
|
|
47
|
+
order = arguments[:order]
|
|
48
|
+
# Business logic for inventory reservation
|
|
49
|
+
reservation_id = InventoryService.reserve(order[:items])
|
|
50
|
+
Success({
|
|
51
|
+
reservation_id: reservation_id,
|
|
52
|
+
reserved_items: order[:items].size
|
|
53
|
+
})
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.compensate(error, arguments, context)
|
|
57
|
+
# Cleanup logic for failed reservations
|
|
58
|
+
puts "Cleaning up inventory reservation due to: #{error.message}"
|
|
59
|
+
# Release any partial reservations
|
|
60
|
+
Success("Inventory reservation cleaned up")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.undo(result, arguments, context)
|
|
64
|
+
# Rollback logic for successful reservations during reactor failure
|
|
65
|
+
reservation_id = result[:reservation_id]
|
|
66
|
+
InventoryService.release(reservation_id)
|
|
67
|
+
Success("Inventory reservation released")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To use a step class in a reactor, reference it by class:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class OrderProcessingReactor < RubyReactor::Reactor
|
|
76
|
+
step :reserve_inventory, ReserveInventoryStep do
|
|
77
|
+
argument :order, result(:validate_order)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Benefits of Step Classes:**
|
|
83
|
+
- **Reusability**: Step classes can be shared across multiple reactors
|
|
84
|
+
- **Testability**: Easier to unit test individual step logic in isolation
|
|
85
|
+
- **Organization**: Complex business logic is better organized in dedicated classes
|
|
86
|
+
- **Maintainability**: Compensation and undo logic is clearly separated
|
|
87
|
+
- **Readability**: Reactor definitions remain focused on orchestration
|
|
88
|
+
|
|
89
|
+
**Step Class Methods:**
|
|
90
|
+
- **`run(arguments, context)`**: The main business logic. Returns `Success(result)` or `Failure(error)`
|
|
91
|
+
- **`compensate(error, arguments, context)`**: Cleanup for the current failing step. Called when the step fails
|
|
92
|
+
- **`undo(result, arguments, context)`**: Rollback for previously successful steps. Called during reactor failure rollback
|
|
93
|
+
|
|
94
|
+
**Step Components:**
|
|
95
|
+
- **Name**: Unique identifier (symbol)
|
|
96
|
+
- **Implementation**: The `run` block containing business logic
|
|
97
|
+
- **Dependencies**: Other steps that must complete first
|
|
98
|
+
- **Compensation**: Undo logic for rollback scenarios
|
|
99
|
+
|
|
100
|
+
## Context
|
|
101
|
+
|
|
102
|
+
Context holds the execution state throughout the reactor lifecycle.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
context = RubyReactor::Context.new(order_id: 123, customer_id: 456)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Context Contents:**
|
|
109
|
+
- **Inputs**: Original parameters passed to `Reactor.run()`
|
|
110
|
+
- **Intermediate Results**: Outputs from completed steps
|
|
111
|
+
- **Completed Steps**: Set of successfully finished step names
|
|
112
|
+
- **Step Results**: Final outputs from each step
|
|
113
|
+
- **Execution Metadata**: Job IDs, timestamps, reactor class info
|
|
114
|
+
|
|
115
|
+
## Dependencies
|
|
116
|
+
|
|
117
|
+
Steps can depend on other steps, creating a directed acyclic graph (DAG) of execution.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
step :validate_order do
|
|
121
|
+
run { validate_order_logic }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
step :process_payment do
|
|
125
|
+
argument :order, result(:validate_order)
|
|
126
|
+
run do |args, _context|
|
|
127
|
+
process_payment_for_order(args[:order])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
step :send_confirmation do
|
|
132
|
+
argument :payment_result, result(:process_payment)
|
|
133
|
+
run do |args, _context|
|
|
134
|
+
payment_result = args[:payment_result]
|
|
135
|
+
send_confirmation_email(payment_result[:order], payment_result[:payment_id])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Dependency Resolution:**
|
|
141
|
+
- Topological sorting ensures correct execution order
|
|
142
|
+
- Future feature: Parallel execution of independent steps (when available)
|
|
143
|
+
- Validation prevents circular dependencies
|
|
144
|
+
|
|
145
|
+
## Results
|
|
146
|
+
|
|
147
|
+
Every reactor execution returns a comprehensive result object.
|
|
148
|
+
<!--
|
|
149
|
+
# TODO
|
|
150
|
+
|
|
151
|
+
This is not true, update to use instance and what is stored
|
|
152
|
+
```ruby
|
|
153
|
+
result = OrderProcessingReactor.run(order_id: 123)
|
|
154
|
+
|
|
155
|
+
# Overall status
|
|
156
|
+
result.success? # => true/false
|
|
157
|
+
result.failure? # => true/false
|
|
158
|
+
|
|
159
|
+
# Step outputs
|
|
160
|
+
result.step_results # => { validate_order: {...}, process_payment: {...} }
|
|
161
|
+
result.intermediate_results # => Hash of all step outputs
|
|
162
|
+
|
|
163
|
+
# Execution tracking
|
|
164
|
+
result.completed_steps # => #<Set: {:validate_order, :process_payment}>
|
|
165
|
+
result.inputs # => { order_id: 123 }
|
|
166
|
+
|
|
167
|
+
# Error information
|
|
168
|
+
result.error # => Exception object if failed
|
|
169
|
+
``` -->
|
|
170
|
+
|
|
171
|
+
## Error Handling
|
|
172
|
+
|
|
173
|
+
RubyReactor provides sophisticated error handling with automatic compensation.
|
|
174
|
+
|
|
175
|
+
### Step Failures
|
|
176
|
+
|
|
177
|
+
When a step fails, execution stops and compensation begins:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
step :process_payment do
|
|
181
|
+
run do
|
|
182
|
+
# This might fail
|
|
183
|
+
PaymentService.charge(amount, token)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
compensate do |payment_id: nil, **|
|
|
187
|
+
# Undo the payment if it was created
|
|
188
|
+
PaymentService.refund(payment_id) if payment_id
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Compensation Order
|
|
194
|
+
|
|
195
|
+
Compensation runs in reverse order of successful steps:
|
|
196
|
+
|
|
197
|
+
```mermaid
|
|
198
|
+
graph TD
|
|
199
|
+
A[Step A succeeds] --> B[Step B succeeds]
|
|
200
|
+
B --> C[Step C fails]
|
|
201
|
+
C --> D[Compensate C]
|
|
202
|
+
D --> E[Undo B]
|
|
203
|
+
E --> F[Undo A]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Error Types
|
|
207
|
+
|
|
208
|
+
- **StepExecutionError**: Business logic failures
|
|
209
|
+
- **DependencyError**: Missing required dependencies
|
|
210
|
+
- **ValidationError**: Input validation failures
|
|
211
|
+
- **CompensationError**: Compensation logic failures
|
|
212
|
+
|
|
213
|
+
## Retries
|
|
214
|
+
|
|
215
|
+
RubyReactor supports automatic retry mechanisms for failed steps with configurable backoff strategies.
|
|
216
|
+
|
|
217
|
+
### When Retries Occur
|
|
218
|
+
|
|
219
|
+
When a step fails during execution, RubyReactor can automatically retry the step before triggering compensation and rollback. Retries occur when:
|
|
220
|
+
|
|
221
|
+
1. A step raises an exception during its `run` block
|
|
222
|
+
2. The step has retry configuration (either reactor-level defaults or step-specific settings)
|
|
223
|
+
3. The maximum retry attempts haven't been exceeded
|
|
224
|
+
|
|
225
|
+
### Retry Execution Flow
|
|
226
|
+
|
|
227
|
+
```mermaid
|
|
228
|
+
graph TD
|
|
229
|
+
A[Step Fails] --> B{Attempts < Max<br/>Attempts?}
|
|
230
|
+
B -->|Yes| C[Calculate Backoff Delay]
|
|
231
|
+
C --> D[Queue for Retry<br/>with Delay]
|
|
232
|
+
D --> E[Resume Execution<br/>from Failed Step]
|
|
233
|
+
B -->|No| F[All Retries Exhausted]
|
|
234
|
+
F --> G[Run Compensation<br/>for Failing Step]
|
|
235
|
+
G --> H[Run Undo for<br/>Successful Steps<br/>in Reverse Order]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Retry Configuration
|
|
239
|
+
|
|
240
|
+
Retries can be configured at the reactor level (as defaults) or per step:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
class OrderProcessingReactor < RubyReactor::Reactor
|
|
244
|
+
step :validate_order do
|
|
245
|
+
run do
|
|
246
|
+
# validate input
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
undo do
|
|
250
|
+
# Nothing to do here just as example
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
step :check_inventory do
|
|
255
|
+
# Uses reactor defaults (5 attempts, fixed backoff)
|
|
256
|
+
run do
|
|
257
|
+
InventoryService.check_availability(product_id, quantity)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
undo do
|
|
261
|
+
# Nothing to do here just as example
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
step :reserve_inventory do
|
|
266
|
+
retries max_attempts: 5, backoff: :fixed, base_delay: 2 # 2 seconds
|
|
267
|
+
|
|
268
|
+
run do
|
|
269
|
+
InventoryService.reserve(product_id, quantity)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
compensate do |error, arguments, context|
|
|
273
|
+
# Cleanup partial reservations
|
|
274
|
+
puts "Cleaning up inventory reservation due to: #{error.message}"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Retry Parameters
|
|
281
|
+
|
|
282
|
+
- **`max_attempts`**: Maximum number of execution attempts (including initial attempt)
|
|
283
|
+
- **`backoff`**: Strategy for calculating delays between retries
|
|
284
|
+
- `:exponential` (default): Delay doubles with each attempt
|
|
285
|
+
- `:linear`: Delay increases linearly
|
|
286
|
+
- `:fixed`: Same delay for each attempt
|
|
287
|
+
- **`base_delay`**: Base delay for calculations (in seconds or ActiveSupport duration)
|
|
288
|
+
|
|
289
|
+
### Example Execution with Retries
|
|
290
|
+
|
|
291
|
+
Consider a reactor where `reserve_inventory` fails has a set `retries` with max_attemps of 5 max attempts with fixed backoff:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
1. run step=validate_order # Success
|
|
295
|
+
2. run step=check_inventory # Success
|
|
296
|
+
3. run step=reserve_inventory # Attempt 1 - Fails
|
|
297
|
+
4. run step=reserve_inventory # Attempt 2 - Fails (retry with 2s delay)
|
|
298
|
+
5. run step=reserve_inventory # Attempt 3 - Fails (retry with 2s delay)
|
|
299
|
+
6. run step=reserve_inventory # Attempt 4 - Fails (retry with 2s delay)
|
|
300
|
+
7. run step=reserve_inventory # Attempt 5 - Fails (retry with 2s delay)
|
|
301
|
+
8. compensate step=reserve_inventory # All retries exhausted
|
|
302
|
+
9. undo step=check_inventory # Rollback successful steps
|
|
303
|
+
10. undo step=validate_order # in reverse order
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Retry vs Compensation vs Undo
|
|
307
|
+
|
|
308
|
+
- **Retries**: Re-attempt the failing step with backoff delays
|
|
309
|
+
- **Compensation**: Cleanup logic for the failing step after all retries are exhausted
|
|
310
|
+
- **Undo**: Rollback logic for previously successful steps during reactor failure
|
|
311
|
+
|
|
312
|
+
Retries happen first, followed by compensation and undo only if all retry attempts fail.
|
|
313
|
+
|
|
314
|
+
### Asynchronous Retries
|
|
315
|
+
|
|
316
|
+
For asynchronous reactors, retries are queued as background jobs with calculated delays, preventing worker thread blocking:
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
class AsyncPaymentReactor < RubyReactor::Reactor
|
|
320
|
+
async true
|
|
321
|
+
|
|
322
|
+
step :charge_card do
|
|
323
|
+
retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
|
|
324
|
+
run do
|
|
325
|
+
# This might fail due to network issues
|
|
326
|
+
PaymentService.charge(card_token, amount)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Failed steps are automatically requeued with exponential backoff delays, allowing workers to process other jobs while waiting.
|
|
333
|
+
|
|
334
|
+
## Execution Models
|
|
335
|
+
|
|
336
|
+
### Synchronous Execution
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
result = Reactor.run(inputs)
|
|
340
|
+
# Blocks until completion
|
|
341
|
+
# Returns Result object immediately
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Characteristics:**
|
|
345
|
+
- Blocking execution in current thread
|
|
346
|
+
- Immediate results
|
|
347
|
+
- Simple error handling
|
|
348
|
+
- Limited scalability
|
|
349
|
+
|
|
350
|
+
### Asynchronous Execution
|
|
351
|
+
|
|
352
|
+
<!-- TODO: review this part -->
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
async_result = Reactor.run(inputs)
|
|
356
|
+
# Returns immediately
|
|
357
|
+
# Check status later
|
|
358
|
+
|
|
359
|
+
case async_result.status
|
|
360
|
+
when :success
|
|
361
|
+
result = async_result.result
|
|
362
|
+
when :failed
|
|
363
|
+
error = async_result.error
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Characteristics:**
|
|
368
|
+
- Non-blocking execution
|
|
369
|
+
- Background processing with Sidekiq
|
|
370
|
+
- Retry capabilities
|
|
371
|
+
- Better scalability
|
|
372
|
+
|
|
373
|
+
## Step Arguments
|
|
374
|
+
|
|
375
|
+
Steps receive arguments through keyword arguments, with automatic dependency injection.
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
step :validate_order do
|
|
379
|
+
run do |order_id:, customer_id:|
|
|
380
|
+
# Direct access to reactor inputs
|
|
381
|
+
order = Order.find_by(id: order_id, customer_id: customer_id)
|
|
382
|
+
Success({ order: order })
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
step :process_payment do
|
|
387
|
+
argument :order_data, result(:validate_order)
|
|
388
|
+
|
|
389
|
+
run do |args, _context|
|
|
390
|
+
# Access results from previous steps
|
|
391
|
+
order = args[:order_data][:order]
|
|
392
|
+
payment = PaymentService.charge(order.total, order.card_token)
|
|
393
|
+
Success({ payment_id: payment.id })
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Argument Resolution:**
|
|
399
|
+
1. **Step Results**: Outputs from completed dependent steps
|
|
400
|
+
2. **Reactor Inputs**: Original inputs passed to `run()`
|
|
401
|
+
3. **Intermediate Results**: Accumulated outputs from all steps
|
|
402
|
+
|
|
403
|
+
## Undo
|
|
404
|
+
|
|
405
|
+
Undo provides transactional rollback for previously successful steps when a later step fails.
|
|
406
|
+
|
|
407
|
+
### When Undo Runs
|
|
408
|
+
|
|
409
|
+
Unlike compensation which only runs for the failing step, undo is triggered during the **backwalk** phase when rolling back the entire reactor execution. When a step fails:
|
|
410
|
+
|
|
411
|
+
1. **Compensation** runs for the failing step itself
|
|
412
|
+
2. **Undo** runs for all previously successful steps in reverse order
|
|
413
|
+
|
|
414
|
+
### Basic Undo
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
step :reserve_inventory do
|
|
418
|
+
run do |items:|
|
|
419
|
+
reservation_id = InventoryService.reserve(items)
|
|
420
|
+
Success({ reservation_id: reservation_id })
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
undo do |reservation_result, arguments, context|
|
|
424
|
+
# Undo receives the step's result, arguments, and full context
|
|
425
|
+
reservation_id = reservation_result[:reservation_id]
|
|
426
|
+
InventoryService.release(reservation_id)
|
|
427
|
+
Success("Inventory reservation released")
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Undo Context
|
|
433
|
+
|
|
434
|
+
Undo blocks receive three parameters:
|
|
435
|
+
- **Result**: The successful result from the step's `run` block
|
|
436
|
+
- **Arguments**: The resolved arguments passed to the step
|
|
437
|
+
- **Context**: The full execution context with all intermediate results
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
step :complex_operation do
|
|
441
|
+
run do |input:|
|
|
442
|
+
# Complex operation that modifies external state
|
|
443
|
+
record = create_record(input)
|
|
444
|
+
notification = send_notification(record)
|
|
445
|
+
Success({ record_id: record.id, notification_id: notification.id })
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
undo do |result, arguments, context|
|
|
449
|
+
# Clean up in reverse order of creation
|
|
450
|
+
notification_id = result[:notification_id]
|
|
451
|
+
record_id = result[:record_id]
|
|
452
|
+
|
|
453
|
+
delete_notification(notification_id) if notification_id
|
|
454
|
+
delete_record(record_id) if record_id
|
|
455
|
+
|
|
456
|
+
Success("Complex operation fully undone")
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Undo vs Compensation
|
|
462
|
+
|
|
463
|
+
- **Compensation**: Handles cleanup for the currently failing step
|
|
464
|
+
- **Undo**: Handles rollback of all previously successful steps during reactor failure
|
|
465
|
+
|
|
466
|
+
Both mechanisms work together to ensure transactional semantics across complex business processes.
|
|
467
|
+
|
|
468
|
+
## Compensation
|
|
469
|
+
|
|
470
|
+
Compensation provides cleanup logic for steps that fail during execution. Unlike undo which handles rollback of successful steps, compensation is specific to the failing step itself.
|
|
471
|
+
|
|
472
|
+
### When Compensation Runs
|
|
473
|
+
|
|
474
|
+
Compensation runs immediately when a step fails, before the broader rollback process begins. It allows the failing step to clean up any partial state changes it may have made.
|
|
475
|
+
|
|
476
|
+
### Basic Compensation
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
step :reserve_inventory do
|
|
480
|
+
run do |items:|
|
|
481
|
+
reservation_id = InventoryService.reserve(items)
|
|
482
|
+
Success({ reservation_id: reservation_id })
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
compensate do |error, arguments, context|
|
|
486
|
+
# Clean up partial reservations if the step failed
|
|
487
|
+
# Note: This step didn't succeed, so we don't have a result to undo
|
|
488
|
+
# Instead, we work with the error and arguments
|
|
489
|
+
puts "Cleaning up after reservation failure: #{error.message}"
|
|
490
|
+
# Any cleanup logic specific to this step's failure
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Compensation Context
|
|
496
|
+
|
|
497
|
+
Compensation blocks receive three parameters:
|
|
498
|
+
- **Error**: The exception that caused the step to fail
|
|
499
|
+
- **Arguments**: The resolved arguments that were passed to the step
|
|
500
|
+
- **Context**: The full execution context
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
step :process_payment do
|
|
504
|
+
run do |order:, payment_method:|
|
|
505
|
+
# Payment processing logic that might fail
|
|
506
|
+
PaymentService.charge(order.total, payment_method)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
compensate do |error, arguments, context|
|
|
510
|
+
# Handle payment processing failure
|
|
511
|
+
order = arguments[:order]
|
|
512
|
+
payment_method = arguments[:payment_method]
|
|
513
|
+
|
|
514
|
+
# Log the failure for audit purposes
|
|
515
|
+
AuditService.log_payment_failure(order.id, error.message)
|
|
516
|
+
|
|
517
|
+
# Send notification about payment failure
|
|
518
|
+
NotificationService.send_payment_failed_email(order.customer_email, order.id)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
## Validation
|
|
526
|
+
|
|
527
|
+
Input validation ensures data integrity before execution.
|
|
528
|
+
|
|
529
|
+
### Built-in Validation
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
class OrderReactor < RubyReactor::Reactor
|
|
533
|
+
input :order_id do
|
|
534
|
+
required(:order_id).filled(:integer, gt?: 0)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Custom Validators
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
class OrderReactor < RubyReactor::Reactor
|
|
543
|
+
input :order do
|
|
544
|
+
required(:order).hash do
|
|
545
|
+
required(:id).filled(:integer, gt?: 0)
|
|
546
|
+
required(:total).filled(:decimal, gt?: 0)
|
|
547
|
+
required(:items).filled(:array, min_size?: 1)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Dependency Graph
|
|
554
|
+
|
|
555
|
+
RubyReactor builds a dependency graph to determine execution order.
|
|
556
|
+
|
|
557
|
+
### Graph Construction
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
# Explicit dependencies
|
|
561
|
+
step :a do; end
|
|
562
|
+
step :b do; argument :a_result, result(:a); end
|
|
563
|
+
step :c do; argument :a_result, result(:a); end
|
|
564
|
+
step :d do; argument :b_result, result(:b); argument :c_result, result(:c); end
|
|
565
|
+
|
|
566
|
+
# Execution order: a → [b,c] → d
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Cycle Detection
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# This would raise DependencyError
|
|
573
|
+
step :a do; argument :b_result, result(:b); end
|
|
574
|
+
step :b do; argument :a_result, result(:a); end # Circular dependency!
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## Execution Flow
|
|
578
|
+
|
|
579
|
+
### Normal Execution
|
|
580
|
+
|
|
581
|
+
```mermaid
|
|
582
|
+
graph TD
|
|
583
|
+
A[Reactor.run] --> B[Validate Inputs]
|
|
584
|
+
B --> C[Build Dependency Graph]
|
|
585
|
+
C --> D[Execute Steps in Order]
|
|
586
|
+
D --> E{All Steps<br/>Complete?}
|
|
587
|
+
E -->|No| F[Execute Next Step]
|
|
588
|
+
F --> G{Step<br/>Success?}
|
|
589
|
+
G -->|Yes| H[Store Result]
|
|
590
|
+
H --> E
|
|
591
|
+
G -->|No| I[Run Compensation]
|
|
592
|
+
I --> J[Return Failure Result]
|
|
593
|
+
E -->|Yes| K[Aggregate Results]
|
|
594
|
+
K --> L[Return Success Result]
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
1. **Input Validation**: Validate reactor inputs
|
|
598
|
+
2. **Graph Building**: Construct dependency graph
|
|
599
|
+
3. **Step Execution**: Execute steps in dependency order
|
|
600
|
+
4. **Result Aggregation**: Collect all step outputs
|
|
601
|
+
5. **Return Result**: Return comprehensive result object
|
|
602
|
+
|
|
603
|
+
### Error Execution
|
|
604
|
+
|
|
605
|
+
```mermaid
|
|
606
|
+
graph TD
|
|
607
|
+
A[Step Execution] --> B{Step<br/>Fails?}
|
|
608
|
+
B -->|No| C[Continue to Next Step]
|
|
609
|
+
B -->|Yes| D[Stop Execution]
|
|
610
|
+
D --> E[Run Compensation<br/>for Failing Step]
|
|
611
|
+
E --> F[Run Undo for<br/>Successful Steps<br/>in Reverse Order]
|
|
612
|
+
F --> G[Aggregate Error Details]
|
|
613
|
+
G --> H[Return Failure Result]
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
1. **Step Failure**: A step raises an exception
|
|
617
|
+
2. **Stop Execution**: Halt remaining steps
|
|
618
|
+
3. **Compensation**: Run compensation block for the failing step
|
|
619
|
+
4. **Undo**: Run undo blocks for all previously successful steps in reverse order
|
|
620
|
+
5. **Rollback**: Return failure result with error details
|
|
621
|
+
|
|
622
|
+
## Threading Model
|
|
623
|
+
|
|
624
|
+
### Synchronous
|
|
625
|
+
- Single-threaded execution
|
|
626
|
+
- Blocking operations halt the entire process
|
|
627
|
+
- Simple debugging and monitoring
|
|
628
|
+
|
|
629
|
+
### Asynchronous
|
|
630
|
+
- Multi-threaded execution via Sidekiq
|
|
631
|
+
- Non-blocking retry mechanisms
|
|
632
|
+
- Complex monitoring and debugging
|
|
633
|
+
|
|
634
|
+
## Best Practices
|
|
635
|
+
|
|
636
|
+
### Step Design
|
|
637
|
+
|
|
638
|
+
1. **Single Responsibility**: Each step should do one thing well
|
|
639
|
+
2. **Idempotency**: Design steps to be safely retryable when possible
|
|
640
|
+
3. **Error Handling**: Use appropriate exception types
|
|
641
|
+
4. **Resource Management**: Clean up resources in compensation blocks
|
|
642
|
+
|
|
643
|
+
### Dependency Management
|
|
644
|
+
|
|
645
|
+
1. **Minimize Dependencies**: Keep the dependency graph simple
|
|
646
|
+
2. **Clear Naming**: Use descriptive step names
|
|
647
|
+
3. **Logical Grouping**: Group related steps together
|
|
648
|
+
|
|
649
|
+
### Error Handling
|
|
650
|
+
|
|
651
|
+
1. **Specific Exceptions**: Use custom exception classes
|
|
652
|
+
2. **Compensation Logic**: Always provide compensation for failing steps
|
|
653
|
+
3. **Undo Logic**: Always provide undo for steps that modify external state
|
|
654
|
+
4. **Logging**: Log important events and errors
|
|
655
|
+
5. **Monitoring**: Track success/failure rates
|
|
656
|
+
|
|
657
|
+
### Performance
|
|
658
|
+
|
|
659
|
+
1. **Efficient Steps**: Keep individual steps fast
|
|
660
|
+
2. **Async for Slow Ops**: Use async for I/O bound operations
|
|
661
|
+
3. **Resource Limits**: Set appropriate timeouts and limits
|
|
662
|
+
4. **Caching**: Cache expensive operations when safe
|