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,365 @@
1
+ # Order Processing Reactor Example
2
+
3
+ This example demonstrates a complete order processing workflow with validation, payment processing, inventory management, and notifications.
4
+
5
+ ## Overview
6
+
7
+ The OrderProcessingReactor handles the complete order fulfillment process:
8
+
9
+ 1. **Validate Order**: Ensure order exists and is processable
10
+ 2. **Check Inventory**: Verify all items are available
11
+ 3. **Reserve Inventory**: Temporarily reserve items
12
+ 4. **Process Payment**: Charge the customer's payment method
13
+ 5. **Update Inventory**: Permanently decrement inventory
14
+ 6. **Send Confirmation**: Email order confirmation
15
+
16
+ ### Order Processing Workflow
17
+
18
+ ```mermaid
19
+ graph TD
20
+ A[Order Submitted] --> B[validate_order]
21
+ B --> C{Order<br/>Valid?}
22
+ C -->|No| D[Fail: Invalid Order]
23
+ C -->|Yes| E[check_inventory]
24
+ E --> F{Inventory<br/>Available?}
25
+ F -->|No| G[Fail: Insufficient Stock]
26
+ F -->|Yes| H[reserve_inventory]
27
+ H --> I{Reservation<br/>Successful?}
28
+ I -->|No| J[Fail: Reservation Error]
29
+ I -->|Yes| K[process_payment]
30
+ K --> L{Payment<br/>Successful?}
31
+ L -->|No| M[Compensate: Release Reservation]
32
+ L -->|Yes| N[update_inventory]
33
+ N --> O{Update<br/>Successful?}
34
+ O -->|No| P[Compensate: Refund + Release]
35
+ O -->|Yes| Q[update_order_status]
36
+ Q --> R[send_confirmation]
37
+ R --> S[Success: Order Complete]
38
+ ```
39
+
40
+ ## Implementation
41
+
42
+ ```ruby
43
+ class OrderProcessingReactor < RubyReactor::Reactor
44
+ async true # Enable asynchronous execution
45
+
46
+ # Reactor-level retry defaults
47
+ retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
48
+
49
+ step :validate_order do
50
+ validate_args do
51
+ required(:order_id).filled(:string)
52
+ end
53
+
54
+ run do |order_id:|
55
+ order = Order.find_by(id: order_id)
56
+ raise "Order not found" unless order
57
+ raise "Order already processed" if order.processed?
58
+ raise "Order cancelled" if order.cancelled?
59
+
60
+ Success({ order: order })
61
+ end
62
+ end
63
+
64
+ step :check_inventory do
65
+ argument :order, result(:validate_order)
66
+
67
+ run do |order:, **|
68
+ unavailable_items = []
69
+
70
+ order.items.each do |item|
71
+ product = Product.find(item.product_id)
72
+ if product.inventory_count < item.quantity
73
+ unavailable_items << {
74
+ product_id: item.product_id,
75
+ requested: item.quantity,
76
+ available: product.inventory_count
77
+ }
78
+ end
79
+ end
80
+
81
+ raise "Insufficient inventory: #{unavailable_items}" unless unavailable_items.empty?
82
+
83
+ Success({ inventory_checked: true })
84
+ end
85
+ end
86
+
87
+ step :reserve_inventory do
88
+ argument :order, result(:validate_order)
89
+
90
+ run do |order:, **|
91
+ reservation_id = InventoryService.reserve_items(order.items)
92
+ raise "Inventory reservation failed" unless reservation_id
93
+
94
+ Success({ reservation_id: reservation_id })
95
+ end
96
+
97
+ undo do |reservation_id:, **|
98
+ # Release reservation on failure
99
+ InventoryService.release_reservation(reservation_id) if reservation_id
100
+ end
101
+ end
102
+
103
+ step :process_payment do
104
+ argument :order, result(:validate_order)
105
+
106
+ # Payment processing needs careful retry handling
107
+ retries max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
108
+
109
+ run do |order:, **|
110
+ payment_result = PaymentService.charge(
111
+ amount: order.total,
112
+ currency: order.currency,
113
+ card_token: order.customer.card_token,
114
+ description: "Order ##{order.id}"
115
+ )
116
+
117
+ raise "Payment failed: #{payment_result.error}" unless payment_result.success?
118
+
119
+ Success({ payment_id: payment_result.id, payment_amount: order.total })
120
+ end
121
+
122
+ undo do |payment_id:, **|
123
+ # Refund payment on failure
124
+ PaymentService.refund(payment_id) if payment_id
125
+ end
126
+ end
127
+
128
+ step :update_inventory do
129
+ argument :order, result(:validate_order)
130
+ argument :reservation_id, result(:reserve_inventory)
131
+
132
+ run do |order:, reservation_id:, **|
133
+ # Convert reservation to permanent inventory reduction
134
+ success = InventoryService.confirm_reservation(reservation_id)
135
+ raise "Inventory update failed" unless success
136
+
137
+ Success({ inventory_updated: true })
138
+ end
139
+
140
+ undo do |order:, reservation_id:, **|
141
+ # This shouldn't normally happen since payment succeeded
142
+ # But if it does, we need to restore inventory
143
+ InventoryService.restore_from_reservation(reservation_id) if reservation_id
144
+ end
145
+ end
146
+
147
+ step :update_order_status do
148
+ argument :order, result(:validate_order)
149
+ argument :payment_id, result(:process_payment)
150
+
151
+ run do |order:, payment_id:, **|
152
+ order.update!(
153
+ status: :completed,
154
+ payment_id: payment_id,
155
+ processed_at: Time.current
156
+ )
157
+
158
+ Success({ order_completed: true })
159
+ end
160
+ end
161
+
162
+ step :send_confirmation do
163
+ argument :order, result(:validate_order)
164
+ argument :payment_id, result(:process_payment)
165
+
166
+ retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
167
+
168
+ run do |order:, payment_id:, **|
169
+ email_result = EmailService.send_order_confirmation(
170
+ to: order.customer.email,
171
+ order: order,
172
+ payment_id: payment_id
173
+ )
174
+
175
+ raise "Confirmation email failed" unless email_result.success?
176
+
177
+ Success({ confirmation_sent: true })
178
+ end
179
+ end
180
+ end
181
+ ```
182
+
183
+ ## Usage
184
+
185
+ ### Asynchronous Execution
186
+
187
+ ```ruby
188
+ # Start order processing asynchronously
189
+ async_result = OrderProcessingReactor.run(order_id: 12345)
190
+
191
+ # Check status later
192
+ case async_result.status
193
+ when :success
194
+ puts "Order processed successfully!"
195
+ result = async_result.result
196
+ puts "Payment ID: #{result.step_results[:process_payment][:payment_id]}"
197
+ when :failed
198
+ puts "Order processing failed: #{async_result.error.message}"
199
+ # Could trigger manual review process
200
+ end
201
+ ```
202
+
203
+ ### Synchronous Execution (for testing)
204
+
205
+ ```ruby
206
+ # For testing or immediate processing
207
+ result = OrderProcessingReactor.run(order_id: 12345)
208
+
209
+ if result.success?
210
+ puts "Order completed!"
211
+ puts "Steps completed: #{result.completed_steps.to_a}"
212
+ else
213
+ puts "Failed at step: #{result.error.step_name}"
214
+ puts "Error: #{result.error.message}"
215
+ end
216
+ ```
217
+
218
+ ## Error Scenarios
219
+
220
+ ### Insufficient Inventory
221
+
222
+ ```
223
+ Step: check_inventory fails
224
+ → Compensation: none (no state changes yet)
225
+ → Result: failure with inventory details
226
+ ```
227
+
228
+ ### Payment Failure
229
+
230
+ ```
231
+ Step: process_payment fails
232
+ → Compensation: release_inventory (reservation_id)
233
+ → Result: failure with payment error
234
+ ```
235
+
236
+ ### Email Failure
237
+
238
+ ```
239
+ Step: send_confirmation fails (after successful payment/inventory update)
240
+ → Compensation: refund_payment → restore_inventory
241
+ → Result: failure (but order is actually complete - manual confirmation may be needed)
242
+ ```
243
+
244
+ ## Testing
245
+
246
+ ```ruby
247
+ RSpec.describe OrderProcessingReactor do
248
+ let(:order) { create(:order, :pending) }
249
+
250
+ context "successful order processing" do
251
+ it "completes all steps successfully" do
252
+ # Mock all external services
253
+ allow(Order).to receive(:find_by).and_return(order)
254
+ allow(InventoryService).to receive(:reserve_items).and_return("res_123")
255
+ allow(PaymentService).to receive(:charge).and_return(successful_payment)
256
+ allow(InventoryService).to receive(:confirm_reservation).and_return(true)
257
+ allow(EmailService).to receive(:send_order_confirmation).and_return(successful_email)
258
+
259
+ result = OrderProcessingReactor.run(order_id: order.id)
260
+
261
+ expect(result).to be_success
262
+ expect(result.completed_steps).to include(:send_confirmation)
263
+ end
264
+ end
265
+
266
+ context "payment failure" do
267
+ it "compensates inventory reservation" do
268
+ allow(Order).to receive(:find_by).and_return(order)
269
+ allow(InventoryService).to receive(:reserve_items).and_return("res_123")
270
+ allow(PaymentService).to receive(:charge).and_return(failed_payment)
271
+
272
+ expect(InventoryService).to receive(:release_reservation).with("res_123")
273
+
274
+ result = OrderProcessingReactor.run(order_id: order.id)
275
+
276
+ expect(result).to be_failure
277
+ expect(result.error.message).to include("Payment failed")
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ ## Monitoring
284
+
285
+ Key metrics to track:
286
+
287
+ ```ruby
288
+ # Success rates
289
+ order_processing_success_rate
290
+ payment_success_rate
291
+ inventory_reservation_success_rate
292
+
293
+ # Performance
294
+ average_order_processing_time
295
+ payment_processing_latency
296
+
297
+ # Error rates
298
+ inventory_insufficient_rate
299
+ payment_failure_rate
300
+ email_delivery_failure_rate
301
+ ```
302
+
303
+ ## Scaling Considerations
304
+
305
+ - **High Volume**: Use async execution with multiple Sidekiq workers
306
+ - **Payment Processing**: Implement idempotency keys for payment providers
307
+ - **Inventory**: Use optimistic locking or database transactions
308
+ - **Email**: Queue emails separately to avoid blocking order completion
309
+
310
+ ## Extensions
311
+
312
+ ### Partial Order Processing
313
+
314
+ ```ruby
315
+ class PartialOrderProcessingReactor < OrderProcessingReactor
316
+ # Override to allow partial fulfillment
317
+ step :check_inventory do
318
+ run do |order:, **|
319
+ available_items, unavailable_items = partition_available_items(order.items)
320
+
321
+ if available_items.any? && unavailable_items.any?
322
+ # Create partial order for available items
323
+ partial_order = create_partial_order(order, available_items)
324
+ Success({ partial_order: partial_order, unavailable_items: unavailable_items })
325
+ elsif available_items.empty?
326
+ raise "No items available"
327
+ else
328
+ Success({ inventory_checked: true })
329
+ end
330
+ end
331
+ end
332
+ end
333
+ ```
334
+
335
+ ### Order Cancellation
336
+
337
+ ```ruby
338
+ class OrderCancellationReactor < RubyReactor::Reactor
339
+ step :load_order do
340
+ validate_args do
341
+ required(:order_id).filled(:string)
342
+ end
343
+
344
+ run do |order_id:|
345
+ order = Order.find_by(id: order_id)
346
+ raise "Order not found" unless order
347
+ Success({ order: order })
348
+ end
349
+ end
350
+
351
+ step :cancel_order do
352
+ run do |order:, **|
353
+ # Only cancel if not already completed
354
+ if order.completed?
355
+ # Initiate refund and inventory restoration
356
+ PaymentService.refund(order.payment_id)
357
+ InventoryService.restore_order_items(order)
358
+ end
359
+
360
+ order.update!(status: :cancelled)
361
+ Success({ cancelled: true })
362
+ end
363
+ end
364
+ end
365
+ ```