ruby_reactor 0.3.1 → 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 (70) 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 +194 -9
  7. data/lib/ruby_reactor/configuration.rb +18 -1
  8. data/lib/ruby_reactor/context_serializer.rb +10 -1
  9. data/lib/ruby_reactor/dsl/lockable.rb +130 -0
  10. data/lib/ruby_reactor/executor/result_handler.rb +19 -0
  11. data/lib/ruby_reactor/executor/step_executor.rb +5 -0
  12. data/lib/ruby_reactor/executor.rb +145 -2
  13. data/lib/ruby_reactor/lock.rb +92 -0
  14. data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
  15. data/lib/ruby_reactor/period.rb +67 -0
  16. data/lib/ruby_reactor/rate_limit.rb +74 -0
  17. data/lib/ruby_reactor/reactor.rb +1 -0
  18. data/lib/ruby_reactor/rspec/matchers.rb +171 -4
  19. data/lib/ruby_reactor/semaphore.rb +58 -0
  20. data/lib/ruby_reactor/sidekiq_workers/worker.rb +128 -9
  21. data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
  22. data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
  23. data/lib/ruby_reactor/version.rb +1 -1
  24. data/lib/ruby_reactor.rb +49 -0
  25. metadata +13 -51
  26. data/documentation/DAG.md +0 -457
  27. data/documentation/README.md +0 -123
  28. data/documentation/async_reactors.md +0 -369
  29. data/documentation/composition.md +0 -199
  30. data/documentation/core_concepts.md +0 -662
  31. data/documentation/data_pipelines.md +0 -230
  32. data/documentation/examples/inventory_management.md +0 -749
  33. data/documentation/examples/order_processing.md +0 -365
  34. data/documentation/examples/payment_processing.md +0 -654
  35. data/documentation/getting_started.md +0 -224
  36. data/documentation/images/failed_order_processing.png +0 -0
  37. data/documentation/images/payment_workflow.png +0 -0
  38. data/documentation/interrupts.md +0 -161
  39. data/documentation/retry_configuration.md +0 -357
  40. data/documentation/testing.md +0 -812
  41. data/gui/.gitignore +0 -24
  42. data/gui/README.md +0 -73
  43. data/gui/eslint.config.js +0 -23
  44. data/gui/index.html +0 -13
  45. data/gui/package-lock.json +0 -5925
  46. data/gui/package.json +0 -46
  47. data/gui/postcss.config.js +0 -6
  48. data/gui/public/vite.svg +0 -1
  49. data/gui/src/App.css +0 -42
  50. data/gui/src/App.tsx +0 -51
  51. data/gui/src/assets/react.svg +0 -1
  52. data/gui/src/components/DagVisualizer.tsx +0 -424
  53. data/gui/src/components/Dashboard.tsx +0 -163
  54. data/gui/src/components/ErrorBoundary.tsx +0 -47
  55. data/gui/src/components/ReactorDetail.tsx +0 -135
  56. data/gui/src/components/StepInspector.tsx +0 -492
  57. data/gui/src/components/__tests__/DagVisualizer.test.tsx +0 -140
  58. data/gui/src/components/__tests__/ReactorDetail.test.tsx +0 -111
  59. data/gui/src/components/__tests__/StepInspector.test.tsx +0 -408
  60. data/gui/src/globals.d.ts +0 -7
  61. data/gui/src/index.css +0 -14
  62. data/gui/src/lib/utils.ts +0 -13
  63. data/gui/src/main.tsx +0 -14
  64. data/gui/src/test/setup.ts +0 -11
  65. data/gui/tailwind.config.js +0 -11
  66. data/gui/tsconfig.app.json +0 -28
  67. data/gui/tsconfig.json +0 -7
  68. data/gui/tsconfig.node.json +0 -26
  69. data/gui/vite.config.ts +0 -8
  70. data/gui/vitest.config.ts +0 -13
