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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -9
  3. data/documentation/README.md +20 -8
  4. data/documentation/async_reactors.md +46 -34
  5. data/documentation/core_concepts.md +75 -61
  6. data/documentation/examples/inventory_management.md +2 -3
  7. data/documentation/examples/order_processing.md +92 -77
  8. data/documentation/examples/payment_processing.md +28 -117
  9. data/documentation/getting_started.md +112 -94
  10. data/documentation/interrupts.md +9 -7
  11. data/documentation/locks_and_semaphores.md +459 -0
  12. data/documentation/retry_configuration.md +19 -14
  13. data/documentation/testing.md +994 -0
  14. data/lib/ruby_reactor/configuration.rb +19 -2
  15. data/lib/ruby_reactor/context.rb +13 -5
  16. data/lib/ruby_reactor/context_serializer.rb +55 -4
  17. data/lib/ruby_reactor/dsl/lockable.rb +130 -0
  18. data/lib/ruby_reactor/dsl/reactor.rb +3 -2
  19. data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
  20. data/lib/ruby_reactor/executor/result_handler.rb +27 -2
  21. data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
  22. data/lib/ruby_reactor/executor/step_executor.rb +29 -99
  23. data/lib/ruby_reactor/executor.rb +148 -15
  24. data/lib/ruby_reactor/lock.rb +92 -0
  25. data/lib/ruby_reactor/map/collector.rb +16 -15
  26. data/lib/ruby_reactor/map/element_executor.rb +90 -104
  27. data/lib/ruby_reactor/map/execution.rb +2 -1
  28. data/lib/ruby_reactor/map/helpers.rb +2 -1
  29. data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
  30. data/lib/ruby_reactor/period.rb +67 -0
  31. data/lib/ruby_reactor/rate_limit.rb +74 -0
  32. data/lib/ruby_reactor/reactor.rb +175 -16
  33. data/lib/ruby_reactor/rspec/helpers.rb +17 -0
  34. data/lib/ruby_reactor/rspec/matchers.rb +423 -0
  35. data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
  36. data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
  37. data/lib/ruby_reactor/rspec.rb +18 -0
  38. data/lib/ruby_reactor/semaphore.rb +58 -0
  39. data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
  40. data/lib/ruby_reactor/sidekiq_workers/worker.rb +69 -9
  41. data/lib/ruby_reactor/step/compose_step.rb +0 -1
  42. data/lib/ruby_reactor/step/map_step.rb +11 -18
  43. data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
  44. data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
  45. data/lib/ruby_reactor/version.rb +1 -1
  46. data/lib/ruby_reactor/web/api.rb +32 -24
  47. data/lib/ruby_reactor.rb +119 -10
  48. metadata +16 -3
@@ -46,15 +46,15 @@ class PaymentProcessingReactor < RubyReactor::Reactor
46
46
  # Payment processing needs careful retry configuration
47
47
  retry_defaults max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
48
48
 
49
- input :amount, validate: -> do
49
+ input :amount do
50
50
  required(:amount).filled(:decimal, gt?: 0)
51
51
  end
52
52
 
53
- input :currency, validate: -> do
53
+ input :currency do
54
54
  required(:currency).filled(:string, included_in?: ['USD', 'EUR', 'GBP'])
55
55
  end
56
56
 
57
- input :card_token, validate: -> do
57
+ input :card_token do
58
58
  required(:card_token).filled(:string, format?: /^tok_/)
59
59
  end
60
60
 
@@ -119,10 +119,10 @@ class PaymentProcessingReactor < RubyReactor::Reactor
119
119
  Success({ auth_id: auth_result.id, auth_amount: amount })
120
120
  end
121
121
 
122
- undo do |args, _context|
123
- auth_id = args[:fraud_data][:auth_id] || args[:auth_id]
124
- # Void the pre-authorization
125
- PaymentGateway.void_authorization(auth_id) if auth_id
122
+ undo do |result, _args, _ctx|
123
+ # Void the pre-authorization if a later step fails
124
+ PaymentGateway.void_authorization(result[:auth_id]) if result[:auth_id]
125
+ Success()
126
126
  end
127
127
  end
128
128
 
@@ -148,10 +148,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
148
148
  Success({ charge_id: charge_result.id, charged_amount: amount })
149
149
  end
150
150
 
