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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +98 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/README.md +570 -0
  6. data/Rakefile +12 -0
  7. data/documentation/DAG.md +457 -0
  8. data/documentation/README.md +123 -0
  9. data/documentation/async_reactors.md +369 -0
  10. data/documentation/composition.md +199 -0
  11. data/documentation/core_concepts.md +662 -0
  12. data/documentation/data_pipelines.md +224 -0
  13. data/documentation/examples/inventory_management.md +749 -0
  14. data/documentation/examples/order_processing.md +365 -0
  15. data/documentation/examples/payment_processing.md +654 -0
  16. data/documentation/getting_started.md +224 -0
  17. data/documentation/retry_configuration.md +357 -0
  18. data/lib/ruby_reactor/async_router.rb +91 -0
  19. data/lib/ruby_reactor/configuration.rb +41 -0
  20. data/lib/ruby_reactor/context.rb +169 -0
  21. data/lib/ruby_reactor/context_serializer.rb +164 -0
  22. data/lib/ruby_reactor/dependency_graph.rb +126 -0
  23. data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
  24. data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
  25. data/lib/ruby_reactor/dsl/reactor.rb +151 -0
  26. data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
  27. data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
  28. data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
  29. data/lib/ruby_reactor/error/base.rb +16 -0
  30. data/lib/ruby_reactor/error/compensation_error.rb +8 -0
  31. data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
  32. data/lib/ruby_reactor/error/dependency_error.rb +8 -0
  33. data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
  34. data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
  35. data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
  36. data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
  37. data/lib/ruby_reactor/error/undo_error.rb +8 -0
  38. data/lib/ruby_reactor/error/validation_error.rb +8 -0
  39. data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
  40. data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
  41. data/lib/ruby_reactor/executor/input_validator.rb +39 -0
  42. data/lib/ruby_reactor/executor/result_handler.rb +103 -0
  43. data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
  44. data/lib/ruby_reactor/executor/step_executor.rb +319 -0
  45. data/lib/ruby_reactor/executor.rb +123 -0
  46. data/lib/ruby_reactor/map/collector.rb +65 -0
  47. data/lib/ruby_reactor/map/element_executor.rb +154 -0
  48. data/lib/ruby_reactor/map/execution.rb +60 -0
  49. data/lib/ruby_reactor/map/helpers.rb +67 -0
  50. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
  51. data/lib/ruby_reactor/reactor.rb +75 -0
  52. data/lib/ruby_reactor/retry_context.rb +92 -0
  53. data/lib/ruby_reactor/retry_queued_result.rb +26 -0
  54. data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
  55. data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
  56. data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
  57. data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
  58. data/lib/ruby_reactor/step/compose_step.rb +107 -0
  59. data/lib/ruby_reactor/step/map_step.rb +234 -0
  60. data/lib/ruby_reactor/step.rb +33 -0
  61. data/lib/ruby_reactor/storage/adapter.rb +51 -0
  62. data/lib/ruby_reactor/storage/configuration.rb +15 -0
  63. data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
  64. data/lib/ruby_reactor/template/base.rb +15 -0
  65. data/lib/ruby_reactor/template/element.rb +25 -0
  66. data/lib/ruby_reactor/template/input.rb +48 -0
  67. data/lib/ruby_reactor/template/result.rb +48 -0
  68. data/lib/ruby_reactor/template/value.rb +22 -0
  69. data/lib/ruby_reactor/validation/base.rb +26 -0
  70. data/lib/ruby_reactor/validation/input_validator.rb +62 -0
  71. data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
  72. data/lib/ruby_reactor/version.rb +5 -0
  73. data/lib/ruby_reactor.rb +159 -0
  74. data/sig/ruby_reactor.rbs +4 -0
  75. 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