@@ -1,224 +0,0 @@
1
- # Getting Started with RubyReactor
2
-
3
- This guide will help you get started with RubyReactor, from installation to your first reactor.
4
-
5
- ## Installation
6
-
7
- Add RubyReactor to your Gemfile:
8
-
9
- ```ruby
10
- gem 'ruby_reactor'
11
- ```
12
-
13
- Or install directly:
14
-
15
- ```bash
16
- gem install ruby_reactor
17
- ```
18
-
19
- ## Your First Reactor
20
-
21
- Let's create a simple order processing reactor:
22
-
23
- ```ruby
24
- require 'ruby_reactor'
25
-
26
- class OrderProcessingReactor < RubyReactor::Reactor
27
- step :validate_order do
28
- validate_args do
29
- required(:order_id).filled(:string)
30
- end
31
-
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?
37
-
38
- Success({ order: order })
39
- end
40
- end
41
-
42
- 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?
47
-
48
- Success({ payment_id: payment_result.id })
49
- end
50
- end
51
-
52
- step :update_inventory do
53
- run do |order:, **|
54
- # Update inventory for each item
55
- order.items.each do |item|
56
- InventoryService.decrement(item.product_id, item.quantity)
57
- end
58
-
59
- Success({ inventory_updated: true })
60
- end
61
- end
62
-
63
- 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? })
73
- end
74
- end
75
- end
76
- ```
77
-
78
- ## Executing a Reactor
79
-
80
- ### Synchronous Execution
81
-
82
- ```ruby
83
- # Run the reactor synchronously
84
- result = OrderProcessingReactor.run(order_id: 123)
85
-
86
- if result.success?
87
- puts "Order processed successfully!"
88
- puts "Results: #{result.step_results}"
89
- else
90
- puts "Order processing failed: #{result.error}"
91
- end
92
- ```
93
-
94
- ### Asynchronous Execution
95
-
96
- For async execution, you need Sidekiq configured. Mark the reactor as async:
97
-
98
- ```ruby
99
- class OrderProcessingReactor < RubyReactor::Reactor
100
- async true # Enable full reactor async
101
-
102
- # ... steps defined above
103
- end
104
-
105
- # Run asynchronously
106
- async_result = OrderProcessingReactor.run(order_id: 123)
107
- # Returns immediately with AsyncResult
108
- # Check status later with async_result.status
109
- ```
110
-
111
- ## Understanding Results
112
-
113
- RubyReactor returns detailed execution results:
114
-
115
- ```ruby
116
- result = OrderProcessingReactor.run(order_id: 123)
117
-
118
- # Check overall success
119
- result.success? # => true/false
120
-
121
- # Access step results
122
- result.step_results[:validate_order] # => { order: #<Order> }
123
- result.step_results[:process_payment] # => { payment_id: "pay_123" }
124
-
125
- # Access intermediate results
126
- result.intermediate_results # => Hash of all step outputs
127
-
128
- # Check completed steps
129
- result.completed_steps # => Set of completed step names
130
-
131
- # Error information (if failed)
132
- result.error # => Exception that caused failure
133
- ```
134
-
135
- ## Step Dependencies
136
-
137
- Steps can depend on each other using the `argument` method with `result()`:
138
-
139
- ```ruby
140
- class ComplexReactor < RubyReactor::Reactor
141
- step :validate_order do
142
- run { validate_order_logic }
143
- end
144
-
145
- step :check_inventory do
146
- argument :order, result(:validate_order)
147
-
148
- run do |args, _context|
149
- check_inventory_for_order(args[:order])
150
- end
151
- end
152
-
153
- step :process_payment do
154
- argument :order, result(:check_inventory)
155
-
156
- run do |args, _context|
157
- process_payment_for_order(args[:order])
158
- end
159
- end
160
- end
161
- ```
162
-
163
- ## Error Handling and Compensation
164
-
165
- RubyReactor automatically handles errors and provides compensation:
166
-
167
- ```ruby
168
- class OrderProcessingReactor < RubyReactor::Reactor
169
- step :validate_order do
170
- run { validate_order_logic }
171
- end
172
-
173
- step :process_payment do
174
- run { process_payment_logic }
175
-
176
- undo do |payment_id:, **|
177
- # Undo the payment if something fails later
178
- PaymentService.refund(payment_id)
179
- end
180
- end
181
-
182
- step :update_inventory do
183
- run { update_inventory_logic }
184
-
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
190
- end
191
- end
192
- end
193
- ```
194
-
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
- ```
217
-
218
-
219
- ## Next Steps
220
-
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
@@ -1,161 +0,0 @@
1
- # Interrupts (Pause & Resume)
2
-
3
- RubyReactor introduces the `interrupt` mechanism to support long-running processes that require external input, such as user approvals, webhooks, or asynchronous job completions. Unlike standard steps that execute immediately, an `interrupt` pauses the reactor execution and persists its state, waiting for a signal to resume.
4
-
5
- ## DSL Usage
6
-
7
- Use the `interrupt` keyword to define a pause point in your reactor.
8
-
9
- ```ruby
10
- class ReportReactor < RubyReactor::Reactor
11
- step :request_report do
12
- run do
13
- response = HTTP.post("https://api.example.com/reports")
14
- Success(response.fetch(:id))
15
- end
16
- end
17
-
18
- interrupt :wait_for_report do
19
- # Declare dependency: execution must trigger this interrupt only after :request_report succeeds
20
- wait_for :request_report
21
-
22
- # Optional: deterministic correlation ID for looking up this execution later
23
- correlation_id do |context|
24
- "report-#{context.result(:request_report)}"
25
- end
26
-
27
- # Optional: timeout in seconds
28
- # Strategies:
29
- # - :lazy (default) - checked only when resume is attempted
30
- # - :active - schedules a background job to wake up the reactor and fail it
31
- timeout 1800, strategy: :active
32
-
33
- # Optional: validate incoming payload immediately using dry-validation
34
- validate do
35
- required(:status).filled(:string)
36
- required(:url).filled(:string)
37
- end
38
-
39
- # Optional: limit validation attempts (default: 1)
40
- # If exhausted, the reactor is cancelled and compensated.
41
- # Use :infinity for unlimited attempts.
42
- max_attempts 3
43
- end
44
-
45
- step :process_report do
46
- # The result of the interrupt step is the payload provided when resuming
47
- argument :webhook_payload, result(:wait_for_report)
48
-
49
- run do |args|
50
- Success(ReportProcessor.call(args[:webhook_payload]))
51
- end
52
- end
53
- end
54
- ```
55
-
56
- ### Options
57
-
58
- * **`wait_for`**: declare dependencies similar to `step`.
59
- * **`correlation_id`**: A block that returns a unique string to identify this execution. This allows you to resume the reactor using a business key (e.g., order ID) instead of the internal execution UUID.
60
- * **`timeout`**: Set a time limit for the interrupt.
61
- * **`validate`**: A `dry-validation` schema block to validate the payload provided when resuming.
62
- * **`max_attempts`**: Limit the number of times `continue` can be called with an invalid payload before the reactor is automatically cancelled and compensated. Defaults to 1. Set to `:infinity` for unlimited retries.
63
-
64
- ## Runtime Behavior
65
-
66
- When a reactor encounters an `interrupt`:
67
-
68
- 1. It executes any dependencies.
69
- 2. It persists the full `Context` (results of previous steps) to the configured storage (e.g., Redis).
70
- 3. It returns an `InterruptResult` and halts execution.
71
-
72
- ```ruby
73
- execution = ReportReactor.run(company_id: 1)
74
-
75
- if execution.paused?
76
- execution.id # => "uuid-123"
77
- execution.status # => :paused
78
- end
79
- ```
80
-
81
- ## Resuming Execution
82
-
83
- You can resume a paused reactor using its UUID or the defined `correlation_id`.
84
-
85
- ### By UUID
86
-
87
- ```ruby
88
- ReportReactor.continue(
89
- id: "uuid-123",
90
- payload: { status: "completed", url: "..." },
91
- step_name: :wait_for_report
92
- )
93
- ```
94
-
95
- ### By Correlation ID
96
-
97
- ```ruby
98
- ReportReactor.continue_by_correlation_id(
99
- correlation_id: "report-999",
100
- payload: { status: "completed", url: "..." },
101
- step_name: :wait_for_report
102
- )
103
- ```
104
-
105
- ### Resuming Method Styles
106
-
107
- There are two ways to invoke continuation:
108
-
109
- 1. **Strict / Fire-and-Forget (Class Method)**:
110
- * `Reactor.continue(...)`
111
- * If payload is invalid, it **automatically compensates (undo)** and cancels the reactor.
112
- * Best for webhooks where you can't ask the sender to fix the payload.
113
-
114
- 2. **Flexible (Instance Method)**:
115
- * First find the reactor: `reactor = ReportReactor.find("uuid-123")`
116
- * Then call: `result = reactor.continue(payload: ..., step_name: :wait_for_report)`
117
- * If payload is invalid, it returns a failure result but **does not** cancel execution.
118
- * Allows you to handle the error (e.g., show a form error to a user) and try again.
119
-
120
- ## Cancellation & Undo
121
-
122
- You can cancel a paused reactor if the operation is no longer needed.
123
-
124
- ```ruby
125
- # Undo: Runs defined compensations for completed steps in reverse order, then deletes execution.
126
- ReportReactor.undo("uuid-123")
127
-
128
- # Cancel: Stops execution immediately and marks the reactor as cancelled with the provided reason.
129
- # The context is preserved for inspection, but resumption is disabled.
130
- ReportReactor.cancel("uuid-123", reason: "User cancelled")
131
- ```
132
-
133
- ## Common Use Cases
134
-
135
- ### Human Approvals
136
-
137
- ```ruby
138
- interrupt :wait_for_approval do
139
- wait_for :submit_request
140
- correlation_id { |ctx| "approval-#{ctx.input(:request_id)}" }
141
- end
142
-
143
- step :process_decision do
144
- argument :decision, result(:wait_for_approval)
145
- run do |args|
146
- if args[:decision][:approved]
147
- Success("Approved")
148
- else
149
- Failure("Rejected")
150
- end
151
- end
152
- end
153
- ```
154
-
155
- ### Webhooks
156
-
157
- Use `correlation_id` to map an external resource ID (like a Payment Intent ID) back to the reactor waiting for confirmation.
158
-
159
- ### Scheduled Follow-ups
160
-
161
- Using `timeout` with `strategy: :active` to wake up a reactor after a delay if no external event occurs (e.g., expiring a reservation).