151
- undo do |args, _context|
152
- charge_id = args[:auth_data][:charge_id] || args[:charge_id]
153
- # Refund the charge
154
- PaymentGateway.refund(charge_id) if charge_id
151
+ undo do |result, _args, _ctx|
152
+ PaymentGateway.refund(result[:charge_id]) if result[:charge_id]
153
+ Success()
155
154
  end
156
155
  end
157
156
 
@@ -182,10 +181,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
182
181
  { transaction_id: transaction.id }
183
182
  end
184
183
 
185
- compensate do |args, _context|
186
- transaction_id = args[:charge_data][:transaction_id] || args[:transaction_id]
187
- # Mark transaction as failed/refunded
188
- PaymentTransaction.find_by(id: transaction_id)&.update!(status: :refunded)
184
+ undo do |result, _args, _ctx|
185
+ PaymentTransaction.find_by(id: result[:transaction_id])&.update!(status: :refunded)
186
+ Success()
189
187
  end
190
188
  end
191
189
 
@@ -301,7 +299,7 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
301
299
 
302
300
  retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
303
301
 
304
- input :subscription_id, validate: -> do
302
+ input :subscription_id do
305
303
  required(:subscription_id).filled(:string)
306
304
  end
307
305
 
@@ -351,97 +349,9 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
351
349
  { charge_id: charge_result.id }
352
350
  end
353
351
 
354
- compensate do |args, _context|
355
- charge_id = args[:validation_data][:charge_id] || args[:charge_id]
356
- # Refund the subscription charge
357
- PaymentGateway.refund_subscription_charge(charge_id) if charge_id
358
- end
359
- end
360
-
361
- step :update_billing_record do
362
- argument :validation_data, result(:validate_subscription)
363
- argument :proration_data, result(:calculate_proration)
364
- argument :charge_data, result(:charge_subscription)
365
-
366
- run do |args, _context|
367
- subscription = args[:validation_data][:subscription]
368
- charge_id = args[:charge_data][:charge_id]
369
- billing_period = args[:proration_data][:billing_period]
370
-
371
- BillingRecord.create!(
372
- subscription: subscription,
373
- charge_id: charge_id,
374
- billing_period: billing_period,
375
- status: :paid
376
- )
377
-
378
- { billing_recorded: true }
379
- end
380
- end
381
- ```
382
-
383
- ### Subscription Payment Processing
384
-
385
- ```ruby
386
- class SubscriptionPaymentReactor < RubyReactor::Reactor
387
- async true
388
-
389
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
390
-
391
- input :subscription_id, validate: -> do
392
- required(:subscription_id).filled(:string)
393
- end
394
-
395
- step :validate_subscription do
396
- validate_args do
397
- required(:subscription_id).filled(:string)
398
- end
399
-
400
- run do |subscription_id:|
401
- subscription = Subscription.find(subscription_id)
402
- raise "Subscription not found" unless subscription
403
- raise "Subscription inactive" unless subscription.active?
404
-
405
- { subscription: subscription }
406
- end
407
- end
408
-
409
- step :calculate_proration do
410
- argument :validation_data, result(:validate_subscription)
411
-
412
- run do |args, _context|
413
- subscription = args[:validation_data][:subscription]
414
-
415
- # Calculate prorated amount for billing period
416
- proration = BillingService.calculate_proration(subscription)
417
- { proration_amount: proration.amount, billing_period: proration.period }
418
- end
419
- end
420
-
421
- step :charge_subscription do
422
- argument :validation_data, result(:validate_subscription)
423
- argument :proration_data, result(:calculate_proration)
424
-
425
- run do |args, _context|
426
- subscription = args[:validation_data][:subscription]
427
- proration_amount = args[:proration_data][:proration_amount]
428
- billing_period = args[:proration_data][:billing_period]
429
-
430
- charge_result = PaymentGateway.charge_subscription(
431
- customer_id: subscription.customer.stripe_id,
432
- amount: proration_amount,
433
- description: "Subscription #{subscription.id} - #{billing_period}"
434
- )
435
-
436
- raise "Subscription charge failed" unless charge_result.success?
437
-
438
- { charge_id: charge_result.id }
439
- end
440
-
441
- compensate do |args, _context|
442
- charge_id = args[:validation_data][:charge_id] || args[:charge_id]
443
- # Refund the subscription charge
444
- PaymentGateway.refund_subscription_charge(charge_id) if charge_id
352
+ undo do |result, _args, _ctx|
353
+ PaymentGateway.refund_subscription_charge(result[:charge_id]) if result[:charge_id]
354
+ Success()
445
355
  end
