ruby_reactor 0.3.2 → 0.4.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-config.json +15 -0
  3. data/.release-please-manifest.json +3 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/README.md +80 -4
  7. data/lib/ruby_reactor/context_serializer.rb +10 -1
  8. data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
  9. data/lib/ruby_reactor/rate_limit.rb +2 -2
  10. data/lib/ruby_reactor/sidekiq_workers/worker.rb +58 -1
  11. data/lib/ruby_reactor/version.rb +1 -1
  12. metadata +7 -52
  13. data/documentation/DAG.md +0 -457
  14. data/documentation/README.md +0 -135
  15. data/documentation/async_reactors.md +0 -381
  16. data/documentation/composition.md +0 -199
  17. data/documentation/core_concepts.md +0 -676
  18. data/documentation/data_pipelines.md +0 -230
  19. data/documentation/examples/inventory_management.md +0 -748
  20. data/documentation/examples/order_processing.md +0 -380
  21. data/documentation/examples/payment_processing.md +0 -565
  22. data/documentation/getting_started.md +0 -242
  23. data/documentation/images/failed_order_processing.png +0 -0
  24. data/documentation/images/payment_workflow.png +0 -0
  25. data/documentation/interrupts.md +0 -163
  26. data/documentation/locks_and_semaphores.md +0 -459
  27. data/documentation/retry_configuration.md +0 -362
  28. data/documentation/testing.md +0 -994
  29. data/gui/.gitignore +0 -24
  30. data/gui/README.md +0 -73
  31. data/gui/eslint.config.js +0 -23
  32. data/gui/index.html +0 -13
  33. data/gui/package-lock.json +0 -5925
  34. data/gui/package.json +0 -46
  35. data/gui/postcss.config.js +0 -6
  36. data/gui/public/vite.svg +0 -1
  37. data/gui/src/App.css +0 -42
  38. data/gui/src/App.tsx +0 -51
  39. data/gui/src/assets/react.svg +0 -1
  40. data/gui/src/components/DagVisualizer.tsx +0 -424
  41. data/gui/src/components/Dashboard.tsx +0 -163
  42. data/gui/src/components/ErrorBoundary.tsx +0 -47
  43. data/gui/src/components/ReactorDetail.tsx +0 -135
  44. data/gui/src/components/StepInspector.tsx +0 -492
  45. data/gui/src/components/__tests__/DagVisualizer.test.tsx +0 -140
  46. data/gui/src/components/__tests__/ReactorDetail.test.tsx +0 -111
  47. data/gui/src/components/__tests__/StepInspector.test.tsx +0 -408
  48. data/gui/src/globals.d.ts +0 -7
  49. data/gui/src/index.css +0 -14
  50. data/gui/src/lib/utils.ts +0 -13
  51. data/gui/src/main.tsx +0 -14
  52. data/gui/src/test/setup.ts +0 -11
  53. data/gui/tailwind.config.js +0 -11
  54. data/gui/tsconfig.app.json +0 -28
  55. data/gui/tsconfig.json +0 -7
  56. data/gui/tsconfig.node.json +0 -26
  57. data/gui/vite.config.ts +0 -8
  58. data/gui/vitest.config.ts +0 -13
