fractor 0.1.8 → 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 +56 -61
- data/README.adoc +137 -0
- 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/persistent_work_queue.rb +218 -0
- data/lib/fractor/queue_persister.rb +253 -0
- data/lib/fractor/result_cache.rb +322 -0
- 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/work.rb +9 -2
- 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/wrapped_ractor3.rb +11 -6
- data/lib/fractor/wrapped_ractor4.rb +11 -6
- data/lib/fractor.rb +14 -0
- metadata +15 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Fractor Architecture
|
|
2
|
+
|
|
3
|
+
This document provides architecture diagrams and descriptions of the Fractor framework's components.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Fractor is a function-driven Ractors framework for Ruby that provides true parallelism using Ruby's Ractor feature with automatic work distribution across isolated workers.
|
|
8
|
+
|
|
9
|
+
## High-Level Architecture
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TB
|
|
13
|
+
subgraph "Application Layer"
|
|
14
|
+
Work[Work<br/>Immutable Input]
|
|
15
|
+
Worker[Worker<br/>Processing Logic]
|
|
16
|
+
WorkResult[WorkResult<br/>Success/Error Output]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subgraph "Orchestration Layer"
|
|
20
|
+
Supervisor[Supervisor<br/>Main Orchestrator]
|
|
21
|
+
ContinuousServer[ContinuousServer<br/>Long-Running Mode]
|
|
22
|
+
WorkflowExecutor[WorkflowExecutor<br/>Multi-Step Pipelines]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
subgraph "Concurrency Layer"
|
|
26
|
+
WorkQueue[WorkQueue<br/>Thread-Safe Queue]
|
|
27
|
+
ResultAggregator[ResultAggregator<br/>Thread-Safe Results]
|
|
28
|
+
CallbackRegistry[CallbackRegistry<br/>Event Callbacks]
|
|
29
|
+
WrappedRactor[WrappedRactor<br/>Ractor Wrapper]
|
|
30
|
+
WorkDistributionManager[WorkDistributionManager<br/>Idle Worker Tracking]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph "Ractor Layer"
|
|
34
|
+
Ractor1[Ractor 1]
|
|
35
|
+
Ractor2[Ractor 2]
|
|
36
|
+
Ractor3[Ractor 3]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Work --> Supervisor
|
|
40
|
+
Worker --> WorkflowExecutor
|
|
41
|
+
WorkResult --> ResultAggregator
|
|
42
|
+
|
|
43
|
+
Supervisor --> WorkQueue
|
|
44
|
+
Supervisor --> ResultAggregator
|
|
45
|
+
Supervisor --> CallbackRegistry
|
|
46
|
+
Supervisor --> WorkDistributionManager
|
|
47
|
+
|
|
48
|
+
ContinuousServer --> Supervisor
|
|
49
|
+
|
|
50
|
+
WorkflowExecutor --> Supervisor
|
|
51
|
+
WorkflowExecutor --> WorkQueue
|
|
52
|
+
|
|
53
|
+
WorkDistributionManager --> WrappedRactor
|
|
54
|
+
WrappedRactor --> Ractor1
|
|
55
|
+
WrappedRactor --> Ractor2
|
|
56
|
+
WrappedRactor --> Ractor3
|
|
57
|
+
|
|
58
|
+
Ractor1 --> Worker
|
|
59
|
+
Ractor2 --> Worker
|
|
60
|
+
Ractor3 --> Worker
|
|
61
|
+
|
|
62
|
+
style Work fill:#e1f5e1
|
|
63
|
+
style Worker fill:#e1f5e1
|
|
64
|
+
style WorkResult fill:#e1f5e1
|
|
65
|
+
style Supervisor fill:#e3f2fd
|
|
66
|
+
style ContinuousServer fill:#e3f2fd
|
|
67
|
+
style WorkflowExecutor fill:#e3f2fd
|
|
68
|
+
style WrappedRactor fill:#fff3e0
|
|
69
|
+
style Ractor1 fill:#fce4ec
|
|
70
|
+
style Ractor2 fill:#fce4ec
|
|
71
|
+
style Ractor3 fill:#fce4ec
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Component Relationships
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
graph LR
|
|
78
|
+
subgraph "User Code"
|
|
79
|
+
MyWork[MyWork < Work]
|
|
80
|
+
MyWorker[MyWorker < Worker]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
subgraph "Fractor Core"
|
|
84
|
+
Supervisor[Supervisor]
|
|
85
|
+
Queue[WorkQueue]
|
|
86
|
+
Results[ResultAggregator]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
subgraph "Worker Pool"
|
|
90
|
+
W1[Worker Ractor 1]
|
|
91
|
+
W2[Worker Ractor 2]
|
|
92
|
+
W3[Worker Ractor 3]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
MyWork --> Supervisor
|
|
96
|
+
MyWorker --> Supervisor
|
|
97
|
+
|
|
98
|
+
Supervisor --> Queue
|
|
99
|
+
Queue --> W1
|
|
100
|
+
Queue --> W2
|
|
101
|
+
Queue --> W3
|
|
102
|
+
|
|
103
|
+
W1 --> Results
|
|
104
|
+
W2 --> Results
|
|
105
|
+
W3 --> Results
|
|
106
|
+
|
|
107
|
+
Results --> Supervisor
|
|
108
|
+
Supervisor --> MyWork
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Pipeline Mode Execution Flow
|
|
112
|
+
|
|
113
|
+
```mermaid
|
|
114
|
+
sequenceDiagram
|
|
115
|
+
participant User
|
|
116
|
+
participant Supervisor
|
|
117
|
+
participant WorkQueue
|
|
118
|
+
participant Worker as Worker Ractor
|
|
119
|
+
participant Results
|
|
120
|
+
participant Callback as CallbackRegistry
|
|
121
|
+
|
|
122
|
+
User->>Supervisor: new(worker_pools: [...])
|
|
123
|
+
User->>Supervisor: add_work_items(items)
|
|
124
|
+
Supervisor->>WorkQueue: enqueue items
|
|
125
|
+
User->>Supervisor: run()
|
|
126
|
+
|
|
127
|
+
loop Main Loop
|
|
128
|
+
Supervisor->>WorkQueue: pop_batch()
|
|
129
|
+
WorkQueue-->>Supervisor: work items
|
|
130
|
+
|
|
131
|
+
Supervisor->>Worker: send work
|
|
132
|
+
Worker->>Worker: process(work)
|
|
133
|
+
Worker-->>Supervisor: WorkResult
|
|
134
|
+
Supervisor->>Results: add(result)
|
|
135
|
+
|
|
136
|
+
Supervisor->>Callback: process_work_callbacks()
|
|
137
|
+
Callback-->>Supervisor: new_work (optional)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
Supervisor-->>User: results
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Continuous Mode Execution Flow
|
|
144
|
+
|
|
145
|
+
```mermaid
|
|
146
|
+
sequenceDiagram
|
|
147
|
+
participant User
|
|
148
|
+
participant Server as ContinuousServer
|
|
149
|
+
participant Supervisor
|
|
150
|
+
participant Queue as WorkQueue
|
|
151
|
+
participant Callbacks as CallbackRegistry
|
|
152
|
+
|
|
153
|
+
User->>Server: new(worker_pools, work_queue)
|
|
154
|
+
Server->>Supervisor: new(continuous_mode: true)
|
|
155
|
+
Queue->>Supervisor: register_work_source()
|
|
156
|
+
Server->>Server: run()
|
|
157
|
+
|
|
158
|
+
loop Continuous Processing
|
|
159
|
+
Supervisor->>Callbacks: process_work_callbacks()
|
|
160
|
+
Callbacks-->>Supervisor: new work items
|
|
161
|
+
Supervisor->>Queue: enqueue new work
|
|
162
|
+
Note over Supervisor,Queue: Distribute to workers
|
|
163
|
+
|
|
164
|
+
Server->>Server: on_result callback
|
|
165
|
+
Server->>Server: on_error callback
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
User->>Server: stop() / Ctrl+C
|
|
169
|
+
Server->>Supervisor: stop()
|
|
170
|
+
Server-->>User: shutdown complete
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Workflow System Architecture
|
|
174
|
+
|
|
175
|
+
```mermaid
|
|
176
|
+
graph TB
|
|
177
|
+
subgraph "Workflow Definition"
|
|
178
|
+
DSL[Workflow DSL]
|
|
179
|
+
Builder[Workflow Builder]
|
|
180
|
+
Job[Job Definitions]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
subgraph "Workflow Execution"
|
|
184
|
+
Executor[WorkflowExecutor]
|
|
185
|
+
Resolver[DependencyResolver<br/>Topological Sort]
|
|
186
|
+
Logger[WorkflowExecutionLogger]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
subgraph "Execution Components"
|
|
190
|
+
JobExecutor[JobExecutor]
|
|
191
|
+
Retry[RetryOrchestrator]
|
|
192
|
+
Circuit[CircuitBreakerOrchestrator]
|
|
193
|
+
Fallback[FallbackJobHandler]
|
|
194
|
+
DLQ[DeadLetterQueue]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
DSL --> Builder
|
|
198
|
+
Builder --> Job
|
|
199
|
+
Job --> Executor
|
|
200
|
+
|
|
201
|
+
Executor --> Resolver
|
|
202
|
+
Executor --> Logger
|
|
203
|
+
Executor --> JobExecutor
|
|
204
|
+
|
|
205
|
+
JobExecutor --> Retry
|
|
206
|
+
JobExecutor --> Circuit
|
|
207
|
+
JobExecutor --> Fallback
|
|
208
|
+
JobExecutor --> DLQ
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Ruby Version-Specific Architecture
|
|
212
|
+
|
|
213
|
+
```mermaid
|
|
214
|
+
graph LR
|
|
215
|
+
subgraph "Ruby 3.x"
|
|
216
|
+
R3Handler[MainLoopHandler]
|
|
217
|
+
R3Wrapped[WrappedRactor]
|
|
218
|
+
R3Method[Ractor.yield / Ractor.receive]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
subgraph "Ruby 4.0+"
|
|
222
|
+
R4Handler[MainLoopHandler4]
|
|
223
|
+
R4Wrapped[WrappedRactor4]
|
|
224
|
+
R4Method[Ractor::Port / Ractor.select]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
subgraph "Shared"
|
|
228
|
+
Supervisor[Supervisor]
|
|
229
|
+
Common[Common Components]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
Supervisor --> R3Handler
|
|
233
|
+
Supervisor --> R4Handler
|
|
234
|
+
|
|
235
|
+
R3Handler --> R3Wrapped
|
|
236
|
+
R3Wrapped --> R3Method
|
|
237
|
+
|
|
238
|
+
R4Handler --> R4Wrapped
|
|
239
|
+
R4Wrapped --> R4Method
|
|
240
|
+
|
|
241
|
+
R3Handler --> Common
|
|
242
|
+
R4Handler --> Common
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Component Responsibilities
|
|
246
|
+
|
|
247
|
+
### Application Layer
|
|
248
|
+
|
|
249
|
+
| Component | Responsibility |
|
|
250
|
+
|-----------|---------------|
|
|
251
|
+
| **Work** | Immutable data container with input data |
|
|
252
|
+
| **Worker** | Processing logic with `process(work)` method |
|
|
253
|
+
| **WorkResult** | Contains success/failure status, result value, or error |
|
|
254
|
+
|
|
255
|
+
### Orchestration Layer
|
|
256
|
+
|
|
257
|
+
| Component | Responsibility |
|
|
258
|
+
|-----------|---------------|
|
|
259
|
+
| **Supervisor** | Main orchestrator for pipeline mode, manages worker lifecycle |
|
|
260
|
+
| **ContinuousServer** | High-level wrapper for long-running services |
|
|
261
|
+
| **WorkflowExecutor** | Orchestrates multi-step workflow executions |
|
|
262
|
+
|
|
263
|
+
### Concurrency Layer
|
|
264
|
+
|
|
265
|
+
| Component | Responsibility |
|
|
266
|
+
|-----------|---------------|
|
|
267
|
+
| **WorkQueue** | Thread-safe queue for work items |
|
|
268
|
+
| **ResultAggregator** | Thread-safe result collection with event notifications |
|
|
269
|
+
| **CallbackRegistry** | Manages work source and error callbacks |
|
|
270
|
+
| **WrappedRactor** | Safe wrapper around Ruby Ractor with version-specific implementations |
|
|
271
|
+
| **WorkDistributionManager** | Tracks idle workers and distributes work efficiently |
|
|
272
|
+
|
|
273
|
+
### Ractor Layer
|
|
274
|
+
|
|
275
|
+
| Component | Responsibility |
|
|
276
|
+
|-----------|---------------|
|
|
277
|
+
| **Ractor 1, 2, 3...** | Isolated Ruby Ractors containing Worker instances |
|
|
278
|
+
| **Worker instances** | Each Ractor has its own Worker instance for processing |
|
|
279
|
+
|
|
280
|
+
## Data Flow
|
|
281
|
+
|
|
282
|
+
### Work Processing Flow
|
|
283
|
+
|
|
284
|
+
```mermaid
|
|
285
|
+
graph LR
|
|
286
|
+
A[User creates Work] --> B[Supervisor.add_work_item]
|
|
287
|
+
B --> C[WorkQueue]
|
|
288
|
+
C --> D[WorkDistributionManager]
|
|
289
|
+
D --> E[Idle Worker Ractor]
|
|
290
|
+
E --> F[Worker.process]
|
|
291
|
+
F --> G[WorkResult]
|
|
292
|
+
G --> H[ResultAggregator]
|
|
293
|
+
H --> I[User retrieves results]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Error Handling Flow
|
|
297
|
+
|
|
298
|
+
```mermaid
|
|
299
|
+
graph LR
|
|
300
|
+
A[Worker.process raises error] --> B[WorkResult with error]
|
|
301
|
+
B --> C[ErrorReporter]
|
|
302
|
+
C --> D[ErrorStatistics]
|
|
303
|
+
C --> E[ErrorCallbacks]
|
|
304
|
+
E --> F[User error handler]
|
|
305
|
+
D --> G[ErrorReportGenerator]
|
|
306
|
+
G --> H[Formatted error output]
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Key Design Principles
|
|
310
|
+
|
|
311
|
+
1. **Function-Driven**: Work is defined as input → processing → output
|
|
312
|
+
2. **Message Passing**: Ractors communicate via messages, no shared state
|
|
313
|
+
3. **Immutability**: Work objects are immutable, ensuring thread safety
|
|
314
|
+
4. **Isolation**: Each Worker runs in its own Ractor with isolated memory
|
|
315
|
+
5. **Scalability**: Automatically distribute work across available workers
|
|
316
|
+
6. **Fault Tolerance**: Errors are captured without crashing other workers
|
|
317
|
+
7. **Version Compatibility**: Separate implementations for Ruby 3.x and 4.0+
|
|
@@ -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
|