446
356
  end
447
357
 
@@ -541,10 +451,10 @@ RSpec.describe PaymentProcessingReactor do
541
451
  allow(PaymentGateway).to receive(:charge).and_return(successful_charge)
542
452
  allow(EmailService).to receive(:send_payment_receipt).and_return(successful_email)
543
453
 
544
- result = PaymentProcessingReactor.run(valid_payment_params)
454
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
545
455
 
546
- expect(result).to be_success
547
- expect(result.step_results[:charge_payment][:charge_id]).to be_present
456
+ expect(subject).to be_success
457
+ expect(subject.step_result(:charge_payment)[:charge_id]).to be_present
548
458
  end
549
459
  end
550
460
 
@@ -552,10 +462,10 @@ RSpec.describe PaymentProcessingReactor do
552
462
  it "blocks high-risk payments" do
553
463
  allow(FraudDetectionService).to receive(:analyze).and_return(0.9)
554
464
 
555
- result = PaymentProcessingReactor.run(valid_payment_params)
465
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
556
466
 
557
- expect(result).to be_failure
558
- expect(result.error.message).to include("High fraud risk")
467
+ expect(subject).to be_failure
468
+ expect(subject.error).to include("High fraud risk")
559
469
  end
560
470
  end
561
471
 
@@ -569,9 +479,10 @@ RSpec.describe PaymentProcessingReactor do
569
479
 
570
480
  expect(PaymentGateway).to receive(:pre_authorize).exactly(3).times
571
481
 
572
- result = PaymentProcessingReactor.run(valid_payment_params)
482
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
573
483
 
574
- expect(result).to be_success
484
+ expect(subject).to be_success
485
+ expect(subject).to have_retried_step(:pre_authorize).times(2)
575
486
  end
576
487
  end
577
488
 
@@ -583,9 +494,9 @@ RSpec.describe PaymentProcessingReactor do
583
494
 
584
495
  expect(PaymentGateway).to receive(:void_authorization)
585
496
 
586
- result = PaymentProcessingReactor.run(valid_payment_params)
497
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
587
498
 
588
- expect(result).to be_failure
499
+ expect(subject).to be_failure
589
500
  end
590
501
  end
591
502
  end
@@ -1,6 +1,6 @@
1
1
  # Getting Started with RubyReactor
2
2
 
3
- This guide will help you get started with RubyReactor, from installation to your first reactor.
3
+ This guide walks you through installation, configuration, and your first reactor.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,130 +16,158 @@ Or install directly:
16
16
  gem install ruby_reactor
17
17
  ```
18
18
 
19
+ ## Configuration
20
+
21
+ RubyReactor uses Redis for state persistence and Sidekiq for async execution. Configure both before running any reactors:
22
+
23
+ ```ruby
24
+ RubyReactor.configure do |config|
25
+ # Redis configuration for state persistence
26
+ config.storage.adapter = :redis
27
+ config.storage.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
28
+ config.storage.redis_options = { timeout: 1 }
29
+
30
+ # Sidekiq configuration for async execution
31
+ config.sidekiq_queue = :default
32
+ config.sidekiq_retry_count = 3
33
+
34
+ # Logger
35
+ config.logger = Logger.new($stdout)
36
+ end
37
+ ```
38
+
19
39
  ## Your First Reactor
20
40
 
21
- Let's create a simple order processing reactor:
41
+ Reactors are subclasses of `RubyReactor::Reactor`. Declare `input`s, define `step`s with their `argument`s and `run` block, and optionally a `returns` step:
22
42
 
23
43
  ```ruby
24
44
  require 'ruby_reactor'
25
45
 
26
46
  class OrderProcessingReactor < RubyReactor::Reactor
47
+ input :order_id do
48
+ required(:order_id).filled(:integer, gt?: 0)
49
+ end
50
+
27
51
  step :validate_order do
28
- validate_args do
29
- required(:order_id).filled(:string)
30
- end
52
+ argument :order_id, input(:order_id)
31
53
 
32
- run do |order_id|
33
- # Validate the order exists and is in correct state
34
- order = Order.find(order_id)
35
- raise "Order not found" unless order
36
- raise "Order already processed" if order.processed?
54
+ run do |args, _context|
55
+ order = Order.find_by(id: args[:order_id])
56
+ return Failure("Order not found") unless order
57
+ return Failure("Order already processed") if order.processed?
37
58
 
38
59
  Success({ order: order })
