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,748 +0,0 @@
1
- # Inventory Management Reactor Example
2
-
3
- This example demonstrates inventory management with reservations, stock updates, and supplier integration.
4
-
5
- ## Overview
6
-
7
- The InventoryManagementReactor handles complex inventory operations:
8
-
9
- 1. **Check Availability**: Verify stock levels
10
- 2. **Reserve Stock**: Temporarily hold inventory
11
- 3. **Update Quantities**: Modify stock levels
12
- 4. **Log Transactions**: Record inventory changes
13
- 5. **Trigger Replenishment**: Auto-order low stock items
14
- 6. **Send Notifications**: Alert relevant parties
15
-
16
- ### Inventory Management Workflow
17
-
18
- ```mermaid
19
- graph TD
20
- A[Inventory Request] --> B[validate_request]
21
- B --> C{Valid<br/>Request?}
22
- C -->|No| D[Fail: Validation Error]
23
- C -->|Yes| E[check_availability]
24
- E --> F{Stock<br/>Available?}
25
- F -->|No| G[Fail: Insufficient Stock]
26
- F -->|Yes| H[acquire_lock]
27
- H --> I{Lock<br/>Acquired?}
28
- I -->|No| J[Fail: Lock Contention]
29
- I -->|Yes| K[perform_operation]
30
- K --> L{Operation<br/>Success?}
31
- L -->|No| M[Compensate: Release Lock]
32
- L -->|Yes| N[release_lock]
33
- N --> O[log_transaction]
34
- O --> P[check_replenishment]
35
- P --> Q{Replenishment<br/>Needed?}
36
- Q -->|Yes| R[Trigger Replenishment Job]
37
- Q -->|No| S[send_notifications]
38
- S --> T[Success: Operation Complete]
39
-
40
- R --> S
41
- ```
42
-
43
- ## Implementation
44
-
45
- ```ruby
46
- class InventoryManagementReactor < RubyReactor::Reactor
47
- async true
48
-
49
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.second
50
-
51
- step :validate_request do
52
- validate_args do
53
- required(:product_id).filled(:string)
54
- required(:quantity).filled(:integer, gt?: 0)
55
- required(:operation).filled(:string, included_in?: ['reserve', 'release', 'consume'])
56
- end
57
-
58
- run do |product_id:, quantity:, operation:, **|
59
- product = Product.find_by(id: product_id)
60
- raise "Product not found" unless product
61
-
62
- Success({ product: product, operation: operation })
63
- end
64
- end
65
-
66
- step :check_availability do
67
- argument :request_data, result(:validate_request)
68
-
69
- run do |args, _context|
70
- product = args[:request_data][:product]
71
- quantity = args[:request_data][:quantity]
72
- operation = args[:request_data][:operation]
73
-
74
- case operation
75
- when 'consume'
76
- raise "Insufficient stock" if product.inventory_count < quantity
77
- when 'reserve'
78
- available = product.inventory_count - product.reserved_count
79
- raise "Insufficient available stock" if available < quantity
80
- end
81
-
82
- Success({ availability_checked: true })
83
- end
84
- end
85
-
86
- step :acquire_lock do
87
- argument :request_data, result(:validate_request)
88
- argument :availability_data, result(:check_availability)
89
-
90
- run do |args, _context|
91
- product = args[:request_data][:product]
92
-
93
- # Acquire distributed lock to prevent race conditions
94
- lock_key = "inventory_lock:#{product.id}"
95
-
96
- acquired = RedisLock.acquire(lock_key, ttl: 30.seconds)
97
- raise "Failed to acquire inventory lock" unless acquired
98
-
99
- Success({ lock_key: lock_key })
100
- end
101
-
102
- compensate do |args, _context|
103
- lock_key = args[:availability_data][:lock_key] || args[:lock_key]
104
- # Release lock on failure
105
- RedisLock.release(lock_key) if lock_key
106
- end
107
- end
108
-
109
- step :perform_operation do
110
- argument :request_data, result(:validate_request)
111
- argument :lock_data, result(:acquire_lock)
112
-
113
- run do |args, _context|
114
- product = args[:request_data][:product]
115
- quantity = args[:request_data][:quantity]
116
- operation = args[:request_data][:operation]
117
-
118
- case operation
119
- when 'reserve'
120
- product.increment!(:reserved_count, quantity)
121
- Success({ operation_completed: true, new_reserved: product.reserved_count })
122
- when 'release'
123
- product.decrement!(:reserved_count, quantity)
124
- Success({ operation_completed: true, new_reserved: product.reserved_count })
125
- when 'consume'
126
- product.decrement!(:inventory_count, quantity)
127
- Success({ operation_completed: true, new_inventory: product.inventory_count })
128
- end
129
- end
130
-
131
- compensate do |args, _context|
132
- product = args[:request_data][:product]
133
- quantity = args[:request_data][:quantity]
134
- operation = args[:request_data][:operation]
135
-
136
- # Reverse the operation
137
- case operation
138
- when 'reserve'
139
- product.decrement!(:reserved_count, quantity)
140
- when 'release'
141
- product.increment!(:reserved_count, quantity)
142
- when 'consume'
143
- product.increment!(:inventory_count, quantity)
144
- end
145
- end
146
- end
147
-
148
- step :release_lock do
149
- argument :lock_data, result(:acquire_lock)
150
- argument :operation_data, result(:perform_operation)
151
-
152
- run do |args, _context|
153
- lock_key = args[:lock_data][:lock_key]
154
-
155
- RedisLock.release(lock_key)
156
- Success({ lock_released: true })
157
- end
158
- end
159
-
160
- step :log_transaction do
161
- argument :request_data, result(:validate_request)
162
- argument :release_data, result(:release_lock)
163
-
164
- run do |args, _context|
165
- product = args[:request_data][:product]
166
- quantity = args[:request_data][:quantity]
167
- operation = args[:request_data][:operation]
168
-
169
- InventoryTransaction.create!(
170
- product: product,
171
- operation: operation,
172
- quantity: quantity,
173
- performed_at: Time.current
174
- )
175
-
176
- Success({ transaction_logged: true })
177
- end
178
- end
179
-
180
- step :check_replenishment do
181
- argument :request_data, result(:validate_request)
182
- argument :log_data, result(:log_transaction)
183
-
184
- run do |args, _context|
185
- product = args[:request_data][:product]
186
-
187
- if product.inventory_count <= product.reorder_point
188
- # Trigger replenishment process
189
- ReplenishmentJob.perform_later(product.id)
190
- Success({ replenishment_triggered: true })
191
- else
192
- Success({ replenishment_checked: true })
193
- end
194
- end
195
- end
196
-
197
- step :send_notifications do
198
- argument :request_data, result(:validate_request)
199
- argument :replenishment_data, result(:check_replenishment)
200
-
201
- retries max_attempts: 3, backoff: :linear, base_delay: 5.seconds
202
-
203
- run do |args, _context|
204
- product = args[:request_data][:product]
205
- operation = args[:request_data][:operation]
206
- quantity = args[:request_data][:quantity]
207
- replenishment_triggered = args[:replenishment_data][:replenishment_triggered]
208
-
209
- notifications = []
210
-
211
- # Notify warehouse staff for significant changes
212
- if quantity > 10
213
- notifications << NotificationService.notify_warehouse(
214
- quantity: quantity
215
- )
216
- end
217
-
218
- # Notify managers for low stock
219
- if replenishment_triggered
220
- notifications << NotificationService.notify_managers(
221
- product: product,
222
- message: "Low stock alert: #{product.inventory_count} remaining"
223
- )
224
- end
225
-
226
- Success({ notifications_sent: notifications.all?(&:success?) })
227
- end
228
- end
229
- end
230
- ```
231
-
232
- ## Advanced Inventory Scenarios
233
-
234
- ### Bulk Inventory Operations
235
-
236
- ```ruby
237
- class BulkInventoryReactor < RubyReactor::Reactor
238
- async true
239
-
240
- step :validate_bulk_request do
241
- validate_args do
242
- required(:operations).filled(:array, max_size?: 100) do
243
- each do
244
- hash do
245
- required(:product_id).filled(:string)
246
- required(:quantity).filled(:integer, gt?: 0)
247
- required(:operation).filled(:string, included_in?: ['reserve', 'release', 'consume'])
248
- end
249
- end
250
- end
251
- end
252
-
253
- run do |operations:, **|
254
- validated_ops = operations.map do |op|
255
- product = Product.find(op[:product_id])
256
- raise "Product #{op[:product_id]} not found" unless product
257
-
258
- op.merge(product: product)
259
- end
260
-
261
- Success({ validated_operations: validated_ops })
262
- end
263
- end
264
-
265
- step :execute_operations do
266
- argument :bulk_data, result(:validate_bulk_request)
267
-
268
- run do |args, _context|
269
- validated_operations = args[:bulk_data][:validated_operations]
270
-
271
- results = []
272
-
273
- validated_operations.each do |op|
274
- begin
275
- # Execute each operation in sequence to maintain consistency
276
- result = InventoryManagementReactor.run(
277
- product_id: op[:product_id],
278
- quantity: op[:quantity],
279
- operation: op[:operation]
280
- )
281
-
282
- if result.success?
283
- results << { success: true, operation: op }
284
- else
285
- results << { success: false, operation: op, error: result.error.to_s }
286
- end
287
- rescue => e
288
- results << { success: false, operation: op, error: e.message }
289
- end
290
- end
291
-
292
- successful_count = results.count { |r| r[:success] }
293
-
294
- if successful_count == 0
295
- raise "All operations failed"
296
- elsif successful_count < results.size
297
- # Partial success - log warnings but don't fail
298
- Rails.logger.warn("Bulk inventory: #{successful_count}/#{results.size} operations succeeded")
299
- end
300
-
301
- Success({ bulk_results: results, partial_success: successful_count < results.size })
302
- end
303
- end
304
- end
305
- ```
306
-
307
- ### Inventory Transfer Between Locations
308
-
309
- ```ruby
310
- class InventoryTransferReactor < RubyReactor::Reactor
311
- async true
312
-
313
- step :validate_transfer do
314
- validate_args do
315
- required(:from_location_id).filled(:string)
316
- required(:to_location_id).filled(:string)
317
- required(:product_id).filled(:string)
318
- required(:quantity).filled(:integer, gt?: 0)
319
- end
320
-
321
- run do |from_location_id:, to_location_id:, product_id:, quantity:, **|
322
- from_location = Location.find(from_location_id)
323
- to_location = Location.find(to_location_id)
324
- product = Product.find(product_id)
325
-
326
- raise "Invalid locations" unless from_location && to_location
327
- raise "Same location" if from_location_id == to_location_id
328
- raise "Product not found" unless product
329
-
330
- # Check source location has enough stock
331
- source_inventory = LocationInventory.find_by(
332
- location: from_location,
333
- product: product
334
- )
335
- raise "Insufficient stock at source" unless source_inventory&.quantity&.>= quantity
336
-
337
- Success({
338
- from_location: from_location,
339
- to_location: to_location,
340
- product: product,
341
- quantity: quantity
342
- })
343
- end
344
- end
345
-
346
- step :reserve_source do
347
- argument :transfer_data, result(:validate_transfer)
348
-
349
- run do |args, _context|
350
- from_location = args[:transfer_data][:from_location]
351
- product = args[:transfer_data][:product]
352
- quantity = args[:transfer_data][:quantity]
353
-
354
- # Reserve inventory at source location
355
- reservation = InventoryService.reserve_at_location(
356
- location: from_location,
357
- product: product,
358
- quantity: quantity
359
- )
360
-
361
- raise "Source reservation failed" unless reservation
362
-
363
- Success({ source_reservation_id: reservation.id })
364
- end
365
-
366
- compensate do |args, _context|
367
- source_reservation_id = args[:transfer_data][:source_reservation_id] || args[:source_reservation_id]
368
- InventoryService.release_reservation(source_reservation_id)
369
- end
370
- end
371
-
372
- step :prepare_destination do
373
- argument :transfer_data, result(:validate_transfer)
374
- argument :reservation_data, result(:reserve_source)
375
-
376
- run do |args, _context|
377
- to_location = args[:transfer_data][:to_location]
378
- product = args[:transfer_data][:product]
379
- quantity = args[:transfer_data][:quantity]
380
-
381
- # Ensure destination location can accept the inventory
382
- destination_inventory = LocationInventory.find_or_create_by(
383
- location: to_location,
384
- product: product
385
- )
386
-
387
- Success({ destination_prepared: true })
388
- end
389
- end
390
-
391
- step :execute_transfer do
392
- argument :transfer_data, result(:validate_transfer)
393
- argument :reservation_data, result(:reserve_source)
394
- argument :destination_data, result(:prepare_destination)
395
-
396
- run do |args, _context|
397
- from_location = args[:transfer_data][:from_location]
398
- to_location = args[:transfer_data][:to_location]
399
- product = args[:transfer_data][:product]
400
- quantity = args[:transfer_data][:quantity]
401
- source_reservation_id = args[:reservation_data][:source_reservation_id]
402
-
403
- # Atomically transfer inventory
404
- InventoryService.transfer_inventory(
405
- from_location: from_location,
406
- to_location: to_location,
407
- product: product,
408
- quantity: quantity,
409
- reservation_id: source_reservation_id
410
- )
411
-
412
- Success({ transfer_completed: true })
413
- end
414
-
415
- compensate do |args, _context|
416
- from_location = args[:transfer_data][:from_location]
417
- to_location = args[:transfer_data][:to_location]
418
- product = args[:transfer_data][:product]
419
- quantity = args[:transfer_data][:quantity]
420
-
421
- # Reverse the transfer - this is complex and might require manual intervention
422
- Rails.logger.error("Transfer compensation needed for #{product.id} x #{quantity}")
423
- # Trigger manual review process
424
- end
425
- end
426
-
427
- step :log_transfer do
428
- argument :transfer_data, result(:validate_transfer)
429
- argument :transfer_result, result(:execute_transfer)
430
-
431
- run do |args, _context|
432
- from_location = args[:transfer_data][:from_location]
433
- to_location = args[:transfer_data][:to_location]
434
- product = args[:transfer_data][:product]
435
- quantity = args[:transfer_data][:quantity]
436
-
437
- InventoryTransfer.create!(
438
- from_location: from_location,
439
- to_location: to_location,
440
- product: product,
441
- quantity: quantity,
442
- transferred_at: Time.current
443
- )
444
-
445
- Success({ transfer_logged: true })
446
- end
447
- end
448
- end
449
- ```
450
-
451
- ## Supplier Integration
452
-
453
- ### Automatic Replenishment
454
-
455
- ```ruby
456
- class ReplenishmentReactor < RubyReactor::Reactor
457
- async true
458
-
459
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 5.minutes
460
-
461
- step :check_supplier_availability do
462
- validate_args do
463
- required(:product_id).filled(:string)
464
- end
465
-
466
- run do |product_id:, **|
467
- product = Product.find(product_id)
468
- supplier = product.supplier
469
-
470
- raise "No supplier configured" unless supplier
471
- raise "Supplier inactive" unless supplier.active?
472
-
473
- Success({ product: product, supplier: supplier })
474
- end
475
- end
476
-
477
- step :calculate_order_quantity do
478
- argument :supplier_data, result(:check_supplier_availability)
479
-
480
- run do |args, _context|
481
- product = args[:supplier_data][:product]
482
- supplier = args[:supplier_data][:supplier]
483
-
484
- # Calculate optimal order quantity
485
- current_stock = product.inventory_count
486
- reorder_point = product.reorder_point
487
- max_stock = product.max_stock_level
488
-
489
- # Order enough to reach max stock
490
- order_quantity = [max_stock - current_stock, supplier.min_order_quantity].max
491
-
492
- # Check supplier constraints
493
- order_quantity = [order_quantity, supplier.max_order_quantity].min
494
-
495
- raise "Invalid order quantity" if order_quantity <= 0
496
-
497
- Success({ order_quantity: order_quantity })
498
- end
499
- end
500
-
501
- step :check_supplier_inventory do
502
- argument :supplier_data, result(:check_supplier_availability)
503
- argument :quantity_data, result(:calculate_order_quantity)
504
-
505
- run do |args, _context|
506
- supplier = args[:supplier_data][:supplier]
507
- product = args[:supplier_data][:product]
508
- order_quantity = args[:quantity_data][:order_quantity]
509
-
510
- # Query supplier API for availability
511
- supplier_response = SupplierAPI.check_availability(
512
- supplier_id: supplier.id,
513
- product_sku: product.sku,
514
- quantity: order_quantity
515
- )
516
-
517
- raise "Product not available from supplier" unless supplier_response.available?
518
-
519
- actual_available = supplier_response.available_quantity
520
- if actual_available < order_quantity
521
- # Adjust order quantity
522
- order_quantity = actual_available
523
- end
524
-
525
- Success({ adjusted_quantity: order_quantity, supplier_available: true })
526
- end
527
- end
528
-
529
- step :place_supplier_order do
530
- argument :supplier_data, result(:check_supplier_availability)
531
- argument :inventory_data, result(:check_supplier_inventory)
532
-
533
- run do |args, _context|
534
- supplier = args[:supplier_data][:supplier]
535
- product = args[:supplier_data][:product]
536
- adjusted_quantity = args[:inventory_data][:adjusted_quantity]
537
-
538
- order_response = SupplierAPI.place_order(
539
- supplier_id: supplier.id,
540
- items: [{
541
- sku: product.sku,
542
- quantity: adjusted_quantity,
543
- unit_price: product.supplier_price
544
- }]
545
- )
546
-
547
- raise "Supplier order failed" unless order_response.success?
548
-
549
- Success({ supplier_order_id: order_response.order_id, order_placed: true })
550
- end
551
-
552
- compensate do |args, _context|
553
- supplier_order_id = args[:inventory_data][:supplier_order_id] || args[:supplier_order_id]
554
- # Cancel the supplier order
555
- SupplierAPI.cancel_order(supplier_order_id) if supplier_order_id
556
- end
557
- end
558
-
559
- step :record_purchase_order do
560
- argument :supplier_data, result(:check_supplier_availability)
561
- argument :inventory_data, result(:check_supplier_inventory)
562
- argument :order_data, result(:place_supplier_order)
563
-
564
- run do |args, _context|
565
- product = args[:supplier_data][:product]
566
- supplier = args[:supplier_data][:supplier]
567
- adjusted_quantity = args[:inventory_data][:adjusted_quantity]
568
- supplier_order_id = args[:order_data][:supplier_order_id]
569
-
570
- purchase_order = PurchaseOrder.create!(
571
- supplier: supplier,
572
- supplier_order_id: supplier_order_id,
573
- status: :ordered,
574
- items_attributes: [{
575
- product: product,
576
- quantity: adjusted_quantity,
577
- unit_price: product.supplier_price,
578
- total_price: adjusted_quantity * product.supplier_price
579
- }]
580
- )
581
-
582
- Success({ purchase_order_id: purchase_order.id })
583
- end
584
- end
585
-
586
- step :schedule_delivery_tracking do
587
- argument :purchase_data, result(:record_purchase_order)
588
-
589
- run do |args, _context|
590
- supplier_order_id = args[:order_data][:supplier_order_id]
591
-
592
- # Schedule job to track delivery status
593
- DeliveryTrackingJob.set(wait: 1.hour).perform_later(supplier_order_id)
594
-
595
- Success({ tracking_scheduled: true })
596
- end
597
- end
598
- end
599
- ```
600
-
601
- ## Testing Inventory Operations
602
-
603
- ```ruby
604
- RSpec.describe InventoryManagementReactor do
605
- let(:product) { create(:product, inventory_count: 100, reserved_count: 0) }
606
-
607
- context "successful reservation" do
608
- it "reserves inventory correctly" do
609
- result = InventoryManagementReactor.run(
610
- product_id: product.id,
611
- quantity: 10,
612
- operation: 'reserve'
613
- )
614
-
615
- expect(result).to be_success
616
- product.reload
617
- expect(product.reserved_count).to eq(10)
618
- expect(product.available_count).to eq(90)
619
- end
620
- end
621
-
622
- context "insufficient stock" do
623
- it "fails when trying to consume more than available" do
624
- result = InventoryManagementReactor.run(
625
- product_id: product.id,
626
- quantity: 150,
627
- operation: 'consume'
628
- )
629
-
630
- expect(result).to be_failure
631
- expect(result.error).to include("Insufficient stock")
632
- end
633
- end
634
-
635
- context "concurrent operations" do
636
- it "handles race conditions with locking" do
637
- # Simulate concurrent operations
638
- threads = 5.times.map do
639
- Thread.new do
640
- InventoryManagementReactor.run(
641
- product_id: product.id,
642
- quantity: 1,
643
- operation: 'reserve'
644
- )
645
- end
646
- end
647
-
648
- threads.each(&:join)
649
-
650
- product.reload
651
- expect(product.reserved_count).to eq(5) # All operations succeeded
652
- end
653
- end
654
-
655
- context "compensation" do
656
- it "releases lock and reverses operation on failure" do
657
- allow_any_instance_of(InventoryManagementReactor).to receive(:perform_operation)
658
- .and_raise("Simulated failure")
659
-
660
- initial_reserved = product.reserved_count
661
-
662
- result = InventoryManagementReactor.run(
663
- product_id: product.id,
664
- quantity: 5,
665
- operation: 'reserve'
666
- )
667
-
668
- expect(result).to be_failure
669
- product.reload
670
- expect(product.reserved_count).to eq(initial_reserved) # No change
671
- end
672
- end
673
- end
674
- ```
675
-
676
- ## Performance Optimization
677
-
678
- ### Database Optimization
679
-
680
- ```ruby
681
- # Use database constraints for inventory integrity
682
- class Product < ApplicationRecord
683
- # Ensure inventory never goes negative
684
- validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
685
-
686
- # Use optimistic locking
687
- # validates :lock_version
688
- end
689
- ```
690
-
691
- ### Caching Strategy
692
-
693
- ```ruby
694
- class InventoryCache
695
- def self.get_availability(product_id)
696
- Rails.cache.fetch("inventory:#{product_id}", expires_in: 5.minutes) do
697
- product = Product.find(product_id)
698
- {
699
- inventory_count: product.inventory_count,
700
- reserved_count: product.reserved_count,
701
- available: product.inventory_count - product.reserved_count
702
- }
703
- end
704
- end
705
-
706
- def self.invalidate(product_id)
707
- Rails.cache.delete("inventory:#{product_id}")
708
- end
709
- end
710
- ```
711
-
712
- ### Batch Operations
713
-
714
- ```ruby
715
- class BatchInventoryUpdateReactor < RubyReactor::Reactor
716
- step :batch_update do
717
- run do |updates:, **|
718
- # Use single transaction for batch updates
719
- Product.transaction do
720
- updates.each do |update|
721
- product = Product.find(update[:product_id])
722
- product.increment!(:inventory_count, update[:quantity])
723
- end
724
- end
725
-
726
- Success({ batch_completed: true })
727
- end
728
- end
729
- end
730
- ```
731
-
732
- ## Monitoring and Alerting
733
-
734
- ```ruby
735
- INVENTORY_METRICS = {
736
- stockout_rate: "Percentage of products out of stock",
737
- reservation_failure_rate: "Rate of failed reservations",
738
- average_operation_time: "Average time for inventory operations",
739
- lock_contention_rate: "Percentage of operations hitting lock contention",
740
- replenishment_delay: "Average time to replenish low stock items"
741
- }
742
-
743
- INVENTORY_ALERTS = {
744
- high_stockout_rate: "Stockout rate above 5%",
745
- high_lock_contention: "Lock contention above 10%",
746
- replenishment_failures: "Failed replenishment orders in last hour"
747
- }
748
- ```