@@ -1,676 +0,0 @@
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
- `run` blocks always receive two positional arguments: the resolved arguments hash and the execution context. Declare inputs with `argument :name, source`:
28
-
29
- ```ruby
30
- step :validate_order do
31
- argument :order_id, input(:order_id)
32
-
33
- run do |args, _context|
34
- order = Order.find(args[:order_id])
35
- return Failure("Order not found") unless order
36
- Success({ order: order })
37
- end
38
- end
39
- ```
40
-
41
- ### Step Classes
42
-
43
- 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.
44
-
45
- ```ruby
46
- class ReserveInventoryStep
47
- include RubyReactor::Step
48
-
49
- def self.run(arguments, context)
50
- order = arguments[:order]
51
- # Business logic for inventory reservation
52
- reservation_id = InventoryService.reserve(order[:items])
53
- Success({
54
- reservation_id: reservation_id,
55
- reserved_items: order[:items].size
56
- })
57
- end
58
-
59
- def self.compensate(error, arguments, context)
60
- # Cleanup logic for failed reservations
61
- puts "Cleaning up inventory reservation due to: #{error.message}"
62
- # Release any partial reservations
63
- Success("Inventory reservation cleaned up")
64
- end
65
-
66
- def self.undo(result, arguments, context)
67
- # Rollback logic for successful reservations during reactor failure
68
- reservation_id = result[:reservation_id]
69
- InventoryService.release(reservation_id)
70
- Success("Inventory reservation released")
71
- end
72
- end
73
- ```
74
-
75
- To use a step class in a reactor, reference it by class:
76
-
77
- ```ruby
78
- class OrderProcessingReactor < RubyReactor::Reactor
79
- step :reserve_inventory, ReserveInventoryStep do
80
- argument :order, result(:validate_order)
81
- end
82
- end
83
- ```
84
-
85
- **Benefits of Step Classes:**
86
- - **Reusability**: Step classes can be shared across multiple reactors
87
- - **Testability**: Easier to unit test individual step logic in isolation
88
- - **Organization**: Complex business logic is better organized in dedicated classes
89
- - **Maintainability**: Compensation and undo logic is clearly separated
90
- - **Readability**: Reactor definitions remain focused on orchestration
91
-
92
- **Step Class Methods:**
93
- - **`run(arguments, context)`**: The main business logic. Returns `Success(result)` or `Failure(error)`
94
- - **`compensate(error, arguments, context)`**: Cleanup for the current failing step. Called when the step fails
95
- - **`undo(result, arguments, context)`**: Rollback for previously successful steps. Called during reactor failure rollback
96
-
97
- **Step Components:**
98
- - **Name**: Unique identifier (symbol)
99
- - **Implementation**: The `run` block containing business logic
100
- - **Dependencies**: Other steps that must complete first
101
- - **Compensation**: Undo logic for rollback scenarios
102
-
103
- ## Context
104
-
105
- Context holds the execution state throughout the reactor lifecycle.
106
-
107
- ```ruby
108
- context = RubyReactor::Context.new(order_id: 123, customer_id: 456)
109
- ```
110
-
111
- **Context Contents:**
112
- - **Inputs**: Original parameters passed to `Reactor.run()`
113
- - **Intermediate Results**: Outputs from completed steps
114
- - **Completed Steps**: Set of successfully finished step names
115
- - **Step Results**: Final outputs from each step
116
- - **Execution Metadata**: Job IDs, timestamps, reactor class info
117
-
118
- ## Dependencies
119
-
120
- Steps can depend on other steps, creating a directed acyclic graph (DAG) of execution.
121
-
122
- ```ruby
123
- step :validate_order do
124
- run { validate_order_logic }
125
- end
126
-
127
- step :process_payment do
128
- argument :order, result(:validate_order)
129
- run do |args, _context|
130
- process_payment_for_order(args[:order])
131
- end
132
- end
133
-
134
- step :send_confirmation do
135
- argument :payment_result, result(:process_payment)
136
- run do |args, _context|
137
- payment_result = args[:payment_result]
138
- send_confirmation_email(payment_result[:order], payment_result[:payment_id])
139
- end
140
- end
141
- ```
142
-
143
- **Dependency Resolution:**
144
- - Topological sorting ensures correct execution order
145
- - Future feature: Parallel execution of independent steps (when available)
146
- - Validation prevents circular dependencies
147
-
148
- ## Results
149
-
150
- `Reactor.run` returns one of four result types:
151
-
152
- - **`RubyReactor::Success`** — `success?` is `true`. `value` holds the output of the step named in `returns`, or the full `intermediate_results` hash if no `returns` is declared.
153
- - **`RubyReactor::Failure`** — `failure?` is `true`. Readers include `error`, `step_name`, `reactor_name`, `step_arguments`, `inputs`, `exception_class`, `file_path`, `line_number`, `backtrace`, `validation_errors`, and `retryable?`.
154
- - **`RubyReactor::AsyncResult`** — returned by an async reactor or when a step hands off to a worker. Readers: `job_id`, `execution_id`, `intermediate_results`.
155
- - **`RubyReactor::InterruptResult`** — returned when an `interrupt` step pauses execution. Readers: `execution_id`, `correlation_id`, `status` (`:paused`), `timeout_at`, `intermediate_results`.
156
-
157
- Step-by-step state lives on the context, not the result object. Reload via `Reactor.find(execution_id)` to inspect:
158
-
159
- ```ruby
160
- reactor = OrderProcessingReactor.find(execution_id)
161
- reactor.context.intermediate_results # => { validate_order: {...}, ... }
162
- reactor.context.status # => "completed" | "failed" | "paused" | "running"
163
- reactor.execution_trace # ordered list of run/undo/compensate entries
164
- reactor.result # reconstructed Success/Failure/InterruptResult
165
- ```
166
-
167
- ## Error Handling
168
-
169
- RubyReactor provides sophisticated error handling with automatic compensation.
170
-
171
- ### Step Failures
172
-
173
- When a step fails, execution stops and compensation begins:
174
-
175
- ```ruby
176
- step :process_payment do
177
- argument :amount, input(:amount)
178
- argument :token, input(:card_token)
179
-
180
- run do |args, _ctx|
181
- # This might fail
182
- PaymentService.charge(args[:amount], args[:token])
183
- end
184
-
185
- compensate do |error, args, _ctx|
186
- # Compensation receives (error, arguments, context)
187
- # Best-effort cleanup specific to this step's failure
188
- AuditService.log_payment_failure(args[:token], error.message)
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
- ```ruby
353
- async_result = Reactor.run(inputs)
354
- # Returns immediately
355
- async_result.execution_id # UUID to look up state later
356
-
357
- # Reload to inspect status / final result
358
- reactor = Reactor.find(async_result.execution_id)
359
- case reactor.context.status.to_s
360
- when "completed" then reactor.result.value
361
- when "failed" then reactor.result.error
362
- when "paused" then reactor.result.correlation_id
363
- when "running" then :still_running
364
- end
365
- ```
366
-
367
- **Characteristics:**
368
-
369
- - Non-blocking execution
370
- - Background processing with Sidekiq
371
- - Retry capabilities
372
- - Better scalability
373
-
374
- ## Step Arguments
375
-
376
- `run` blocks always receive two positional arguments: the resolved arguments hash and the context. Declare each argument explicitly with `argument :name, source` — there is no implicit keyword injection.
377
-
378
- Sources you can use:
379
-
380
- - `input(:name)` — value from the reactor's inputs (the hash passed to `Reactor.run`).
381
- - `input(:name, :path)` — nested path access into a hash input.
382
- - `result(:step_name)` — full output of a previous step.
383
- - `result(:step_name, :path)` — nested path into a previous step's output.
384
- - `value(literal)` — a constant value.
385
-
386
- ```ruby
387
- step :validate_order do
388
- argument :order_id, input(:order_id)
389
- argument :customer_id, input(:customer_id)
390
-
391
- run do |args, _context|
392
- order = Order.find_by(id: args[:order_id], customer_id: args[:customer_id])
393
- Success({ order: order })
394
- end
395
- end
396
-
397
- step :process_payment do
398
- argument :order, result(:validate_order, :order)
399
-
400
- run do |args, _context|
401
- payment = PaymentService.charge(args[:order].total, args[:order].card_token)
402
- Success({ payment_id: payment.id })
403
- end
404
- end
405
- ```
406
-
407
- If a step declares no `argument`s, the reactor's raw inputs hash is passed as `args`.
408
-
409
- ## Undo
410
-
411
- Undo provides transactional rollback for previously successful steps when a later step fails.
412
-
413
- ### When Undo Runs
414
-
415
- 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:
416
-
417
- 1. **Compensation** runs for the failing step itself
418
- 2. **Undo** runs for all previously successful steps in reverse order
419
-
420
- ### Basic Undo
421
-
422
- ```ruby
423
- step :reserve_inventory do
424
- argument :items, input(:items)
425
-
426
- run do |args, _ctx|
427
- reservation_id = InventoryService.reserve(args[:items])
428
- Success({ reservation_id: reservation_id })
429
- end
430
-
431
- undo do |result, arguments, context|
432
- # Undo receives the step's result, arguments, and full context
433
- InventoryService.release(result[:reservation_id])
434
- Success("Inventory reservation released")
435
- end
436
- end
437
- ```
438
-
439
- ### Undo Context
440
-
441
- Undo blocks receive three parameters:
442
- - **Result**: The successful result from the step's `run` block
443
- - **Arguments**: The resolved arguments passed to the step
444
- - **Context**: The full execution context with all intermediate results
445
-
446
- ```ruby
447
- step :complex_operation do
448
- argument :input, input(:payload)
449
-
450
- run do |args, _ctx|
451
- # Complex operation that modifies external state
452
- record = create_record(args[:input])
453
- notification = send_notification(record)
454
- Success({ record_id: record.id, notification_id: notification.id })
455
- end
456
-
457
- undo do |result, arguments, context|
458
- # Clean up in reverse order of creation
459
- notification_id = result[:notification_id]
460
- record_id = result[:record_id]
461
-
462
- delete_notification(notification_id) if notification_id
463
- delete_record(record_id) if record_id
464
-
465
- Success("Complex operation fully undone")
466
- end
467
- end
468
- ```
469
-
470
- ### Undo vs Compensation
471
-
472
- - **Compensation**: Handles cleanup for the currently failing step
473
- - **Undo**: Handles rollback of all previously successful steps during reactor failure
474
-
475
- Both mechanisms work together to ensure transactional semantics across complex business processes.
476
-
477
- ## Compensation
478
-
479
- 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.
480
-
481
- ### When Compensation Runs
482
-
483
- 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.
484
-
485
- ### Basic Compensation
486
-
487
- ```ruby
488
- step :reserve_inventory do
489
- argument :items, input(:items)
490
-
491
- run do |args, _ctx|
492
- reservation_id = InventoryService.reserve(args[:items])
493
- Success({ reservation_id: reservation_id })
494
- end
495
-
496
- compensate do |error, arguments, context|
497
- # Clean up partial reservations if the step failed
498
- # Note: This step didn't succeed, so we don't have a result to undo
499
- # Instead, we work with the error and arguments
500
- puts "Cleaning up after reservation failure: #{error.message}"
501
- # Any cleanup logic specific to this step's failure
502
- end
503
- end
504
- ```
505
-
506
- ### Compensation Context
507
-
508
- Compensation blocks receive three parameters:
509
- - **Error**: The exception that caused the step to fail
510
- - **Arguments**: The resolved arguments that were passed to the step
511
- - **Context**: The full execution context
512
-
513
- ```ruby
514
- step :process_payment do
515
- argument :order, result(:validate_order)
516
- argument :payment_method, input(:payment_method)
517
-
518
- run do |args, _ctx|
519
- # Payment processing logic that might fail
520
- PaymentService.charge(args[:order].total, args[:payment_method])
521
- end
522
-
523
- compensate do |error, arguments, context|
524
- # Handle payment processing failure
525
- order = arguments[:order]
526
- payment_method = arguments[:payment_method]
527
-
528
- # Log the failure for audit purposes
529
- AuditService.log_payment_failure(order.id, error.message)
530
-
531
- # Send notification about payment failure
532
- NotificationService.send_payment_failed_email(order.customer_email, order.id)
533
- end
534
- end
535
- ```
536
-
537
-
538
-
539
- ## Validation
540
-
541
- Input validation ensures data integrity before execution.
542
-
543
- ### Built-in Validation
544
-
545
- ```ruby
546
- class OrderReactor < RubyReactor::Reactor
547
- input :order_id do
548
- required(:order_id).filled(:integer, gt?: 0)
549
- end
550
- end
551
- ```
552
-
553
- ### Custom Validators
554
-
555
- ```ruby
556
- class OrderReactor < RubyReactor::Reactor
557
- input :order do
558
- required(:order).hash do
559
- required(:id).filled(:integer, gt?: 0)
560
- required(:total).filled(:decimal, gt?: 0)
561
- required(:items).filled(:array, min_size?: 1)
562
- end
563
- end
564
- end
565
- ```
566
-
567
- ## Dependency Graph
568
-
569
- RubyReactor builds a dependency graph to determine execution order.
570
-
571
- ### Graph Construction
572
-
573
- ```ruby
574
- # Explicit dependencies
575
- step :a do; end
576
- step :b do; argument :a_result, result(:a); end
577
- step :c do; argument :a_result, result(:a); end
578
- step :d do; argument :b_result, result(:b); argument :c_result, result(:c); end
579
-
580
- # Execution order: a → [b,c] → d
581
- ```
582
-
583
- ### Cycle Detection
584
-
585
- ```ruby
586
- # This would raise DependencyError
587
- step :a do; argument :b_result, result(:b); end
588
- step :b do; argument :a_result, result(:a); end # Circular dependency!
589
- ```
590
-
591
- ## Execution Flow
592
-
593
- ### Normal Execution
594
-
595
- ```mermaid
596
- graph TD
597
- A[Reactor.run] --> B[Validate Inputs]
598
- B --> C[Build Dependency Graph]
599
- C --> D[Execute Steps in Order]
600
- D --> E{All Steps<br/>Complete?}
601
- E -->|No| F[Execute Next Step]
602
- F --> G{Step<br/>Success?}
603
- G -->|Yes| H[Store Result]
604
- H --> E
605
- G -->|No| I[Run Compensation]
606
- I --> J[Return Failure Result]
607
- E -->|Yes| K[Aggregate Results]
608
- K --> L[Return Success Result]
609
- ```
610
-
611
- 1. **Input Validation**: Validate reactor inputs
612
- 2. **Graph Building**: Construct dependency graph
613
- 3. **Step Execution**: Execute steps in dependency order
614
- 4. **Result Aggregation**: Collect all step outputs
615
- 5. **Return Result**: Return comprehensive result object
616
-
617
- ### Error Execution
618
-
619
- ```mermaid
620
- graph TD
621
- A[Step Execution] --> B{Step<br/>Fails?}
622
- B -->|No| C[Continue to Next Step]
623
- B -->|Yes| D[Stop Execution]
624
- D --> E[Run Compensation<br/>for Failing Step]
625
- E --> F[Run Undo for<br/>Successful Steps<br/>in Reverse Order]
626
- F --> G[Aggregate Error Details]
627
- G --> H[Return Failure Result]
628
- ```
629
-
630
- 1. **Step Failure**: A step raises an exception
631
- 2. **Stop Execution**: Halt remaining steps
632
- 3. **Compensation**: Run compensation block for the failing step
633
- 4. **Undo**: Run undo blocks for all previously successful steps in reverse order
634
- 5. **Rollback**: Return failure result with error details
635
-
636
- ## Threading Model
637
-
638
- ### Synchronous
639
- - Single-threaded execution
640
- - Blocking operations halt the entire process
641
- - Simple debugging and monitoring
642
-
643
- ### Asynchronous
644
- - Multi-threaded execution via Sidekiq
645
- - Non-blocking retry mechanisms
646
- - Complex monitoring and debugging
647
-
648
- ## Best Practices
649
-
650
- ### Step Design
651
-
652
- 1. **Single Responsibility**: Each step should do one thing well
653
- 2. **Idempotency**: Design steps to be safely retryable when possible
654
- 3. **Error Handling**: Use appropriate exception types
655
- 4. **Resource Management**: Clean up resources in compensation blocks
656
-
657
- ### Dependency Management
658
-
659
- 1. **Minimize Dependencies**: Keep the dependency graph simple
660
- 2. **Clear Naming**: Use descriptive step names
661
- 3. **Logical Grouping**: Group related steps together
662
-
663
- ### Error Handling
664
-
665
- 1. **Specific Exceptions**: Use custom exception classes
666
- 2. **Compensation Logic**: Always provide compensation for failing steps
667
- 3. **Undo Logic**: Always provide undo for steps that modify external state
668
- 4. **Logging**: Log important events and errors
669
- 5. **Monitoring**: Track success/failure rates
670
-
671
- ### Performance
672
-
673
- 1. **Efficient Steps**: Keep individual steps fast
674
- 2. **Async for Slow Ops**: Use async for I/O bound operations
675
- 3. **Resource Limits**: Set appropriate timeouts and limits
676
- 4. **Caching**: Cache expensive operations when safe