39
60
  end
40
61
  end
41
62
 
42
63
  step :process_payment do
43
- run do |order:, **|
44
- # Process payment for the order
45
- payment_result = PaymentService.charge(order.total, order.customer.card_token)
46
- raise "Payment failed" unless payment_result.success?
64
+ argument :order, result(:validate_order, :order)
47
65
 
48
- Success({ payment_id: payment_result.id })
66
+ run do |args, _context|
67
+ payment = PaymentService.charge(args[:order].total, args[:order].customer.card_token)
68
+ payment.success? ? Success({ payment_id: payment.id }) : Failure("Payment failed")
49
69
  end
50
70
  end
51
71
 
52
72
  step :update_inventory do
53
- run do |order:, **|
54
- # Update inventory for each item
55
- order.items.each do |item|
73
+ argument :order, result(:validate_order, :order)
74
+
75
+ run do |args, _context|
76
+ args[:order].items.each do |item|
56
77
  InventoryService.decrement(item.product_id, item.quantity)
57
78
  end
58
-
59
79
  Success({ inventory_updated: true })
60
80
  end
61
81
  end
62
82
 
63
83
  step :send_confirmation do
64
- run do |order:, payment_id:, **|
65
- # Send confirmation email
66
- email_result = EmailService.send_confirmation(
67
- order.customer.email,
68
- order_id: order.id,
69
- payment_id: payment_id
70
- )
71
-
72
- Success({ confirmation_sent: email_result.success? })
84
+ argument :order, result(:validate_order, :order)
85
+ argument :payment_id, result(:process_payment, :payment_id)
86
+
87
+ run do |args, _context|
88
+ EmailService.send_confirmation(args[:order].customer.email, order: args[:order])
89
+ Success({ confirmation_sent: true })
73
90
  end
74
91
  end
92
+
93
+ returns :send_confirmation
75
94
  end
76
95
  ```
77
96
 
97
+ ### Run blocks always receive `(arguments, context)`
98
+
99
+ Every step's `run` block receives two positional arguments: the resolved arguments hash and the execution context. Use `argument :name, source` to declare which value goes into `args[:name]`.
100
+
78
101
  ## Executing a Reactor
79
102
 
80
103
  ### Synchronous Execution
81
104
 
82
105
  ```ruby
83
- # Run the reactor synchronously
84
106
  result = OrderProcessingReactor.run(order_id: 123)
85
107
 
86
108
  if result.success?
87
- puts "Order processed successfully!"
88
- puts "Results: #{result.step_results}"
109
+ puts "Order processed: #{result.value}"
89
110
  else
90
111
  puts "Order processing failed: #{result.error}"
91
112
  end
92
113
  ```
93
114
 
115
+ `Reactor.run` returns one of:
116
+
117
+ - `RubyReactor::Success` — `result.success?` is `true`, `result.value` holds the step output for `returns` (or the full `intermediate_results` hash if no `returns` is set).
118
+ - `RubyReactor::Failure` — `result.failure?` is `true`. Useful readers: `result.error`, `result.step_name`, `result.exception_class`, `result.backtrace`, `result.step_arguments`.
119
+ - `RubyReactor::AsyncResult` — returned when the reactor (or a step) is async. Holds `job_id`, `execution_id`, and any `intermediate_results` available at handoff.
120
+ - `RubyReactor::InterruptResult` — returned when an `interrupt` step pauses execution. Use `result.execution_id` and `result.correlation_id` to resume later.
121
+
94
122
  ### Asynchronous Execution
95
123
 
96
- For async execution, you need Sidekiq configured. Mark the reactor as async:
124
+ For async execution, configure Sidekiq and either mark the reactor `async true` or mark individual steps async:
97
125
 
98
126
  ```ruby
99
127
  class OrderProcessingReactor < RubyReactor::Reactor
100
- async true # Enable full reactor async
128
+ async true # Entire reactor runs in a Sidekiq worker
101
129
 
102
130
  # ... steps defined above
103
131
  end
104
132
 
105
- # Run asynchronously
106
133
  async_result = OrderProcessingReactor.run(order_id: 123)
107
- # Returns immediately with AsyncResult
108
- # Check status later with async_result.status
134
+ async_result.execution_id # => UUID for looking up state later
109
135
  ```
110
136
 
111
- ## Understanding Results
112
-
113
- RubyReactor returns detailed execution results:
137
+ To inspect a running execution, reload it from storage:
114
138
 
115
139
  ```ruby
