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.
- checksums.yaml +4 -4
- data/.release-please-config.json +15 -0
- data/.release-please-manifest.json +3 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +13 -0
- data/README.md +194 -9
- data/lib/ruby_reactor/configuration.rb +18 -1
- data/lib/ruby_reactor/context_serializer.rb +10 -1
- data/lib/ruby_reactor/dsl/lockable.rb +130 -0
- data/lib/ruby_reactor/executor/result_handler.rb +19 -0
- data/lib/ruby_reactor/executor/step_executor.rb +5 -0
- data/lib/ruby_reactor/executor.rb +145 -2
- data/lib/ruby_reactor/lock.rb +92 -0
- data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
- data/lib/ruby_reactor/period.rb +67 -0
- data/lib/ruby_reactor/rate_limit.rb +74 -0
- data/lib/ruby_reactor/reactor.rb +1 -0
- data/lib/ruby_reactor/rspec/matchers.rb +171 -4
- data/lib/ruby_reactor/semaphore.rb +58 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +128 -9
- 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.rb +49 -0
- metadata +13 -51
- data/documentation/DAG.md +0 -457
- data/documentation/README.md +0 -123
- data/documentation/async_reactors.md +0 -369
- data/documentation/composition.md +0 -199
- data/documentation/core_concepts.md +0 -662
- data/documentation/data_pipelines.md +0 -230
- data/documentation/examples/inventory_management.md +0 -749
- data/documentation/examples/order_processing.md +0 -365
- data/documentation/examples/payment_processing.md +0 -654
- data/documentation/getting_started.md +0 -224
- data/documentation/images/failed_order_processing.png +0 -0
- data/documentation/images/payment_workflow.png +0 -0
- data/documentation/interrupts.md +0 -161
- data/documentation/retry_configuration.md +0 -357
- data/documentation/testing.md +0 -812
- data/gui/.gitignore +0 -24
- data/gui/README.md +0 -73
- data/gui/eslint.config.js +0 -23
- data/gui/index.html +0 -13
- data/gui/package-lock.json +0 -5925
- data/gui/package.json +0 -46
- data/gui/postcss.config.js +0 -6
- data/gui/public/vite.svg +0 -1
- data/gui/src/App.css +0 -42
- data/gui/src/App.tsx +0 -51
- data/gui/src/assets/react.svg +0 -1
- data/gui/src/components/DagVisualizer.tsx +0 -424
- data/gui/src/components/Dashboard.tsx +0 -163
- data/gui/src/components/ErrorBoundary.tsx +0 -47
- data/gui/src/components/ReactorDetail.tsx +0 -135
- data/gui/src/components/StepInspector.tsx +0 -492
- data/gui/src/components/__tests__/DagVisualizer.test.tsx +0 -140
- data/gui/src/components/__tests__/ReactorDetail.test.tsx +0 -111
- data/gui/src/components/__tests__/StepInspector.test.tsx +0 -408
- data/gui/src/globals.d.ts +0 -7
- data/gui/src/index.css +0 -14
- data/gui/src/lib/utils.ts +0 -13
- data/gui/src/main.tsx +0 -14
- data/gui/src/test/setup.ts +0 -11
- data/gui/tailwind.config.js +0 -11
- data/gui/tsconfig.app.json +0 -28
- data/gui/tsconfig.json +0 -7
- data/gui/tsconfig.node.json +0 -26
- data/gui/vite.config.ts +0 -8
- 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
|
|
Binary file
|
|
Binary file
|
data/documentation/interrupts.md
DELETED
|
@@ -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).
|