fractor 0.1.9 → 0.1.10
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/.rubocop_todo.yml +28 -91
- data/docs/ARCHITECTURE.md +317 -0
- data/docs/PERFORMANCE_TUNING.md +355 -0
- data/docs/TROUBLESHOOTING.md +463 -0
- data/lib/fractor/callback_registry.rb +106 -0
- data/lib/fractor/config_schema.rb +170 -0
- data/lib/fractor/main_loop_handler.rb +4 -8
- data/lib/fractor/main_loop_handler3.rb +10 -12
- data/lib/fractor/main_loop_handler4.rb +48 -20
- data/lib/fractor/result_cache.rb +58 -10
- data/lib/fractor/shutdown_handler.rb +12 -6
- data/lib/fractor/supervisor.rb +100 -13
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/workflow/execution/dependency_resolver.rb +149 -0
- data/lib/fractor/workflow/execution/fallback_job_handler.rb +68 -0
- data/lib/fractor/workflow/execution/job_executor.rb +242 -0
- data/lib/fractor/workflow/execution/result_builder.rb +76 -0
- data/lib/fractor/workflow/execution/workflow_execution_logger.rb +241 -0
- data/lib/fractor/workflow/workflow_executor.rb +97 -476
- data/lib/fractor/wrapped_ractor.rb +2 -4
- data/lib/fractor.rb +11 -0
- metadata +12 -2
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Performance Tuning Guide
|
|
2
|
+
|
|
3
|
+
This guide helps you optimize Fractor for your specific use case.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Worker Pool Configuration](#worker-pool-configuration)
|
|
8
|
+
- [Work Item Design](#work-item-design)
|
|
9
|
+
- [Batch Size Tuning](#batch-size-tuning)
|
|
10
|
+
- [Memory Management](#memory-management)
|
|
11
|
+
- [Workflow Optimization](#workflow-optimization)
|
|
12
|
+
- [Monitoring and Profiling](#monitoring-and-profiling)
|
|
13
|
+
- [Common Performance Issues](#common-performance-issues)
|
|
14
|
+
|
|
15
|
+
## Worker Pool Configuration
|
|
16
|
+
|
|
17
|
+
### Determining Optimal Worker Count
|
|
18
|
+
|
|
19
|
+
The number of workers depends on your workload characteristics:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# CPU-bound tasks: Use number of processors
|
|
23
|
+
num_workers: Etc.nprocessors
|
|
24
|
+
|
|
25
|
+
# I/O-bound tasks: Use 2-4x processors
|
|
26
|
+
num_workers: Etc.nprocessors * 2
|
|
27
|
+
|
|
28
|
+
# Mixed workload: Start with processors, tune from there
|
|
29
|
+
num_workers: Etc.nprocessors
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Guidelines:**
|
|
33
|
+
- **CPU-bound** (data processing, computation): Use `Etc.nprocessors`
|
|
34
|
+
- **I/O-bound** (HTTP requests, database queries): Use `2-4 * Etc.nprocessors`
|
|
35
|
+
- **Mixed workload**: Start with `Etc.nprocessors`, monitor, and adjust
|
|
36
|
+
|
|
37
|
+
### Multiple Worker Pools
|
|
38
|
+
|
|
39
|
+
Use different worker pools for different task types:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Fractor::Supervisor.new(
|
|
43
|
+
worker_pools: [
|
|
44
|
+
# Fast CPU-bound tasks - more workers
|
|
45
|
+
{ worker_class: FastProcessor, num_workers: 8 },
|
|
46
|
+
# Slow I/O-bound tasks - fewer workers
|
|
47
|
+
{ worker_class: SlowAPICaller, num_workers: 2 },
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Work Item Design
|
|
53
|
+
|
|
54
|
+
### Keep Work Items Small
|
|
55
|
+
|
|
56
|
+
**Optimal**: Small, independent work items
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Good: Many small items
|
|
60
|
+
1000.times do |i|
|
|
61
|
+
queue << ProcessDataWork.new(data[i])
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Suboptimal**: Large, monolithic work items
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Less efficient: One large item
|
|
69
|
+
queue << ProcessAllDataWork.new(all_data)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Avoid Shared State
|
|
73
|
+
|
|
74
|
+
Work items should be self-contained:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Good: Self-contained work
|
|
78
|
+
class ProcessUserWork < Fractor::Work
|
|
79
|
+
def initialize(user_id)
|
|
80
|
+
super({ user_id: user_id })
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Bad: Work that depends on external state
|
|
85
|
+
class ProcessUserWork < Fractor::Work
|
|
86
|
+
def initialize(user_id)
|
|
87
|
+
super({ user_id: user_id, cache: $shared_cache }) # Avoid!
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Use Result Caching for Expensive Operations
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
cache = Fractor::ResultCache.new(ttl: 300) # 5 minute TTL
|
|
96
|
+
|
|
97
|
+
# Cached expensive operation
|
|
98
|
+
result = cache.get(expensive_work) do
|
|
99
|
+
# Only executes if not cached
|
|
100
|
+
expensive_work.process
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Batch Size Tuning
|
|
105
|
+
|
|
106
|
+
### WorkQueue Batch Size
|
|
107
|
+
|
|
108
|
+
When using `WorkQueue`, the default batch size is 10. Adjust based on:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# For many small, quick tasks: larger batch
|
|
112
|
+
queue.register_with_supervisor(supervisor, batch_size: 50)
|
|
113
|
+
|
|
114
|
+
# For fewer, slower tasks: smaller batch
|
|
115
|
+
queue.register_with_supervisor(supervisor, batch_size: 5)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Worker Processing Batch Size
|
|
119
|
+
|
|
120
|
+
Workers can process multiple items per message:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class BatchWorker < Fractor::Worker
|
|
124
|
+
def process(work)
|
|
125
|
+
# Process single item
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Memory Management
|
|
131
|
+
|
|
132
|
+
### Result Aggregator Memory
|
|
133
|
+
|
|
134
|
+
For large result sets, consider processing incrementally:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Instead of collecting all results:
|
|
138
|
+
supervisor.run
|
|
139
|
+
all_results = supervisor.results.results # May use lots of memory
|
|
140
|
+
|
|
141
|
+
# Use on_complete callbacks:
|
|
142
|
+
supervisor.results.on_new_result do |result|
|
|
143
|
+
# Process each result as it arrives
|
|
144
|
+
save_to_database(result)
|
|
145
|
+
end
|
|
146
|
+
supervisor.run
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Result Cache Memory Limits
|
|
150
|
+
|
|
151
|
+
Configure cache limits for memory-constrained environments:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Limit by entry count
|
|
155
|
+
cache = Fractor::ResultCache.new(max_size: 1000)
|
|
156
|
+
|
|
157
|
+
# Limit by memory (approximate)
|
|
158
|
+
cache = Fractor::ResultCache.new(max_memory: 100_000_000) # 100MB
|
|
159
|
+
|
|
160
|
+
# Both limits
|
|
161
|
+
cache = Fractor::ResultCache.new(
|
|
162
|
+
max_size: 1000,
|
|
163
|
+
max_memory: 100_000_000
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Queue Memory Limits
|
|
168
|
+
|
|
169
|
+
For very large work sets, use persistent queue:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# Use file-based queue for large datasets
|
|
173
|
+
queue = Fractor::PersistentWorkQueue.new(
|
|
174
|
+
queue_file: "/tmp/work_queue.db"
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Workflow Optimization
|
|
179
|
+
|
|
180
|
+
### Enable Execution Order Caching
|
|
181
|
+
|
|
182
|
+
For repeated workflow executions:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class MyWorkflow < Fractor::Workflow
|
|
186
|
+
# Enable caching for repeated executions
|
|
187
|
+
enable_cache
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Optimize Job Dependencies
|
|
192
|
+
|
|
193
|
+
Minimize dependencies for better parallelism:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
Fractor::Workflow.define("optimized") do
|
|
197
|
+
job "fetch_data" do
|
|
198
|
+
runs FetchWorker
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# These can run in parallel (both depend only on fetch_data)
|
|
202
|
+
job "process_a" do
|
|
203
|
+
runs ProcessAWorker
|
|
204
|
+
needs "fetch_data"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
job "process_b" do
|
|
208
|
+
runs ProcessBWorker
|
|
209
|
+
needs "fetch_data"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# This depends on both, so runs after them
|
|
213
|
+
job "combine" do
|
|
214
|
+
runs CombineWorker
|
|
215
|
+
needs ["process_a", "process_b"]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Use Circuit Breakers for Failing Services
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
Fractor::Workflow.define("resilient") do
|
|
224
|
+
job "external_api" do
|
|
225
|
+
runs ExternalAPIWorker
|
|
226
|
+
|
|
227
|
+
# Circuit breaker prevents cascading failures
|
|
228
|
+
circuit_breaker threshold: 5, timeout: 60
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Monitoring and Profiling
|
|
234
|
+
|
|
235
|
+
### Enable Performance Monitoring
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
supervisor = Fractor::Supervisor.new(
|
|
239
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
240
|
+
enable_performance_monitoring: true
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
supervisor.run
|
|
244
|
+
|
|
245
|
+
# Get performance metrics
|
|
246
|
+
metrics = supervisor.performance_metrics
|
|
247
|
+
puts "Latency: #{metrics.avg_latency}ms"
|
|
248
|
+
puts "Throughput: #{metrics.throughput} items/sec"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Monitor Cache Performance
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
cache = Fractor::ResultCache.new
|
|
255
|
+
|
|
256
|
+
# Run workload
|
|
257
|
+
# ...
|
|
258
|
+
|
|
259
|
+
stats = cache.stats
|
|
260
|
+
puts "Hit rate: #{stats[:hit_rate]}%"
|
|
261
|
+
puts "Cache size: #{stats[:size]}"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Use Debug Output
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
supervisor = Fractor::Supervisor.new(
|
|
268
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
269
|
+
debug: true # Enable verbose output
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Common Performance Issues
|
|
274
|
+
|
|
275
|
+
### Issue: Workers Idle but Work in Queue
|
|
276
|
+
|
|
277
|
+
**Symptom**: `workers_status` shows idle workers but work isn't being distributed.
|
|
278
|
+
|
|
279
|
+
**Solution**: Check that `work_distribution_manager` is properly initialized:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
# This is handled automatically by Supervisor
|
|
283
|
+
# If using custom setup, ensure:
|
|
284
|
+
@work_distribution_manager = WorkDistributionManager.new(...)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Issue: High Memory Usage
|
|
288
|
+
|
|
289
|
+
**Symptom**: Memory grows continuously during execution.
|
|
290
|
+
|
|
291
|
+
**Solutions**:
|
|
292
|
+
1. Process results incrementally with `on_new_result` callbacks
|
|
293
|
+
2. Configure cache limits with `max_size` and `max_memory`
|
|
294
|
+
3. Use persistent queue for large datasets
|
|
295
|
+
|
|
296
|
+
### Issue: Slow Workflow Execution
|
|
297
|
+
|
|
298
|
+
**Symptom**: Workflow takes longer than expected.
|
|
299
|
+
|
|
300
|
+
**Solutions**:
|
|
301
|
+
1. Enable execution order caching
|
|
302
|
+
2. Optimize job dependencies for parallelism
|
|
303
|
+
3. Use `parallel_map` for independent transformations
|
|
304
|
+
|
|
305
|
+
### Issue: Uneven Worker Utilization
|
|
306
|
+
|
|
307
|
+
**Symptom**: Some workers busy, others idle.
|
|
308
|
+
|
|
309
|
+
**Solution**: Use separate worker pools for different task types:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
# Instead of mixed workload in one pool:
|
|
313
|
+
# { worker_class: MixedWorker, num_workers: 8 }
|
|
314
|
+
|
|
315
|
+
# Use separate pools:
|
|
316
|
+
worker_pools: [
|
|
317
|
+
{ worker_class: FastWorker, num_workers: 6 },
|
|
318
|
+
{ worker_class: SlowWorker, num_workers: 2 },
|
|
319
|
+
]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Performance Benchmarks
|
|
323
|
+
|
|
324
|
+
### Typical Throughput (CPU-bound)
|
|
325
|
+
|
|
326
|
+
| Workers | Throughput (items/sec) | Speedup |
|
|
327
|
+
|---------|------------------------|---------|
|
|
328
|
+
| 1 | 1,000 | 1x |
|
|
329
|
+
| 2 | 1,900 | 1.9x |
|
|
330
|
+
| 4 | 3,600 | 3.6x |
|
|
331
|
+
| 8 | 6,800 | 6.8x |
|
|
332
|
+
|
|
333
|
+
*Benchmarks on 8-core system, CPU-bound workload*
|
|
334
|
+
|
|
335
|
+
### Typical Throughput (I/O-bound)
|
|
336
|
+
|
|
337
|
+
| Workers | Throughput (requests/sec) | Speedup |
|
|
338
|
+
|---------|---------------------------|---------|
|
|
339
|
+
| 1 | 100 | 1x |
|
|
340
|
+
| 2 | 190 | 1.9x |
|
|
341
|
+
| 4 | 380 | 3.8x |
|
|
342
|
+
| 8 | 750 | 7.5x |
|
|
343
|
+
| 16 | 1,400 | 14x |
|
|
344
|
+
|
|
345
|
+
*Benchmarks with HTTP API calls, 100ms latency*
|
|
346
|
+
|
|
347
|
+
## Best Practices Summary
|
|
348
|
+
|
|
349
|
+
1. **Start simple**: Use default settings, then optimize based on measurements
|
|
350
|
+
2. **Measure first**: Enable performance monitoring before tuning
|
|
351
|
+
3. **Profile**: Use debug output to understand bottlenecks
|
|
352
|
+
4. **Batch appropriately**: Balance batch size for your workload
|
|
353
|
+
5. **Cache wisely**: Use result caching for expensive, deterministic operations
|
|
354
|
+
6. **Monitor memory**: Set limits on cache and queue sizes
|
|
355
|
+
7. **Design for isolation**: Keep work items independent and self-contained
|