116
- result = OrderProcessingReactor.run(order_id: 123)
140
+ reactor = OrderProcessingReactor.find(async_result.execution_id)
141
+ reactor.context.status # => "running" | "completed" | "failed" | "paused"
142
+ reactor.result # Success / Failure / InterruptResult
143
+ ```
117
144
 
118
- # Check overall success
119
- result.success? # => true/false
145
+ See [Async Reactors](async_reactors.md) for the full async model.
120
146
 
121
- # Access step results
122
- result.step_results[:validate_order] # => { order: #<Order> }
123
- result.step_results[:process_payment] # => { payment_id: "pay_123" }
147
+ ## Inspecting the Context
124
148
 
125
- # Access intermediate results
126
- result.intermediate_results # => Hash of all step outputs
149
+ Step outputs are stored on the context, not the result object. After a sync execution you can reach them via the reactor instance:
127
150
 
128
- # Check completed steps
129
- result.completed_steps # => Set of completed step names
151
+ ```ruby
152
+ reactor = OrderProcessingReactor.new
153
+ reactor.run(order_id: 123)
130
154
 
131
- # Error information (if failed)
132
- result.error # => Exception that caused failure
155
+ reactor.context.intermediate_results[:validate_order] # => { order: <Order> }
156
+ reactor.context.intermediate_results[:process_payment] # => { payment_id: "pay_123" }
157
+ reactor.context.status # => "completed"
158
+ reactor.execution_trace # => [{ type: :run, step: :validate_order, ... }, ...]
133
159
  ```
134
160
 
161
+ For black-box assertions in tests, use the `test_reactor` helper described in [Testing with RSpec](testing.md).
162
+
135
163
  ## Step Dependencies
136
164
 
137
- Steps can depend on each other using the `argument` method with `result()`:
165
+ Steps depend on each other through `argument :name, result(:other_step)`. The dependency graph topologically sorts steps; circular dependencies raise `RubyReactor::Error::DependencyError`:
138
166
 
139
167
  ```ruby
140
168
  class ComplexReactor < RubyReactor::Reactor
141
169
  step :validate_order do
142
- run { validate_order_logic }
170
+ run { |args, _ctx| validate_order_logic(args) }
143
171
  end
144
172
 
145
173
  step :check_inventory do
@@ -160,65 +188,55 @@ class ComplexReactor < RubyReactor::Reactor
160
188
  end
161
189
  ```
162
190
 
191
+ You can also declare order without data flow using `wait_for :step_name`.
192
+
163
193
  ## Error Handling and Compensation
164
194
 
165
- RubyReactor automatically handles errors and provides compensation:
195
+ When a step fails (returns `Failure(...)` or raises), the reactor:
196
+
197
+ 1. Runs the **`compensate`** block of the failing step (signature: `|error, arguments, context|`).
198
+ 2. Walks back through previously successful steps and runs each one's **`undo`** block in reverse order (signature: `|result, arguments, context|`).
199
+ 3. Returns a `Failure`.
166
200
 
167
201
  ```ruby
168
202
  class OrderProcessingReactor < RubyReactor::Reactor
169
- step :validate_order do
170
- run { validate_order_logic }
171
- end
172
-
173
203
  step :process_payment do
174
- run { process_payment_logic }
204
+ argument :order, result(:validate_order)
175
205
 
176
- undo do |payment_id:, **|
177
- # Undo the payment if something fails later
178
- PaymentService.refund(payment_id)
206
+ run { |args, _ctx| PaymentService.charge(args[:order]) }
207
+
208
+ undo do |result, _args, _ctx|
209
+ # Runs if a later step fails
210
+ PaymentService.refund(result[:payment_id])
211
+ Success()
179
212
  end
180
213
  end
181
214
 
182
215
  step :update_inventory do
183
- run { update_inventory_logic }
216
+ argument :order, result(:validate_order)
184
217
 
185
- compensate do |order:, **|
186
- # Restore inventory if something fails later
187
- order.items.each do |item|
188
- InventoryService.increment(item.product_id, item.quantity)
189
- end
218
+ run { |args, _ctx| InventoryService.decrement_all(args[:order].items) }
219
+
220
+ compensate do |_error, args, _ctx|
221
+ # Runs only if THIS step fails
222
+ args[:order].items.each { |i| InventoryService.increment(i.product_id, i.quantity) }
223
+ Success()
190
224
  end
