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,565 +0,0 @@
1
- # Payment Processing Reactor Example
2
-
3
- This example shows a payment processing workflow with fraud detection, multiple payment attempts, and comprehensive error handling.
4
-
5
- ## Overview
6
-
7
- The PaymentProcessingReactor handles complex payment scenarios:
8
-
9
- 1. **Validate Payment**: Check payment details and amounts
10
- 2. **Fraud Check**: Run fraud detection algorithms
11
- 3. **Pre-authorization**: Pre-authorize the payment
12
- 4. **Final Charge**: Complete the payment
13
- 5. **Record Transaction**: Store payment details
14
- 6. **Send Receipt**: Email payment confirmation
15
-
16
- ### Payment Processing Workflow
17
-
18
- ```mermaid
19
- graph TD
20
- A[Payment Request] --> B[validate_payment]
21
- B --> C{Valid<br/>Payment?}
22
- C -->|No| D[Fail: Validation Error]
23
- C -->|Yes| E[fraud_detection]
24
- E --> F{Fraud<br/>Score OK?}
25
- F -->|No| G[Fail: High Fraud Risk]
26
- F -->|Yes| H[pre_authorize]
27
- H --> I{Pre-auth<br/>Success?}
28
- I -->|No| J[Retry with Backoff]
29
- J --> H
30
- I -->|Yes| K[charge_payment]
31
- K --> L{Charge<br/>Success?}
32
- L -->|No| M[Compensate: Void Pre-auth]
33
- L -->|Yes| N[record_transaction]
34
- N --> O[send_receipt]
35
- O --> P[Success: Payment Complete]
36
-
37
- J -->|Max Retries| Q[Fail: Pre-auth Failed]
38
- ```
39
-
40
- ## Implementation
41
-
42
- ```ruby
43
- class PaymentProcessingReactor < RubyReactor::Reactor
44
- async true
45
-
46
- # Payment processing needs careful retry configuration
47
- retry_defaults max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
48
-
49
- input :amount do
50
- required(:amount).filled(:decimal, gt?: 0)
51
- end
52
-
53
- input :currency do
54
- required(:currency).filled(:string, included_in?: ['USD', 'EUR', 'GBP'])
55
- end
56
-
57
- input :card_token do
58
- required(:card_token).filled(:string, format?: /^tok_/)
59
- end
60
-
61
- step :validate_payment do
62
- argument :amount, input(:amount)
63
- argument :currency, input(:currency)
64
- argument :card_token, input(:card_token)
65
-
66
- run do |args, _context|
67
- amount = args[:amount]
68
- currency = args[:currency]
69
- card_token = args[:card_token]
70
-
71
- Success({ validated_amount: amount, validated_currency: currency })
72
- end
73
- end
74
-
75
- step :fraud_detection do
76
- argument :validation_data, result(:validate_payment)
77
- argument :amount, input(:amount)
78
- argument :card_token, input(:card_token)
79
-
80
- run do |args, _context|
81
- amount = args[:amount]
82
- card_token = args[:card_token]
83
-
84
- fraud_score = FraudDetectionService.analyze(
85
- amount: amount,
86
- card_token: card_token,
87
- # Additional context...
88
- )
89
-
90
- if fraud_score > 0.8
91
- raise "High fraud risk detected (score: #{fraud_score})"
92
- end
93
-
94
- Success({ fraud_score: fraud_score, fraud_check_passed: true })
95
- end
96
- end
97
-
98
- step :pre_authorize do
99
- argument :fraud_data, result(:fraud_detection)
100
- argument :amount, input(:amount)
101
- argument :currency, input(:currency)
102
- argument :card_token, input(:card_token)
103
-
104
- retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
105
-
106
- run do |args, _context|
107
- amount = args[:amount]
108
- currency = args[:currency]
109
- card_token = args[:card_token]
110
-
111
- auth_result = PaymentGateway.pre_authorize(
112
- amount: amount,
113
- currency: currency,
114
- card_token: card_token
115
- )
116
-
117
- raise "Pre-authorization failed: #{auth_result.error}" unless auth_result.success?
118
-
119
- Success({ auth_id: auth_result.id, auth_amount: amount })
120
- end
121
-
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
- end
127
- end
128
-
129
- step :charge_payment do
130
- argument :auth_data, result(:pre_authorize)
131
-
132
- # Final charge - critical operation
133
- retries max_attempts: 3, backoff: :fixed, base_delay: 60.seconds
134
-
135
- run do |args, _context|
136
- auth_id = args[:auth_data][:auth_id]
137
- amount = args[:auth_data][:auth_amount]
138
- currency = args[:currency]
139
-
140
- charge_result = PaymentGateway.charge(
141
- auth_id: auth_id,
142
- amount: amount,
143
- currency: currency
144
- )
145
-
146
- raise "Charge failed: #{charge_result.error}" unless charge_result.success?
147
-
148
- Success({ charge_id: charge_result.id, charged_amount: amount })
149
- end
150
-
151
- undo do |result, _args, _ctx|
152
- PaymentGateway.refund(result[:charge_id]) if result[:charge_id]
153
- Success()
154
- end
155
- end
156
-
157
- step :record_transaction do
158
- argument :charge_data, result(:charge_payment)
159
- argument :fraud_data, result(:fraud_detection)
160
- argument :amount, input(:amount)
161
- argument :currency, input(:currency)
162
- argument :card_token, input(:card_token)
163
-
164
- run do |args, _context|
165
- charge_id = args[:charge_data][:charge_id]
166
- amount = args[:amount]
167
- currency = args[:currency]
168
- card_token = args[:card_token]
169
- fraud_score = args[:fraud_data][:fraud_score]
170
-
171
- transaction = PaymentTransaction.create!(
172
- charge_id: charge_id,
173
- amount: amount,
174
- currency: currency,
175
- card_token: card_token,
176
- fraud_score: fraud_score,
177
- status: :completed,
178
- processed_at: Time.current
179
- )
180
-
181
- { transaction_id: transaction.id }
182
- end
183
-
184
- undo do |result, _args, _ctx|
185
- PaymentTransaction.find_by(id: result[:transaction_id])&.update!(status: :refunded)
186
- Success()
187
- end
188
- end
189
-
190
- step :send_receipt do
191
- argument :transaction_data, result(:record_transaction)
192
- argument :amount, input(:amount)
193
- argument :currency, input(:currency)
194
-
195
- retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
196
-
197
- run do |args, _context|
198
- transaction_id = args[:transaction_data][:transaction_id]
199
- amount = args[:amount]
200
- currency = args[:currency]
201
-
202
- transaction = PaymentTransaction.find(transaction_id)
203
- customer = transaction.customer
204
-
205
- receipt_result = EmailService.send_payment_receipt(
206
- to: customer.email,
207
- transaction: transaction,
208
- amount: amount,
209
- currency: currency
210
- )
211
-
212
- raise "Receipt delivery failed" unless receipt_result.success?
213
-
214
- { receipt_sent: true }
215
- end
216
- end
217
- end
218
- ```
219
-
220
- ## Advanced Payment Scenarios
221
-
222
- ### Multi-Attempt Payment Processing
223
-
224
- ```ruby
225
- class MultiAttemptPaymentReactor < RubyReactor::Reactor
226
- async true
227
-
228
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 10.seconds
229
-
230
- step :attempt_primary_card do
231
- argument :order, input(:order)
232
-
233
- run do |args, _context|
234
- order = args[:order]
235
-
236
- result = process_payment_with_card(order, order.primary_card)
237
- if result.success?
238
- { payment_result: result, card_used: :primary }
239
- else
240
- { payment_failed: true, primary_card_failed: true }
241
- end
242
- end
243
- end
244
-
245
- step :attempt_backup_card do
246
- argument :primary_attempt, result(:attempt_primary_card)
247
- argument :order, input(:order)
248
-
249
- run do |args, _context|
250
- order = args[:order]
251
- primary_card_failed = args[:primary_attempt][:primary_card_failed]
252
-
253
- return { skipped: true } unless primary_card_failed
254
-
255
- result = process_payment_with_card(order, order.backup_card)
256
- if result.success?
257
- { payment_result: result, card_used: :backup }
258
- else
259
- raise "All payment attempts failed"
260
- end
261
- end
262
- end
263
-
264
- step :process_successful_payment do
265
- argument :attempt_result, result(:attempt_backup_card)
266
-
267
- run do |args, _context|
268
- payment_result = args[:attempt_result][:payment_result]
269
- card_used = args[:attempt_result][:card_used]
270
-
271
- # Record successful payment
272
- PaymentRecord.create!(
273
- charge_id: payment_result.id,
274
- card_used: card_used,
275
- amount: payment_result.amount
276
- )
277
-
278
- { payment_recorded: true }
279
- end
280
- end
281
-
282
- private
283
-
284
- def process_payment_with_card(order, card)
285
- PaymentGateway.charge(
286
- amount: order.total,
287
- card_token: card.token,
288
- description: "Order ##{order.id}"
289
- )
290
- end
291
- end
292
- ```
293
-
294
- ### Subscription Payment Processing
295
-
296
- ```ruby
297
- class SubscriptionPaymentReactor < RubyReactor::Reactor
298
- async true
299
-
300
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
301
-
302
- input :subscription_id do
303
- required(:subscription_id).filled(:string)
304
- end
305
-
306
- step :validate_subscription do
307
- argument :subscription_id, input(:subscription_id)
308
-
309
- run do |args, _context|
310
- subscription_id = args[:subscription_id]
311
-
312
- subscription = Subscription.find(subscription_id)
313
- raise "Subscription not found" unless subscription
314
- raise "Subscription inactive" unless subscription.active?
315
-
316
- { subscription: subscription }
317
- end
318
- end
319
-
320
- step :calculate_proration do
321
- argument :validation_data, result(:validate_subscription)
322
-
323
- run do |args, _context|
324
- subscription = args[:validation_data][:subscription]
325
-
326
- # Calculate prorated amount for billing period
327
- proration = BillingService.calculate_proration(subscription)
328
- { proration_amount: proration.amount, billing_period: proration.period }
329
- end
330
- end
331
-
332
- step :charge_subscription do
333
- argument :validation_data, result(:validate_subscription)
334
- argument :proration_data, result(:calculate_proration)
335
-
336
- run do |args, _context|
337
- subscription = args[:validation_data][:subscription]
338
- proration_amount = args[:proration_data][:proration_amount]
339
- billing_period = args[:proration_data][:billing_period]
340
-
341
- charge_result = PaymentGateway.charge_subscription(
342
- customer_id: subscription.customer.stripe_id,
343
- amount: proration_amount,
344
- description: "Subscription #{subscription.id} - #{billing_period}"
345
- )
346
-
347
- raise "Subscription charge failed" unless charge_result.success?
348
-
349
- { charge_id: charge_result.id }
350
- end
351
-
352
- undo do |result, _args, _ctx|
353
- PaymentGateway.refund_subscription_charge(result[:charge_id]) if result[:charge_id]
354
- Success()
355
- end
356
- end
357
-
358
- step :update_billing_record do
359
- argument :validation_data, result(:validate_subscription)
360
- argument :proration_data, result(:calculate_proration)
361
- argument :charge_data, result(:charge_subscription)
362
-
363
- run do |args, _context|
364
- subscription = args[:validation_data][:subscription]
365
- charge_id = args[:charge_data][:charge_id]
366
- billing_period = args[:proration_data][:billing_period]
367
-
368
- BillingRecord.create!(
369
- subscription: subscription,
370
- charge_id: charge_id,
371
- billing_period: billing_period,
372
- status: :paid
373
- )
374
-
375
- { billing_recorded: true }
376
- end
377
- end
378
- end
379
- ```
380
-
381
- ## Error Handling Patterns
382
-
383
- ### Payment Gateway Timeouts
384
-
385
- ```ruby
386
- class TimeoutHandlingReactor < RubyReactor::Reactor
387
- step :handle_timeout do
388
- retries max_attempts: 5, backoff: :exponential, base_delay: 2.seconds
389
-
390
- run do
391
- begin
392
- Timeout.timeout(10.seconds) do
393
- PaymentGateway.charge(amount: 100, card_token: token)
394
- end
395
- rescue Timeout::Error
396
- raise PaymentTimeoutError.new("Gateway timeout")
397
- end
398
- end
399
- end
400
- end
401
-
402
- class PaymentTimeoutError < StandardError
403
- def retryable?
404
- true
405
- end
406
- end
407
- ```
408
-
409
- ### Insufficient Funds Handling
410
-
411
- ```ruby
412
- class InsufficientFundsReactor < RubyReactor::Reactor
413
- step :handle_insufficient_funds do
414
- run do
415
- result = PaymentGateway.charge(amount: 100, card_token: token)
416
-
417
- if result.error_code == 'insufficient_funds'
418
- # Trigger alternative payment flow
419
- notify_customer_insufficient_funds
420
- raise NonRetryablePaymentError.new("Insufficient funds")
421
- end
422
-
423
- result
424
- end
425
- end
426
- end
427
-
428
- class NonRetryablePaymentError < StandardError
429
- def retryable?
430
- false
431
- end
432
- end
433
- ```
434
-
435
- ## Testing Payment Reactors
436
-
437
- ```ruby
438
- RSpec.describe PaymentProcessingReactor do
439
- let(:valid_payment_params) do
440
- {
441
- amount: 100.00,
442
- currency: 'USD',
443
- card_token: 'tok_1234567890'
444
- }
445
- end
446
-
447
- context "successful payment" do
448
- it "completes all payment steps" do
449
- allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
450
- allow(PaymentGateway).to receive(:pre_authorize).and_return(successful_auth)
451
- allow(PaymentGateway).to receive(:charge).and_return(successful_charge)
452
- allow(EmailService).to receive(:send_payment_receipt).and_return(successful_email)
453
-
454
- subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
455
-
456
- expect(subject).to be_success
457
- expect(subject.step_result(:charge_payment)[:charge_id]).to be_present
458
- end
459
- end
460
-
461
- context "fraud detection" do
462
- it "blocks high-risk payments" do
463
- allow(FraudDetectionService).to receive(:analyze).and_return(0.9)
464
-
465
- subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
466
-
467
- expect(subject).to be_failure
468
- expect(subject.error).to include("High fraud risk")
469
- end
470
- end
471
-
472
- context "payment gateway failure" do
473
- it "retries pre-authorization" do
474
- allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
475
- allow(PaymentGateway).to receive(:pre_authorize)
476
- .and_raise(PaymentGateway::TimeoutError)
477
- .and_raise(PaymentGateway::TimeoutError)
478
- .and_return(successful_auth)
479
-
480
- expect(PaymentGateway).to receive(:pre_authorize).exactly(3).times
481
-
482
- subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
483
-
484
- expect(subject).to be_success
485
- expect(subject).to have_retried_step(:pre_authorize).times(2)
486
- end
487
- end
488
-
489
- context "compensation" do
490
- it "refunds on final charge failure" do
491
- allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
492
- allow(PaymentGateway).to receive(:pre_authorize).and_return(successful_auth)
493
- allow(PaymentGateway).to receive(:charge).and_raise(PaymentGateway::DeclineError)
494
-
495
- expect(PaymentGateway).to receive(:void_authorization)
496
-
497
- subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
498
-
499
- expect(subject).to be_failure
500
- end
501
- end
502
- end
503
- ```
504
-
505
- ## Monitoring and Alerting
506
-
507
- ```ruby
508
- # Key metrics for payment processing
509
- PAYMENT_METRICS = {
510
- payment_success_rate: "Percentage of successful payments",
511
- fraud_detection_rate: "Percentage of payments flagged as fraud",
512
- average_payment_time: "Average time to process payment",
513
- retry_rate: "Percentage of payments requiring retries",
514
- chargeback_rate: "Rate of payment chargebacks"
515
- }
516
-
517
- # Alerts
518
- PAYMENT_ALERTS = {
519
- high_failure_rate: "Payment success rate below 95%",
520
- high_fraud_rate: "Fraud detection rate above 10%",
521
- slow_processing: "Average payment time above 30 seconds"
522
- }
523
- ```
524
-
525
- ## Security Considerations
526
-
527
- 1. **PCI Compliance**: Never log full card details
528
- 2. **Tokenization**: Use payment provider tokens, not raw card data
529
- 3. **Idempotency Keys**: Prevent duplicate charges
530
- 4. **Rate Limiting**: Implement per-customer rate limits
531
- 5. **Audit Logging**: Log all payment attempts (without sensitive data)
532
-
533
- ## Performance Optimization
534
-
535
- ### Connection Pooling
536
-
537
- ```ruby
538
- # Configure payment gateway connection pooling
539
- PaymentGateway.configure do |config|
540
- config.connection_pool_size = 10
541
- config.connection_timeout = 5.seconds
542
- end
543
- ```
544
-
545
- ### Caching
546
-
547
- ```ruby
548
- # Cache fraud detection models
549
- Rails.cache.fetch("fraud_model_#{Date.today}", expires_in: 1.day) do
550
- FraudDetectionService.load_model
551
- end
552
- ```
553
-
554
- ### Async Processing
555
-
556
- ```ruby
557
- # Use async for non-critical steps
558
- step :send_receipt do
559
- async true # Don't block payment completion on email delivery
560
-
561
- run do
562
- # Email sending logic
563
- end
564
- end
565
- ```