ruby_reactor 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +145 -9
- data/documentation/README.md +20 -8
- data/documentation/async_reactors.md +46 -34
- data/documentation/core_concepts.md +75 -61
- data/documentation/examples/inventory_management.md +2 -3
- data/documentation/examples/order_processing.md +92 -77
- data/documentation/examples/payment_processing.md +28 -117
- data/documentation/getting_started.md +112 -94
- data/documentation/interrupts.md +9 -7
- data/documentation/locks_and_semaphores.md +459 -0
- data/documentation/retry_configuration.md +19 -14
- data/documentation/testing.md +994 -0
- data/lib/ruby_reactor/configuration.rb +19 -2
- data/lib/ruby_reactor/context.rb +13 -5
- data/lib/ruby_reactor/context_serializer.rb +55 -4
- data/lib/ruby_reactor/dsl/lockable.rb +130 -0
- data/lib/ruby_reactor/dsl/reactor.rb +3 -2
- data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
- data/lib/ruby_reactor/executor/result_handler.rb +27 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
- data/lib/ruby_reactor/executor/step_executor.rb +29 -99
- data/lib/ruby_reactor/executor.rb +148 -15
- data/lib/ruby_reactor/lock.rb +92 -0
- data/lib/ruby_reactor/map/collector.rb +16 -15
- data/lib/ruby_reactor/map/element_executor.rb +90 -104
- data/lib/ruby_reactor/map/execution.rb +2 -1
- data/lib/ruby_reactor/map/helpers.rb +2 -1
- data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
- data/lib/ruby_reactor/period.rb +67 -0
- data/lib/ruby_reactor/rate_limit.rb +74 -0
- data/lib/ruby_reactor/reactor.rb +175 -16
- data/lib/ruby_reactor/rspec/helpers.rb +17 -0
- data/lib/ruby_reactor/rspec/matchers.rb +423 -0
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
- data/lib/ruby_reactor/rspec.rb +18 -0
- data/lib/ruby_reactor/semaphore.rb +58 -0
- data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +69 -9
- data/lib/ruby_reactor/step/compose_step.rb +0 -1
- data/lib/ruby_reactor/step/map_step.rb +11 -18
- data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
- data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +32 -24
- data/lib/ruby_reactor.rb +119 -10
- metadata +16 -3
|
@@ -24,12 +24,15 @@ Steps are the individual units of work within a reactor. Each step has a name an
|
|
|
24
24
|
|
|
25
25
|
### Inline Step Definition
|
|
26
26
|
|
|
27
|
+
`run` blocks always receive two positional arguments: the resolved arguments hash and the execution context. Declare inputs with `argument :name, source`:
|
|
28
|
+
|
|
27
29
|
```ruby
|
|
28
30
|
step :validate_order do
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
36
|
Success({ order: order })
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -144,29 +147,22 @@ end
|
|
|
144
147
|
|
|
145
148
|
## Results
|
|
146
149
|
|
|
147
|
-
|
|
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
|
|
150
|
+
`Reactor.run` returns one of four result types:
|
|
158
151
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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`.
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
result.completed_steps # => #<Set: {:validate_order, :process_payment}>
|
|
165
|
-
result.inputs # => { order_id: 123 }
|
|
157
|
+
Step-by-step state lives on the context, not the result object. Reload via `Reactor.find(execution_id)` to inspect:
|
|
166
158
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
```
|
|
170
166
|
|
|
171
167
|
## Error Handling
|
|
172
168
|
|
|
@@ -178,14 +174,18 @@ When a step fails, execution stops and compensation begins:
|
|
|
178
174
|
|
|
179
175
|
```ruby
|
|
180
176
|
step :process_payment do
|
|
181
|
-
|
|
177
|
+
argument :amount, input(:amount)
|
|
178
|
+
argument :token, input(:card_token)
|
|
179
|
+
|
|
180
|
+
run do |args, _ctx|
|
|
182
181
|
# This might fail
|
|
183
|
-
PaymentService.charge(amount, token)
|
|
182
|
+
PaymentService.charge(args[:amount], args[:token])
|
|
184
183
|
end
|
|
185
184
|
|
|
186
|
-
compensate do |
|
|
187
|
-
#
|
|
188
|
-
|
|
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
189
|
end
|
|
190
190
|
end
|
|
191
191
|
```
|
|
@@ -349,22 +349,23 @@ result = Reactor.run(inputs)
|
|
|
349
349
|
|
|
350
350
|
### Asynchronous Execution
|
|
351
351
|
|
|
352
|
-
<!-- TODO: review this part -->
|
|
353
|
-
|
|
354
352
|
```ruby
|
|
355
353
|
async_result = Reactor.run(inputs)
|
|
356
354
|
# Returns immediately
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
when
|
|
363
|
-
|
|
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
364
|
end
|
|
365
365
|
```
|
|
366
366
|
|
|
367
367
|
**Characteristics:**
|
|
368
|
+
|
|
368
369
|
- Non-blocking execution
|
|
369
370
|
- Background processing with Sidekiq
|
|
370
371
|
- Retry capabilities
|
|
@@ -372,33 +373,38 @@ end
|
|
|
372
373
|
|
|
373
374
|
## Step Arguments
|
|
374
375
|
|
|
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.
|
|
376
385
|
|
|
377
386
|
```ruby
|
|
378
387
|
step :validate_order do
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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])
|
|
382
393
|
Success({ order: order })
|
|
383
394
|
end
|
|
384
395
|
end
|
|
385
396
|
|
|
386
397
|
step :process_payment do
|
|
387
|
-
argument :
|
|
398
|
+
argument :order, result(:validate_order, :order)
|
|
388
399
|
|
|
389
400
|
run do |args, _context|
|
|
390
|
-
|
|
391
|
-
order = args[:order_data][:order]
|
|
392
|
-
payment = PaymentService.charge(order.total, order.card_token)
|
|
401
|
+
payment = PaymentService.charge(args[:order].total, args[:order].card_token)
|
|
393
402
|
Success({ payment_id: payment.id })
|
|
394
403
|
end
|
|
395
404
|
end
|
|
396
405
|
```
|
|
397
406
|
|
|
398
|
-
|
|
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
|
|
407
|
+
If a step declares no `argument`s, the reactor's raw inputs hash is passed as `args`.
|
|
402
408
|
|
|
403
409
|
## Undo
|
|
404
410
|
|
|
@@ -415,15 +421,16 @@ Unlike compensation which only runs for the failing step, undo is triggered duri
|
|
|
415
421
|
|
|
416
422
|
```ruby
|
|
417
423
|
step :reserve_inventory do
|
|
418
|
-
|
|
419
|
-
|
|
424
|
+
argument :items, input(:items)
|
|
425
|
+
|
|
426
|
+
run do |args, _ctx|
|
|
427
|
+
reservation_id = InventoryService.reserve(args[:items])
|
|
420
428
|
Success({ reservation_id: reservation_id })
|
|
421
429
|
end
|
|
422
430
|
|
|
423
|
-
undo do |
|
|
431
|
+
undo do |result, arguments, context|
|
|
424
432
|
# Undo receives the step's result, arguments, and full context
|
|
425
|
-
|
|
426
|
-
InventoryService.release(reservation_id)
|
|
433
|
+
InventoryService.release(result[:reservation_id])
|
|
427
434
|
Success("Inventory reservation released")
|
|
428
435
|
end
|
|
429
436
|
end
|
|
@@ -438,9 +445,11 @@ Undo blocks receive three parameters:
|
|
|
438
445
|
|
|
439
446
|
```ruby
|
|
440
447
|
step :complex_operation do
|
|
441
|
-
|
|
448
|
+
argument :input, input(:payload)
|
|
449
|
+
|
|
450
|
+
run do |args, _ctx|
|
|
442
451
|
# Complex operation that modifies external state
|
|
443
|
-
record = create_record(input)
|
|
452
|
+
record = create_record(args[:input])
|
|
444
453
|
notification = send_notification(record)
|
|
445
454
|
Success({ record_id: record.id, notification_id: notification.id })
|
|
446
455
|
end
|
|
@@ -477,8 +486,10 @@ Compensation runs immediately when a step fails, before the broader rollback pro
|
|
|
477
486
|
|
|
478
487
|
```ruby
|
|
479
488
|
step :reserve_inventory do
|
|
480
|
-
|
|
481
|
-
|
|
489
|
+
argument :items, input(:items)
|
|
490
|
+
|
|
491
|
+
run do |args, _ctx|
|
|
492
|
+
reservation_id = InventoryService.reserve(args[:items])
|
|
482
493
|
Success({ reservation_id: reservation_id })
|
|
483
494
|
end
|
|
484
495
|
|
|
@@ -501,9 +512,12 @@ Compensation blocks receive three parameters:
|
|
|
501
512
|
|
|
502
513
|
```ruby
|
|
503
514
|
step :process_payment do
|
|
504
|
-
|
|
515
|
+
argument :order, result(:validate_order)
|
|
516
|
+
argument :payment_method, input(:payment_method)
|
|
517
|
+
|
|
518
|
+
run do |args, _ctx|
|
|
505
519
|
# Payment processing logic that might fail
|
|
506
|
-
PaymentService.charge(order.total, payment_method)
|
|
520
|
+
PaymentService.charge(args[:order].total, args[:payment_method])
|
|
507
521
|
end
|
|
508
522
|
|
|
509
523
|
compensate do |error, arguments, context|
|
|
@@ -198,7 +198,6 @@ class InventoryManagementReactor < RubyReactor::Reactor
|
|
|
198
198
|
argument :request_data, result(:validate_request)
|
|
199
199
|
argument :replenishment_data, result(:check_replenishment)
|
|
200
200
|
|
|
201
|
-
idempotent true
|
|
202
201
|
retries max_attempts: 3, backoff: :linear, base_delay: 5.seconds
|
|
203
202
|
|
|
204
203
|
run do |args, _context|
|
|
@@ -283,7 +282,7 @@ class BulkInventoryReactor < RubyReactor::Reactor
|
|
|
283
282
|
if result.success?
|
|
284
283
|
results << { success: true, operation: op }
|
|
285
284
|
else
|
|
286
|
-
results << { success: false, operation: op, error: result.error.
|
|
285
|
+
results << { success: false, operation: op, error: result.error.to_s }
|
|
287
286
|
end
|
|
288
287
|
rescue => e
|
|
289
288
|
results << { success: false, operation: op, error: e.message }
|
|
@@ -629,7 +628,7 @@ RSpec.describe InventoryManagementReactor do
|
|
|
629
628
|
)
|
|
630
629
|
|
|
631
630
|
expect(result).to be_failure
|
|
632
|
-
expect(result.error
|
|
631
|
+
expect(result.error).to include("Insufficient stock")
|
|
633
632
|
end
|
|
634
633
|
end
|
|
635
634
|
|
|
@@ -46,28 +46,30 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
46
46
|
# Reactor-level retry defaults
|
|
47
47
|
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
|
|
48
48
|
|
|
49
|
+
input :order_id do
|
|
50
|
+
required(:order_id).filled(:string)
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
step :validate_order do
|
|
50
|
-
|
|
51
|
-
required(:order_id).filled(:string)
|
|
52
|
-
end
|
|
54
|
+
argument :order_id, input(:order_id)
|
|
53
55
|
|
|
54
|
-
run do |
|
|
55
|
-
order = Order.find_by(id: order_id)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
run do |args, _ctx|
|
|
57
|
+
order = Order.find_by(id: args[:order_id])
|
|
58
|
+
return Failure("Order not found") unless order
|
|
59
|
+
return Failure("Order already processed") if order.processed?
|
|
60
|
+
return Failure("Order cancelled") if order.cancelled?
|
|
59
61
|
|
|
60
62
|
Success({ order: order })
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
step :check_inventory do
|
|
65
|
-
argument :order, result(:validate_order)
|
|
67
|
+
argument :order, result(:validate_order, :order)
|
|
66
68
|
|
|
67
|
-
run do |
|
|
69
|
+
run do |args, _ctx|
|
|
68
70
|
unavailable_items = []
|
|
69
71
|
|
|
70
|
-
order.items.each do |item|
|
|
72
|
+
args[:order].items.each do |item|
|
|
71
73
|
product = Product.find(item.product_id)
|
|
72
74
|
if product.inventory_count < item.quantity
|
|
73
75
|
unavailable_items << {
|
|
@@ -78,35 +80,37 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
return Failure("Insufficient inventory: #{unavailable_items}") unless unavailable_items.empty?
|
|
82
84
|
|
|
83
85
|
Success({ inventory_checked: true })
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
step :reserve_inventory do
|
|
88
|
-
argument :order, result(:validate_order)
|
|
90
|
+
argument :order, result(:validate_order, :order)
|
|
89
91
|
|
|
90
|
-
run do |
|
|
91
|
-
reservation_id = InventoryService.reserve_items(order.items)
|
|
92
|
-
|
|
92
|
+
run do |args, _ctx|
|
|
93
|
+
reservation_id = InventoryService.reserve_items(args[:order].items)
|
|
94
|
+
return Failure("Inventory reservation failed") unless reservation_id
|
|
93
95
|
|
|
94
96
|
Success({ reservation_id: reservation_id })
|
|
95
97
|
end
|
|
96
98
|
|
|
97
|
-
undo do |
|
|
98
|
-
# Release reservation
|
|
99
|
-
InventoryService.release_reservation(reservation_id) if reservation_id
|
|
99
|
+
undo do |result, _args, _ctx|
|
|
100
|
+
# Release reservation when a later step fails
|
|
101
|
+
InventoryService.release_reservation(result[:reservation_id]) if result[:reservation_id]
|
|
102
|
+
Success()
|
|
100
103
|
end
|
|
101
104
|
end
|
|
102
105
|
|
|
103
106
|
step :process_payment do
|
|
104
|
-
argument :order, result(:validate_order)
|
|
107
|
+
argument :order, result(:validate_order, :order)
|
|
105
108
|
|
|
106
109
|
# Payment processing needs careful retry handling
|
|
107
110
|
retries max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
|
|
108
111
|
|
|
109
|
-
run do |
|
|
112
|
+
run do |args, _ctx|
|
|
113
|
+
order = args[:order]
|
|
110
114
|
payment_result = PaymentService.charge(
|
|
111
115
|
amount: order.total,
|
|
112
116
|
currency: order.currency,
|
|
@@ -114,44 +118,42 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
114
118
|
description: "Order ##{order.id}"
|
|
115
119
|
)
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
return Failure("Payment failed: #{payment_result.error}") unless payment_result.success?
|
|
118
122
|
|
|
119
123
|
Success({ payment_id: payment_result.id, payment_amount: order.total })
|
|
120
124
|
end
|
|
121
125
|
|
|
122
|
-
undo do |
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
undo do |result, _args, _ctx|
|
|
127
|
+
PaymentService.refund(result[:payment_id]) if result[:payment_id]
|
|
128
|
+
Success()
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
step :update_inventory do
|
|
129
|
-
argument :order, result(:validate_order)
|
|
130
|
-
argument :reservation_id, result(:reserve_inventory)
|
|
133
|
+
argument :order, result(:validate_order, :order)
|
|
134
|
+
argument :reservation_id, result(:reserve_inventory, :reservation_id)
|
|
131
135
|
|
|
132
|
-
run do |
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
raise "Inventory update failed" unless success
|
|
136
|
+
run do |args, _ctx|
|
|
137
|
+
success = InventoryService.confirm_reservation(args[:reservation_id])
|
|
138
|
+
return Failure("Inventory update failed") unless success
|
|
136
139
|
|
|
137
140
|
Success({ inventory_updated: true })
|
|
138
141
|
end
|
|
139
142
|
|
|
140
|
-
undo do |
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
InventoryService.restore_from_reservation(reservation_id) if reservation_id
|
|
143
|
+
undo do |_result, args, _ctx|
|
|
144
|
+
InventoryService.restore_from_reservation(args[:reservation_id]) if args[:reservation_id]
|
|
145
|
+
Success()
|
|
144
146
|
end
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
step :update_order_status do
|
|
148
|
-
argument :order, result(:validate_order)
|
|
149
|
-
argument :payment_id, result(:process_payment)
|
|
150
|
+
argument :order, result(:validate_order, :order)
|
|
151
|
+
argument :payment_id, result(:process_payment, :payment_id)
|
|
150
152
|
|
|
151
|
-
run do |
|
|
152
|
-
order.update!(
|
|
153
|
+
run do |args, _ctx|
|
|
154
|
+
args[:order].update!(
|
|
153
155
|
status: :completed,
|
|
154
|
-
payment_id: payment_id,
|
|
156
|
+
payment_id: args[:payment_id],
|
|
155
157
|
processed_at: Time.current
|
|
156
158
|
)
|
|
157
159
|
|
|
@@ -160,23 +162,25 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
step :send_confirmation do
|
|
163
|
-
argument :order, result(:validate_order)
|
|
164
|
-
argument :payment_id, result(:process_payment)
|
|
165
|
+
argument :order, result(:validate_order, :order)
|
|
166
|
+
argument :payment_id, result(:process_payment, :payment_id)
|
|
165
167
|
|
|
166
168
|
retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
|
|
167
169
|
|
|
168
|
-
run do |
|
|
170
|
+
run do |args, _ctx|
|
|
169
171
|
email_result = EmailService.send_order_confirmation(
|
|
170
|
-
to: order.customer.email,
|
|
171
|
-
order: order,
|
|
172
|
-
payment_id: payment_id
|
|
172
|
+
to: args[:order].customer.email,
|
|
173
|
+
order: args[:order],
|
|
174
|
+
payment_id: args[:payment_id]
|
|
173
175
|
)
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
return Failure("Confirmation email failed") unless email_result.success?
|
|
176
178
|
|
|
177
179
|
Success({ confirmation_sent: true })
|
|
178
180
|
end
|
|
179
181
|
end
|
|
182
|
+
|
|
183
|
+
returns :send_confirmation
|
|
180
184
|
end
|
|
181
185
|
```
|
|
182
186
|
|
|
@@ -187,16 +191,21 @@ end
|
|
|
187
191
|
```ruby
|
|
188
192
|
# Start order processing asynchronously
|
|
189
193
|
async_result = OrderProcessingReactor.run(order_id: 12345)
|
|
194
|
+
async_result.execution_id # UUID for state lookup
|
|
190
195
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
# Reload state later (e.g. from a polling endpoint)
|
|
197
|
+
reactor = OrderProcessingReactor.find(async_result.execution_id)
|
|
198
|
+
case reactor.context.status.to_s
|
|
199
|
+
when "completed"
|
|
194
200
|
puts "Order processed successfully!"
|
|
195
|
-
|
|
196
|
-
puts "Payment ID: #{
|
|
197
|
-
when
|
|
198
|
-
|
|
201
|
+
payment_id = reactor.context.intermediate_results[:process_payment][:payment_id]
|
|
202
|
+
puts "Payment ID: #{payment_id}"
|
|
203
|
+
when "failed"
|
|
204
|
+
failure = reactor.result # RubyReactor::Failure
|
|
205
|
+
puts "Order processing failed at #{failure.step_name}: #{failure.error}"
|
|
199
206
|
# Could trigger manual review process
|
|
207
|
+
when "running"
|
|
208
|
+
puts "Still processing..."
|
|
200
209
|
end
|
|
201
210
|
```
|
|
202
211
|
|
|
@@ -204,14 +213,15 @@ end
|
|
|
204
213
|
|
|
205
214
|
```ruby
|
|
206
215
|
# For testing or immediate processing
|
|
207
|
-
|
|
216
|
+
reactor = OrderProcessingReactor.new
|
|
217
|
+
result = reactor.run(order_id: 12345)
|
|
208
218
|
|
|
209
219
|
if result.success?
|
|
210
220
|
puts "Order completed!"
|
|
211
|
-
puts "Steps completed: #{
|
|
221
|
+
puts "Steps completed: #{reactor.context.intermediate_results.keys}"
|
|
212
222
|
else
|
|
213
|
-
puts "Failed at step: #{result.
|
|
214
|
-
puts "Error: #{result.error
|
|
223
|
+
puts "Failed at step: #{result.step_name}"
|
|
224
|
+
puts "Error: #{result.error}"
|
|
215
225
|
end
|
|
216
226
|
```
|
|
217
227
|
|
|
@@ -256,10 +266,10 @@ RSpec.describe OrderProcessingReactor do
|
|
|
256
266
|
allow(InventoryService).to receive(:confirm_reservation).and_return(true)
|
|
257
267
|
allow(EmailService).to receive(:send_order_confirmation).and_return(successful_email)
|
|
258
268
|
|
|
259
|
-
|
|
269
|
+
subject = test_reactor(OrderProcessingReactor, order_id: order.id)
|
|
260
270
|
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
271
|
+
expect(subject).to be_success
|
|
272
|
+
expect(subject).to have_run_step(:send_confirmation)
|
|
263
273
|
end
|
|
264
274
|
end
|
|
265
275
|
|
|
@@ -271,10 +281,10 @@ RSpec.describe OrderProcessingReactor do
|
|
|
271
281
|
|
|
272
282
|
expect(InventoryService).to receive(:release_reservation).with("res_123")
|
|
273
283
|
|
|
274
|
-
|
|
284
|
+
subject = test_reactor(OrderProcessingReactor, order_id: order.id)
|
|
275
285
|
|
|
276
|
-
expect(
|
|
277
|
-
expect(
|
|
286
|
+
expect(subject).to be_failure
|
|
287
|
+
expect(subject.error).to include("Payment failed")
|
|
278
288
|
end
|
|
279
289
|
end
|
|
280
290
|
end
|
|
@@ -315,15 +325,16 @@ email_delivery_failure_rate
|
|
|
315
325
|
class PartialOrderProcessingReactor < OrderProcessingReactor
|
|
316
326
|
# Override to allow partial fulfillment
|
|
317
327
|
step :check_inventory do
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
argument :order, result(:validate_order, :order)
|
|
329
|
+
|
|
330
|
+
run do |args, _ctx|
|
|
331
|
+
available_items, unavailable_items = partition_available_items(args[:order].items)
|
|
320
332
|
|
|
321
333
|
if available_items.any? && unavailable_items.any?
|
|
322
|
-
|
|
323
|
-
partial_order = create_partial_order(order, available_items)
|
|
334
|
+
partial_order = create_partial_order(args[:order], available_items)
|
|
324
335
|
Success({ partial_order: partial_order, unavailable_items: unavailable_items })
|
|
325
336
|
elsif available_items.empty?
|
|
326
|
-
|
|
337
|
+
Failure("No items available")
|
|
327
338
|
else
|
|
328
339
|
Success({ inventory_checked: true })
|
|
329
340
|
end
|
|
@@ -336,23 +347,27 @@ end
|
|
|
336
347
|
|
|
337
348
|
```ruby
|
|
338
349
|
class OrderCancellationReactor < RubyReactor::Reactor
|
|
350
|
+
input :order_id do
|
|
351
|
+
required(:order_id).filled(:string)
|
|
352
|
+
end
|
|
353
|
+
|
|
339
354
|
step :load_order do
|
|
340
|
-
|
|
341
|
-
required(:order_id).filled(:string)
|
|
342
|
-
end
|
|
355
|
+
argument :order_id, input(:order_id)
|
|
343
356
|
|
|
344
|
-
run do |
|
|
345
|
-
order = Order.find_by(id: order_id)
|
|
346
|
-
|
|
357
|
+
run do |args, _ctx|
|
|
358
|
+
order = Order.find_by(id: args[:order_id])
|
|
359
|
+
return Failure("Order not found") unless order
|
|
347
360
|
Success({ order: order })
|
|
348
361
|
end
|
|
349
362
|
end
|
|
350
363
|
|
|
351
364
|
step :cancel_order do
|
|
352
|
-
|
|
365
|
+
argument :order, result(:load_order, :order)
|
|
366
|
+
|
|
367
|
+
run do |args, _ctx|
|
|
368
|
+
order = args[:order]
|
|
353
369
|
# Only cancel if not already completed
|
|
354
370
|
if order.completed?
|
|
355
|
-
# Initiate refund and inventory restoration
|
|
356
371
|
PaymentService.refund(order.payment_id)
|
|
357
372
|
InventoryService.restore_order_items(order)
|
|
358
373
|
end
|