191
225
  end
192
226
  end
193
227
  ```
194
228
 
195
- If `update_inventory` fails, RubyReactor will:
196
- 1. Run the `update_inventory` compensate block
197
- 2. Run the `process_payment` undo block
198
- 3. Return a failure result
199
-
200
- ## Configuration
201
-
202
- ### Sidekiq Setup (for Async)
203
-
204
- Add to your Sidekiq configuration:
205
-
206
- ```ruby
207
- # config/sidekiq.rb
208
- require 'ruby_reactor/worker'
209
-
210
- # Configure RubyReactor
211
- RubyReactor.configure do |config|
212
- config.sidekiq_queue = :default
213
- config.sidekiq_retry_count = 3
214
- config.logger = Logger.new('log/ruby_reactor.log')
215
- end
216
- ```
229
+ If `update_inventory` fails: its `compensate` runs, then `process_payment`'s `undo` runs.
217
230
 
231
+ See [Core Concepts](core_concepts.md#compensation) for the full compensation/undo model.
218
232
 
219
233
  ## Next Steps
220
234
 
221
- - Learn about [async reactors](async_reactors.md)
222
- - Configure [retry policies](retry_configuration.md)
223
- - See [examples](examples/) for more patterns
224
- - Check the [API reference](api_reference.md) for detailed documentation
235
+ - [Core Concepts](core_concepts.md) — Reactors, Steps, Context, Results
236
+ - [Async Reactors](async_reactors.md) — Full and step-level async execution
237
+ - [Retry Configuration](retry_configuration.md) Backoff strategies and retry policies
238
+ - [Interrupts](interrupts.md) Pause/resume workflows
239
+ - [Composition](composition.md) — Build complex flows from smaller reactors
240
+ - [Data Pipelines](data_pipelines.md) — Map over collections in parallel
241
+ - [Testing with RSpec](testing.md) — `test_reactor`, mocks, matchers
242
+ - [Examples](examples/) — End-to-end workflows
@@ -9,7 +9,7 @@ Use the `interrupt` keyword to define a pause point in your reactor.
9
9
  ```ruby
10
10
  class ReportReactor < RubyReactor::Reactor
11
11
  step :request_report do
12
- run do
12
+ run do |_args, _ctx|
13
13
  response = HTTP.post("https://api.example.com/reports")
14
14
  Success(response.fetch(:id))
15
15
  end
@@ -46,7 +46,7 @@ class ReportReactor < RubyReactor::Reactor
46
46
  # The result of the interrupt step is the payload provided when resuming
47
47
  argument :webhook_payload, result(:wait_for_report)
48
48
 
49
- run do |args|
49
+ run do |args, _ctx|
50
50
  Success(ReportProcessor.call(args[:webhook_payload]))
51
51
  end
52
52
  end
@@ -73,8 +73,9 @@ When a reactor encounters an `interrupt`:
73
73
  execution = ReportReactor.run(company_id: 1)
74
74
 
75
75
  if execution.paused?
76
- execution.id # => "uuid-123"
77
- execution.status # => :paused
76
+ execution.execution_id # => "uuid-123"
77
+ execution.correlation_id # => "report-..." (if defined)
78
+ execution.status # => :paused
78
79
  end
79
80
  ```
80
81
 
@@ -122,12 +123,13 @@ There are two ways to invoke continuation:
122
123
  You can cancel a paused reactor if the operation is no longer needed.
123
124
 
124
125
  ```ruby
125
- # Undo: Runs defined compensations for completed steps in reverse order, then deletes execution.
126
+ # Undo: Runs defined undo/compensate blocks for completed steps in reverse order,
127
+ # then marks the execution as cancelled.
126
128
  ReportReactor.undo("uuid-123")
127
129
 
128
130
  # Cancel: Stops execution immediately and marks the reactor as cancelled with the provided reason.
129
131
  # The context is preserved for inspection, but resumption is disabled.
130
- ReportReactor.cancel("uuid-123", reason: "User cancelled")
132
+ ReportReactor.cancel(id: "uuid-123", reason: "User cancelled")
131
133
  ```
132
134
 
133
135
  ## Common Use Cases
@@ -142,7 +144,7 @@ end
142
144
 
143
145
  step :process_decision do
144
146
  argument :decision, result(:wait_for_approval)
145
- run do |args|
147
+ run do |args, _ctx|
146
148
  if args[:decision][:approved]
147
149
  Success("Approved")
148
150
  else