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.
- 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 +80 -4
- data/lib/ruby_reactor/context_serializer.rb +10 -1
- data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
- data/lib/ruby_reactor/rate_limit.rb +2 -2
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +58 -1
- data/lib/ruby_reactor/version.rb +1 -1
- metadata +7 -52
- data/documentation/DAG.md +0 -457
- data/documentation/README.md +0 -135
- data/documentation/async_reactors.md +0 -381
- data/documentation/composition.md +0 -199
- data/documentation/core_concepts.md +0 -676
- data/documentation/data_pipelines.md +0 -230
- data/documentation/examples/inventory_management.md +0 -748
- data/documentation/examples/order_processing.md +0 -380
- data/documentation/examples/payment_processing.md +0 -565
- data/documentation/getting_started.md +0 -242
- data/documentation/images/failed_order_processing.png +0 -0
- data/documentation/images/payment_workflow.png +0 -0
- data/documentation/interrupts.md +0 -163
- data/documentation/locks_and_semaphores.md +0 -459
- data/documentation/retry_configuration.md +0 -362
- data/documentation/testing.md +0 -994
- 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,362 +0,0 @@
|
|
|
1
|
-
# Retry Configuration
|
|
2
|
-
|
|
3
|
-
RubyReactor provides flexible, non-blocking retry mechanisms that requeue jobs instead of blocking worker threads. Retry policies can be configured at both reactor and step levels.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The retry system offers:
|
|
8
|
-
|
|
9
|
-
- **Non-blocking retries on Async**: Jobs are requeued with calculated delays
|
|
10
|
-
- **Multiple backoff strategies**: Exponential, linear, and fixed delays
|
|
11
|
-
- **Step-level control**: Different retry policies for different steps
|
|
12
|
-
- **Full observability**: Complete visibility into retry attempts
|
|
13
|
-
|
|
14
|
-
### Retry Flow Architecture
|
|
15
|
-
|
|
16
|
-
```mermaid
|
|
17
|
-
graph TD
|
|
18
|
-
A[Step Execution] --> B{Step<br/>Succeeds?}
|
|
19
|
-
B -->|Yes| C[Continue to Next Step]
|
|
20
|
-
B -->|No| D{Can Retry?<br/>attempts < max_attempts}
|
|
21
|
-
D -->|No| E[Final Failure<br/>Run Compensation]
|
|
22
|
-
D -->|Yes| F[Calculate Backoff Delay<br/>exponential/linear/fixed]
|
|
23
|
-
F --> G[Serialize Context<br/>with Retry State]
|
|
24
|
-
G --> H[Queue Job for Retry<br/>with Calculated Delay]
|
|
25
|
-
H --> I[Worker Freed<br/>No Thread Blocking]
|
|
26
|
-
I --> J[Delay Elapses]
|
|
27
|
-
J --> K[Worker Picks Up<br/>Retry Job]
|
|
28
|
-
K --> L[Deserialize Context]
|
|
29
|
-
L --> M[Resume Execution<br/>from Failed Step]
|
|
30
|
-
M --> A
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Basic Retry Configuration
|
|
34
|
-
|
|
35
|
-
### Step-Level Retry
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
class PaymentReactor < RubyReactor::Reactor
|
|
39
|
-
async true
|
|
40
|
-
|
|
41
|
-
step :charge_card do
|
|
42
|
-
retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
|
|
43
|
-
run { PaymentService.charge(card_token, amount) }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Reactor-Level Defaults
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
class PaymentReactor < RubyReactor::Reactor
|
|
52
|
-
async true
|
|
53
|
-
|
|
54
|
-
# All steps inherit these defaults
|
|
55
|
-
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
|
|
56
|
-
|
|
57
|
-
step :validate_card do
|
|
58
|
-
# Uses reactor defaults
|
|
59
|
-
run { validate_card_details }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
step :charge_card do
|
|
63
|
-
# Override for this specific step
|
|
64
|
-
retries max_attempts: 5, backoff: :linear, base_delay: 10.seconds
|
|
65
|
-
run { PaymentService.charge(card_token, amount) }
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Retry Parameters
|
|
71
|
-
|
|
72
|
-
### max_attempts
|
|
73
|
-
Maximum number of execution attempts (including the initial attempt).
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
retries max_attempts: 5 # 1 initial + 4 retries = 5 total attempts
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### backoff
|
|
80
|
-
The backoff strategy for calculating delays between retry attempts.
|
|
81
|
-
|
|
82
|
-
**Options:**
|
|
83
|
-
- `:exponential` (default): Delay doubles with each attempt
|
|
84
|
-
- `:linear`: Delay increases linearly
|
|
85
|
-
- `:fixed`: Same delay for each attempt
|
|
86
|
-
|
|
87
|
-
### base_delay
|
|
88
|
-
|
|
89
|
-
The base delay for retry calculations. Can be a number (seconds) or ActiveSupport duration.
|
|
90
|
-
|
|
91
|
-
```ruby
|
|
92
|
-
retries base_delay: 5.seconds
|
|
93
|
-
retries base_delay: 300 # 5 minutes in seconds
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Backoff Strategies
|
|
97
|
-
|
|
98
|
-
### Exponential Backoff
|
|
99
|
-
|
|
100
|
-
Delay doubles with each retry attempt. Best for external services that may be temporarily overloaded.
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
retries max_attempts: 4, backoff: :exponential, base_delay: 1.second
|
|
104
|
-
# Delays: 1s, 2s, 4s (total: 7 seconds)
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**Use cases:**
|
|
108
|
-
- API rate limiting
|
|
109
|
-
- Temporary service unavailability
|
|
110
|
-
- Network timeouts
|
|
111
|
-
|
|
112
|
-
### Linear Backoff
|
|
113
|
-
|
|
114
|
-
Delay increases linearly with each attempt. Provides predictable, gradually increasing delays.
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
retries max_attempts: 4, backoff: :linear, base_delay: 5.seconds
|
|
118
|
-
# Delays: 5s, 10s, 15s (total: 30 seconds)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**Use cases:**
|
|
122
|
-
- Database connection issues
|
|
123
|
-
- Resource contention
|
|
124
|
-
- Gradual backpressure
|
|
125
|
-
|
|
126
|
-
### Fixed Backoff
|
|
127
|
-
|
|
128
|
-
Same delay between each retry attempt. Simplest strategy with predictable timing.
|
|
129
|
-
|
|
130
|
-
```ruby
|
|
131
|
-
retries max_attempts: 4, backoff: :fixed, base_delay: 10.seconds
|
|
132
|
-
# Delays: 10s, 10s, 10s (total: 30 seconds)
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Use cases:**
|
|
136
|
-
- Simple retry scenarios
|
|
137
|
-
- When timing precision matters
|
|
138
|
-
- Testing environments
|
|
139
|
-
|
|
140
|
-
## Idempotency
|
|
141
|
-
|
|
142
|
-
### What is Idempotency?
|
|
143
|
-
|
|
144
|
-
An operation is idempotent if executing it multiple times produces the same result as executing it once.
|
|
145
|
-
|
|
146
|
-
### Idempotent vs Non-Idempotent Operations
|
|
147
|
-
|
|
148
|
-
**Idempotent operations (safe to retry):**
|
|
149
|
-
- Reading data
|
|
150
|
-
- Updating records with same values
|
|
151
|
-
- Sending notifications (with deduplication)
|
|
152
|
-
- Idempotent API calls
|
|
153
|
-
|
|
154
|
-
**Non-idempotent operations (unsafe to retry):**
|
|
155
|
-
- Creating new records
|
|
156
|
-
- Charging payments (without deduplication)
|
|
157
|
-
- Sending unique messages
|
|
158
|
-
- File system operations
|
|
159
|
-
|
|
160
|
-
### Best Practices
|
|
161
|
-
|
|
162
|
-
1. **Design for idempotency**: Structure operations to be safely retryable
|
|
163
|
-
2. **Use idempotency keys**: For payments, orders, etc.
|
|
164
|
-
3. **Test thoroughly**: Verify retry behavior doesn't cause issues
|
|
165
|
-
|
|
166
|
-
## Advanced Configuration
|
|
167
|
-
|
|
168
|
-
### Complex Retry Scenarios
|
|
169
|
-
|
|
170
|
-
```ruby
|
|
171
|
-
class OrderProcessingReactor < RubyReactor::Reactor
|
|
172
|
-
async true
|
|
173
|
-
|
|
174
|
-
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
|
|
175
|
-
|
|
176
|
-
step :validate_order do
|
|
177
|
-
# Quick validation - no retry needed
|
|
178
|
-
run { validate_order_exists(order_id) }
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
step :check_inventory do
|
|
182
|
-
# Inventory checks can be retried
|
|
183
|
-
retries max_attempts: 5, backoff: :linear, base_delay: 1.second
|
|
184
|
-
run { check_inventory_availability(order) }
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
step :reserve_inventory do
|
|
188
|
-
# Inventory reservation - must be idempotent
|
|
189
|
-
retries max_attempts: 3, backoff: :fixed, base_delay: 5.seconds
|
|
190
|
-
run { InventoryService.reserve_items(order.items) }
|
|
191
|
-
|
|
192
|
-
compensate do
|
|
193
|
-
# Release reservation on failure
|
|
194
|
-
InventoryService.release_reservation(order.items)
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
step :process_payment do
|
|
199
|
-
# Payment processing - critical, fewer retries
|
|
200
|
-
retries max_attempts: 2, backoff: :exponential, base_delay: 10.seconds
|
|
201
|
-
run { PaymentService.charge(order.total, order.card_token) }
|
|
202
|
-
|
|
203
|
-
compensate do |payment_id:|
|
|
204
|
-
# Refund on failure
|
|
205
|
-
PaymentService.refund(payment_id) if payment_id
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
step :confirm_order do
|
|
210
|
-
# Final confirmation - must succeed
|
|
211
|
-
run { OrderService.mark_completed(order_id) }
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Conditional Retry Logic
|
|
217
|
-
|
|
218
|
-
For more complex retry logic, you can implement custom retry handlers:
|
|
219
|
-
|
|
220
|
-
```ruby
|
|
221
|
-
class CustomRetryReactor < RubyReactor::Reactor
|
|
222
|
-
async true
|
|
223
|
-
|
|
224
|
-
step :call_external_api do
|
|
225
|
-
retries max_attempts: 5, backoff: :exponential, base_delay: 1.second
|
|
226
|
-
run do |_args, _ctx|
|
|
227
|
-
response = ExternalAPI.call
|
|
228
|
-
# Build a Failure with the right retryable flag so the retry manager
|
|
229
|
-
# can short-circuit non-transient errors.
|
|
230
|
-
case response.status
|
|
231
|
-
when 429 # Rate limited
|
|
232
|
-
Failure(RateLimitError.new(response), retryable: true)
|
|
233
|
-
when 500 # Server error
|
|
234
|
-
Failure(ServerError.new(response), retryable: true)
|
|
235
|
-
when 400 # Bad request - don't retry
|
|
236
|
-
Failure(ValidationError.new(response), retryable: false)
|
|
237
|
-
else
|
|
238
|
-
Success(response)
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
When a `Failure` is returned with `retryable: false`, the retry manager stops immediately and falls through to compensation. Custom error classes can also implement `retryable?` to control this from the exception side.
|
|
246
|
-
|
|
247
|
-
## Monitoring and Observability
|
|
248
|
-
|
|
249
|
-
### Retry Metrics
|
|
250
|
-
|
|
251
|
-
Track these important metrics:
|
|
252
|
-
|
|
253
|
-
```ruby
|
|
254
|
-
# In your monitoring system
|
|
255
|
-
retry_attempt_count(step_name)
|
|
256
|
-
retry_success_rate(step_name)
|
|
257
|
-
average_retry_delay(step_name)
|
|
258
|
-
retry_timeout_count(step_name)
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
### Sidekiq Web UI
|
|
263
|
-
|
|
264
|
-
Retry jobs are visible in Sidekiq web interface with:
|
|
265
|
-
- Step name and attempt number
|
|
266
|
-
- Failure reason and stack trace
|
|
267
|
-
- Scheduled retry time
|
|
268
|
-
- Job arguments and context
|
|
269
|
-
|
|
270
|
-
## Performance Considerations
|
|
271
|
-
|
|
272
|
-
### Retry Storm Prevention
|
|
273
|
-
|
|
274
|
-
Avoid retry storms by:
|
|
275
|
-
|
|
276
|
-
1. **Reasonable delays**: Don't use very short base delays
|
|
277
|
-
2. **Limited attempts**: Set appropriate max_attempts limits
|
|
278
|
-
3. **Circuit breakers**: Implement circuit breaker patterns for external services
|
|
279
|
-
4. **Rate limiting**: Consider rate limiting at the application level
|
|
280
|
-
|
|
281
|
-
### Resource Usage
|
|
282
|
-
|
|
283
|
-
- **Worker threads**: Retries don't block workers, improving utilization
|
|
284
|
-
- **Memory**: Context serialization adds memory overhead
|
|
285
|
-
- **Redis**: Job storage and queue management
|
|
286
|
-
- **Database**: Potential increased load from idempotent operations
|
|
287
|
-
|
|
288
|
-
### Tuning Guidelines
|
|
289
|
-
|
|
290
|
-
```ruby
|
|
291
|
-
# Fast-retry scenario (API calls)
|
|
292
|
-
retries max_attempts: 3, backoff: :exponential, base_delay: 1.second
|
|
293
|
-
|
|
294
|
-
# Slow-retry scenario (batch processing)
|
|
295
|
-
retries max_attempts: 5, backoff: :linear, base_delay: 5.minutes
|
|
296
|
-
|
|
297
|
-
# Critical operations (payments)
|
|
298
|
-
retries max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
## Error Types and Handling
|
|
302
|
-
|
|
303
|
-
### Retryable Errors
|
|
304
|
-
|
|
305
|
-
```ruby
|
|
306
|
-
class NetworkTimeoutError < StandardError
|
|
307
|
-
def retryable?
|
|
308
|
-
true
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
class ValidationError < StandardError
|
|
313
|
-
def retryable?
|
|
314
|
-
false # Don't retry validation errors
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Testing Retry Behavior
|
|
320
|
-
|
|
321
|
-
### Unit Testing
|
|
322
|
-
|
|
323
|
-
```ruby
|
|
324
|
-
RSpec.describe PaymentReactor do
|
|
325
|
-
it "retries failed payment with exponential backoff" do
|
|
326
|
-
allow(PaymentService).to receive(:charge)
|
|
327
|
-
.and_raise(NetworkError.new("Timeout"))
|
|
328
|
-
.and_raise(NetworkError.new("Timeout"))
|
|
329
|
-
.and_return(payment_result)
|
|
330
|
-
|
|
331
|
-
expect(PaymentService).to receive(:charge).exactly(3).times
|
|
332
|
-
|
|
333
|
-
subject = test_reactor(PaymentReactor, card_token: "tok_123", amount: 100)
|
|
334
|
-
|
|
335
|
-
expect(subject).to be_success
|
|
336
|
-
expect(subject).to have_retried_step(:charge_card).times(2)
|
|
337
|
-
expect(subject.step_result(:charge_card)[:payment_id]).to eq("pay_123")
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### Integration Testing
|
|
343
|
-
|
|
344
|
-
```ruby
|
|
345
|
-
describe "Retry integration" do
|
|
346
|
-
it "handles real Sidekiq retry scenarios" do
|
|
347
|
-
# Test with actual Sidekiq worker
|
|
348
|
-
Sidekiq::Testing.fake! do
|
|
349
|
-
result = FailingReactor.run(input: "test")
|
|
350
|
-
|
|
351
|
-
# Verify job was queued for retry
|
|
352
|
-
expect(RubyReactor::SidekiqWorkers::Worker.jobs.size).to eq(1)
|
|
353
|
-
|
|
354
|
-
# Process the retry
|
|
355
|
-
RubyReactor::SidekiqWorkers::Worker.drain
|
|
356
|
-
|
|
357
|
-
# Verify final success
|
|
358
|
-
expect(result).to be_success
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
```
|