fractor 0.1.6 → 0.1.7
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 +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Core concepts
|
|
4
|
+
nav_order: 1
|
|
5
|
+
---
|
|
6
|
+
= Core concepts
|
|
7
|
+
|
|
8
|
+
== Overview
|
|
9
|
+
|
|
10
|
+
Fractor consists of several core components that work together to provide parallel processing capabilities. Understanding these components and their interactions is essential for effective use of the framework.
|
|
11
|
+
|
|
12
|
+
.Fractor architecture
|
|
13
|
+
[source]
|
|
14
|
+
----
|
|
15
|
+
┌─────────────────────────────────────────────────────────┐
|
|
16
|
+
│ Client Application │
|
|
17
|
+
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
18
|
+
│ │ Work Items │ │ Workers │ │ Supervisor │ │
|
|
19
|
+
│ │ (Your Data) │ │ (Your Logic) │ │ (Manages) │ │
|
|
20
|
+
│ └───────────────┘ └──────────────┘ └──────────────┘ │
|
|
21
|
+
└─────────────────────────────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
┌─────────────────────────────────────────────────────────┐
|
|
25
|
+
│ Fractor Framework │
|
|
26
|
+
│ │
|
|
27
|
+
│ ┌────────────────────────────────────────────────┐ │
|
|
28
|
+
│ │ Supervisor │ │
|
|
29
|
+
│ │ - Manages worker pool │ │
|
|
30
|
+
│ │ - Distributes work │ │
|
|
31
|
+
│ │ - Collects results │ │
|
|
32
|
+
│ └────────────────────────────────────────────────┘ │
|
|
33
|
+
│ │ │
|
|
34
|
+
│ ▼ │
|
|
35
|
+
│ ┌────────────────────────────────────────────────┐ │
|
|
36
|
+
│ │ WrappedRactors (Pool) │ │
|
|
37
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
38
|
+
│ │ │ Ractor 1 │ │ Ractor 2 │ │ Ractor N │ │ │
|
|
39
|
+
│ │ │ Worker │ │ Worker │ │ Worker │ │ │
|
|
40
|
+
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
|
41
|
+
│ └────────────────────────────────────────────────┘ │
|
|
42
|
+
│ │ │
|
|
43
|
+
│ ▼ │
|
|
44
|
+
│ ┌────────────────────────────────────────────────┐ │
|
|
45
|
+
│ │ ResultAggregator │ │
|
|
46
|
+
│ │ - Successful results │ │
|
|
47
|
+
│ │ - Error results │ │
|
|
48
|
+
│ └────────────────────────────────────────────────┘ │
|
|
49
|
+
└─────────────────────────────────────────────────────────┘
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
== Core components
|
|
53
|
+
|
|
54
|
+
=== Fractor::Work
|
|
55
|
+
|
|
56
|
+
The `Fractor::Work` class is an abstract base class that represents a unit of work to be processed.
|
|
57
|
+
|
|
58
|
+
==== Purpose
|
|
59
|
+
|
|
60
|
+
* Encapsulates input data needed for processing
|
|
61
|
+
* Provides a standard interface for work items
|
|
62
|
+
* Can be subclassed to create specific work types
|
|
63
|
+
|
|
64
|
+
==== Implementation
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
class Fractor::Work
|
|
69
|
+
attr_reader :input
|
|
70
|
+
|
|
71
|
+
def initialize(input)
|
|
72
|
+
@input = input
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
----
|
|
76
|
+
|
|
77
|
+
==== Usage
|
|
78
|
+
|
|
79
|
+
Subclass `Fractor::Work` to define your work items:
|
|
80
|
+
|
|
81
|
+
[source,ruby]
|
|
82
|
+
----
|
|
83
|
+
class MyWork < Fractor::Work
|
|
84
|
+
def initialize(data)
|
|
85
|
+
super(data)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Add convenience methods
|
|
89
|
+
def value
|
|
90
|
+
input[:value]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_s
|
|
94
|
+
"MyWork(#{value})"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Create work items
|
|
99
|
+
work = MyWork.new({ value: 42 })
|
|
100
|
+
----
|
|
101
|
+
|
|
102
|
+
=== Fractor::Worker
|
|
103
|
+
|
|
104
|
+
The `Fractor::Worker` class is an abstract base class that defines processing logic.
|
|
105
|
+
|
|
106
|
+
==== Purpose
|
|
107
|
+
|
|
108
|
+
* Implements the `process(work)` method that performs the actual work
|
|
109
|
+
* Runs inside a Ractor for parallel execution
|
|
110
|
+
* Returns `WorkResult` objects
|
|
111
|
+
|
|
112
|
+
==== Implementation
|
|
113
|
+
|
|
114
|
+
[source,ruby]
|
|
115
|
+
----
|
|
116
|
+
class Fractor::Worker
|
|
117
|
+
def process(work)
|
|
118
|
+
raise NotImplementedError, "Subclasses must implement process(work)"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
----
|
|
122
|
+
|
|
123
|
+
==== Usage
|
|
124
|
+
|
|
125
|
+
Subclass `Fractor::Worker` and implement `process`:
|
|
126
|
+
|
|
127
|
+
[source,ruby]
|
|
128
|
+
----
|
|
129
|
+
class MyWorker < Fractor::Worker
|
|
130
|
+
def process(work)
|
|
131
|
+
# Perform processing
|
|
132
|
+
result = work.value * 2
|
|
133
|
+
|
|
134
|
+
# Return success
|
|
135
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
136
|
+
rescue => e
|
|
137
|
+
# Return error
|
|
138
|
+
Fractor::WorkResult.new(error: e.message, work: work)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
----
|
|
142
|
+
|
|
143
|
+
==== Best practices
|
|
144
|
+
|
|
145
|
+
* Keep `process` focused on a single responsibility
|
|
146
|
+
* Handle both success and error cases
|
|
147
|
+
* Return valid `WorkResult` objects for all code paths
|
|
148
|
+
* Avoid side effects outside the `process` method
|
|
149
|
+
* Use meaningful error messages
|
|
150
|
+
|
|
151
|
+
=== Fractor::WorkResult
|
|
152
|
+
|
|
153
|
+
The `Fractor::WorkResult` class represents the outcome of processing a work item with rich error context.
|
|
154
|
+
|
|
155
|
+
==== Purpose
|
|
156
|
+
|
|
157
|
+
* Contains either a successful result or an error with metadata
|
|
158
|
+
* Links back to the original work item
|
|
159
|
+
* Provides error categorization and severity levels
|
|
160
|
+
* Enables intelligent error handling and retry logic
|
|
161
|
+
|
|
162
|
+
==== Basic usage
|
|
163
|
+
|
|
164
|
+
Create results in your worker:
|
|
165
|
+
|
|
166
|
+
[source,ruby]
|
|
167
|
+
----
|
|
168
|
+
# Success result
|
|
169
|
+
Fractor::WorkResult.new(
|
|
170
|
+
result: computed_value,
|
|
171
|
+
work: work
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Simple error result
|
|
175
|
+
Fractor::WorkResult.new(
|
|
176
|
+
error: StandardError.new("Something went wrong"),
|
|
177
|
+
work: work
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Check result status
|
|
181
|
+
if work_result.success?
|
|
182
|
+
puts work_result.result
|
|
183
|
+
else
|
|
184
|
+
puts work_result.error
|
|
185
|
+
end
|
|
186
|
+
----
|
|
187
|
+
|
|
188
|
+
==== Enhanced error context
|
|
189
|
+
|
|
190
|
+
WorkResult supports rich error metadata for production systems:
|
|
191
|
+
|
|
192
|
+
[source,ruby]
|
|
193
|
+
----
|
|
194
|
+
# Error with full context
|
|
195
|
+
Fractor::WorkResult.new(
|
|
196
|
+
error: Timeout::Error.new("API timeout"),
|
|
197
|
+
work: work,
|
|
198
|
+
error_code: :api_timeout,
|
|
199
|
+
error_context: {
|
|
200
|
+
endpoint: "https://api.example.com/data",
|
|
201
|
+
timeout_seconds: 30,
|
|
202
|
+
attempt: 3
|
|
203
|
+
},
|
|
204
|
+
error_category: :network, # Optional, auto-inferred if not provided
|
|
205
|
+
error_severity: :error # Optional, auto-inferred if not provided
|
|
206
|
+
)
|
|
207
|
+
----
|
|
208
|
+
|
|
209
|
+
==== Error categories
|
|
210
|
+
|
|
211
|
+
WorkResult automatically categorizes errors to enable intelligent error handling:
|
|
212
|
+
|
|
213
|
+
[cols="1,2,3"]
|
|
214
|
+
|===
|
|
215
|
+
|Category |Error Types |Examples
|
|
216
|
+
|
|
217
|
+
|`:validation`
|
|
218
|
+
|Input validation errors
|
|
219
|
+
|`ArgumentError`, `TypeError`
|
|
220
|
+
|
|
221
|
+
|`:timeout`
|
|
222
|
+
|Timeout errors
|
|
223
|
+
|`Timeout::Error`
|
|
224
|
+
|
|
225
|
+
|`:network`
|
|
226
|
+
|Network-related errors
|
|
227
|
+
|`SocketError`, `Errno::ECONNREFUSED`, `Errno::ETIMEDOUT`
|
|
228
|
+
|
|
229
|
+
|`:resource`
|
|
230
|
+
|Resource exhaustion
|
|
231
|
+
|`Errno::ENOMEM`, `Errno::ENOSPC`
|
|
232
|
+
|
|
233
|
+
|`:business`
|
|
234
|
+
|Business logic errors
|
|
235
|
+
|Custom domain errors (manual categorization)
|
|
236
|
+
|
|
237
|
+
|`:system`
|
|
238
|
+
|System errors
|
|
239
|
+
|`SystemCallError`, `SystemStackError`
|
|
240
|
+
|
|
241
|
+
|`:unknown`
|
|
242
|
+
|Uncategorized errors
|
|
243
|
+
|Other exceptions
|
|
244
|
+
|===
|
|
245
|
+
|
|
246
|
+
==== Error severity levels
|
|
247
|
+
|
|
248
|
+
Severity levels help prioritize error handling:
|
|
249
|
+
|
|
250
|
+
[cols="1,3"]
|
|
251
|
+
|===
|
|
252
|
+
|Severity |Description
|
|
253
|
+
|
|
254
|
+
|`:critical`
|
|
255
|
+
|System-breaking errors requiring immediate attention
|
|
256
|
+
|
|
257
|
+
|`:error`
|
|
258
|
+
|Standard errors that prevent operation completion
|
|
259
|
+
|
|
260
|
+
|`:warning`
|
|
261
|
+
|Non-fatal issues that may affect quality
|
|
262
|
+
|
|
263
|
+
|`:info`
|
|
264
|
+
|Informational messages
|
|
265
|
+
|===
|
|
266
|
+
|
|
267
|
+
==== Helper methods
|
|
268
|
+
|
|
269
|
+
WorkResult provides convenient methods for error inspection:
|
|
270
|
+
|
|
271
|
+
[source,ruby]
|
|
272
|
+
----
|
|
273
|
+
work_result = Fractor::WorkResult.new(
|
|
274
|
+
error: Timeout::Error.new("API timeout"),
|
|
275
|
+
work: work
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Status checks
|
|
279
|
+
work_result.success? # => false
|
|
280
|
+
work_result.failure? # => true
|
|
281
|
+
work_result.critical? # => false
|
|
282
|
+
work_result.retriable? # => true (timeout/network/resource errors are retriable)
|
|
283
|
+
|
|
284
|
+
# Get comprehensive error information
|
|
285
|
+
error_info = work_result.error_info
|
|
286
|
+
# => {
|
|
287
|
+
# error: #<Timeout::Error>,
|
|
288
|
+
# error_class: "Timeout::Error",
|
|
289
|
+
# error_message: "API timeout",
|
|
290
|
+
# error_code: nil,
|
|
291
|
+
# error_category: :timeout,
|
|
292
|
+
# error_severity: :error,
|
|
293
|
+
# error_context: {}
|
|
294
|
+
# }
|
|
295
|
+
----
|
|
296
|
+
|
|
297
|
+
==== Automatic categorization
|
|
298
|
+
|
|
299
|
+
Errors are automatically categorized based on their type:
|
|
300
|
+
|
|
301
|
+
[source,ruby]
|
|
302
|
+
----
|
|
303
|
+
# Validation error
|
|
304
|
+
result = Fractor::WorkResult.new(error: ArgumentError.new("Invalid input"))
|
|
305
|
+
result.error_category # => :validation
|
|
306
|
+
result.retriable? # => false
|
|
307
|
+
|
|
308
|
+
# Network error
|
|
309
|
+
result = Fractor::WorkResult.new(error: SocketError.new("Connection refused"))
|
|
310
|
+
result.error_category # => :network
|
|
311
|
+
result.retriable? # => true
|
|
312
|
+
|
|
313
|
+
# Critical system error
|
|
314
|
+
result = Fractor::WorkResult.new(error: SystemStackError.new)
|
|
315
|
+
result.error_severity # => :critical
|
|
316
|
+
result.critical? # => true
|
|
317
|
+
----
|
|
318
|
+
|
|
319
|
+
==== Best practices
|
|
320
|
+
|
|
321
|
+
* Use `error_code` for machine-readable error identification
|
|
322
|
+
* Include relevant context in `error_context` (endpoint URLs, timing, attempts, etc.)
|
|
323
|
+
* Let auto-categorization work for standard errors
|
|
324
|
+
* Manually set `error_category` for domain-specific errors
|
|
325
|
+
* Use `retriable?` to determine if retry logic should apply
|
|
326
|
+
* Check `critical?` for errors requiring immediate escalation
|
|
327
|
+
|
|
328
|
+
=== Fractor::WrappedRactor
|
|
329
|
+
|
|
330
|
+
The `Fractor::WrappedRactor` class manages individual Ruby Ractors.
|
|
331
|
+
|
|
332
|
+
==== Purpose
|
|
333
|
+
|
|
334
|
+
* Encapsulates a single Ractor instance
|
|
335
|
+
* Creates and manages a Worker instance within the Ractor
|
|
336
|
+
* Handles communication between the Supervisor and Worker
|
|
337
|
+
* Manages Ractor lifecycle
|
|
338
|
+
|
|
339
|
+
==== Implementation details
|
|
340
|
+
|
|
341
|
+
Each `WrappedRactor`:
|
|
342
|
+
|
|
343
|
+
* Creates a new Ruby Ractor
|
|
344
|
+
* Instantiates the Worker class inside the Ractor
|
|
345
|
+
* Receives Work items from the Supervisor
|
|
346
|
+
* Calls the Worker's `process` method
|
|
347
|
+
* Returns WorkResult objects to the Supervisor
|
|
348
|
+
* Handles errors and ensures proper cleanup
|
|
349
|
+
|
|
350
|
+
==== Internal usage
|
|
351
|
+
|
|
352
|
+
You typically don't interact with `WrappedRactor` directly. The Supervisor manages these for you:
|
|
353
|
+
|
|
354
|
+
[source,ruby]
|
|
355
|
+
----
|
|
356
|
+
# Created internally by Supervisor
|
|
357
|
+
wrapped_ractor = Fractor::WrappedRactor.new(MyWorker)
|
|
358
|
+
|
|
359
|
+
# Supervisor sends work
|
|
360
|
+
wrapped_ractor.send_work(work_item)
|
|
361
|
+
|
|
362
|
+
# Supervisor receives results via Ractor.select
|
|
363
|
+
----
|
|
364
|
+
|
|
365
|
+
=== Fractor::Supervisor
|
|
366
|
+
|
|
367
|
+
The `Fractor::Supervisor` class orchestrates the entire framework.
|
|
368
|
+
|
|
369
|
+
==== Purpose
|
|
370
|
+
|
|
371
|
+
* Manages the pool of WrappedRactor instances
|
|
372
|
+
* Distributes work items to available workers
|
|
373
|
+
* Collects results from workers using `Ractor.select`
|
|
374
|
+
* Handles graceful shutdown
|
|
375
|
+
* Provides access to aggregated results
|
|
376
|
+
|
|
377
|
+
==== Key responsibilities
|
|
378
|
+
|
|
379
|
+
. *Worker pool management*: Creates and manages WrappedRactor instances
|
|
380
|
+
. *Work distribution*: Routes work items to available workers
|
|
381
|
+
. *Result collection*: Gathers WorkResult objects from workers
|
|
382
|
+
. *Error handling*: Captures and stores errors
|
|
383
|
+
. *Signal handling*: Responds to SIGINT, SIGTERM, SIGUSR1/SIGBREAK
|
|
384
|
+
|
|
385
|
+
==== Configuration
|
|
386
|
+
|
|
387
|
+
[source,ruby]
|
|
388
|
+
----
|
|
389
|
+
supervisor = Fractor::Supervisor.new(
|
|
390
|
+
worker_pools: [
|
|
391
|
+
{
|
|
392
|
+
worker_class: MyWorker, # Required
|
|
393
|
+
num_workers: 4 # Optional, auto-detected if omitted
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
worker_class: OtherWorker, # Can have multiple pools
|
|
397
|
+
num_workers: 2
|
|
398
|
+
}
|
|
399
|
+
],
|
|
400
|
+
continuous_mode: false # Default: false for pipeline mode
|
|
401
|
+
)
|
|
402
|
+
----
|
|
403
|
+
|
|
404
|
+
==== Usage patterns
|
|
405
|
+
|
|
406
|
+
Pipeline mode:
|
|
407
|
+
|
|
408
|
+
[source,ruby]
|
|
409
|
+
----
|
|
410
|
+
# Create supervisor
|
|
411
|
+
supervisor = Fractor::Supervisor.new(
|
|
412
|
+
worker_pools: [{ worker_class: MyWorker }]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Add work
|
|
416
|
+
supervisor.add_work_items([work1, work2, work3])
|
|
417
|
+
|
|
418
|
+
# Run
|
|
419
|
+
supervisor.run
|
|
420
|
+
|
|
421
|
+
# Access results
|
|
422
|
+
supervisor.results.results # Successful results
|
|
423
|
+
supervisor.results.errors # Error results
|
|
424
|
+
----
|
|
425
|
+
|
|
426
|
+
Continuous mode:
|
|
427
|
+
|
|
428
|
+
[source,ruby]
|
|
429
|
+
----
|
|
430
|
+
supervisor = Fractor::Supervisor.new(
|
|
431
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
432
|
+
continuous_mode: true
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Register work source
|
|
436
|
+
supervisor.register_work_source do
|
|
437
|
+
# Return work items or nil
|
|
438
|
+
fetch_next_work_items
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Run (blocks until shutdown signal)
|
|
442
|
+
supervisor.run
|
|
443
|
+
----
|
|
444
|
+
|
|
445
|
+
=== Fractor::ResultAggregator
|
|
446
|
+
|
|
447
|
+
The `Fractor::ResultAggregator` class collects and organizes processing results.
|
|
448
|
+
|
|
449
|
+
==== Purpose
|
|
450
|
+
|
|
451
|
+
* Separates successful results from errors
|
|
452
|
+
* Provides convenient access to results
|
|
453
|
+
* Thread-safe result collection
|
|
454
|
+
|
|
455
|
+
==== Implementation
|
|
456
|
+
|
|
457
|
+
[source,ruby]
|
|
458
|
+
----
|
|
459
|
+
class Fractor::ResultAggregator
|
|
460
|
+
attr_reader :results, :errors
|
|
461
|
+
|
|
462
|
+
def initialize
|
|
463
|
+
@results = []
|
|
464
|
+
@errors = []
|
|
465
|
+
@mutex = Mutex.new
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def add_result(work_result)
|
|
469
|
+
@mutex.synchronize do
|
|
470
|
+
if work_result.success?
|
|
471
|
+
@results << work_result
|
|
472
|
+
else
|
|
473
|
+
@errors << work_result
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
----
|
|
479
|
+
|
|
480
|
+
==== Usage
|
|
481
|
+
|
|
482
|
+
Access results after processing:
|
|
483
|
+
|
|
484
|
+
[source,ruby]
|
|
485
|
+
----
|
|
486
|
+
aggregator = supervisor.results
|
|
487
|
+
|
|
488
|
+
# Get counts
|
|
489
|
+
puts "Processed: #{aggregator.results.size}"
|
|
490
|
+
puts "Failed: #{aggregator.errors.size}"
|
|
491
|
+
|
|
492
|
+
# Iterate successful results
|
|
493
|
+
aggregator.results.each do |result|
|
|
494
|
+
puts "Result: #{result.result}"
|
|
495
|
+
puts "Original work: #{result.work}"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Iterate errors
|
|
499
|
+
aggregator.errors.each do |error_result|
|
|
500
|
+
puts "Error: #{error_result.error}"
|
|
501
|
+
puts "Failed work: #{error_result.work}"
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Extract values
|
|
505
|
+
values = aggregator.results.map(&:result)
|
|
506
|
+
error_messages = aggregator.errors.map(&:error)
|
|
507
|
+
----
|
|
508
|
+
|
|
509
|
+
== Component interactions
|
|
510
|
+
|
|
511
|
+
=== Pipeline mode flow
|
|
512
|
+
|
|
513
|
+
[source]
|
|
514
|
+
----
|
|
515
|
+
1. Client creates Supervisor with Worker class(es)
|
|
516
|
+
2. Client adds Work items to Supervisor
|
|
517
|
+
3. Client calls supervisor.run
|
|
518
|
+
4. Supervisor creates WrappedRactor instances
|
|
519
|
+
5. Each WrappedRactor starts a Ractor with a Worker
|
|
520
|
+
6. Supervisor distributes Work to available Ractors
|
|
521
|
+
7. Workers process Work and return WorkResult
|
|
522
|
+
8. Supervisor collects results via Ractor.select
|
|
523
|
+
9. ResultAggregator stores results/errors
|
|
524
|
+
10. Supervisor completes when all work done
|
|
525
|
+
11. Client accesses aggregated results
|
|
526
|
+
----
|
|
527
|
+
|
|
528
|
+
=== Continuous mode flow
|
|
529
|
+
|
|
530
|
+
[source]
|
|
531
|
+
----
|
|
532
|
+
1. Client creates Supervisor in continuous mode
|
|
533
|
+
2. Client registers work source callback
|
|
534
|
+
3. Client calls supervisor.run
|
|
535
|
+
4. Supervisor creates WrappedRactor instances
|
|
536
|
+
5. Supervisor enters event loop
|
|
537
|
+
6. Work source callback provides new work
|
|
538
|
+
7. Supervisor distributes work to available Ractors
|
|
539
|
+
8. Workers process and return results
|
|
540
|
+
9. ResultAggregator stores results/errors
|
|
541
|
+
10. Loop continues until shutdown signal
|
|
542
|
+
11. Graceful shutdown completes in-progress work
|
|
543
|
+
----
|
|
544
|
+
|
|
545
|
+
== System architecture
|
|
546
|
+
|
|
547
|
+
=== Layered architecture
|
|
548
|
+
|
|
549
|
+
Fractor employs a layered architecture that separates concerns and provides clear boundaries between components:
|
|
550
|
+
|
|
551
|
+
[source]
|
|
552
|
+
----
|
|
553
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
554
|
+
│ Application Layer │
|
|
555
|
+
│ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │
|
|
556
|
+
│ │ Workflow │ │ Business │ │ Work/Worker │ │
|
|
557
|
+
│ │ DSL │ │ Logic │ │ Definitions │ │
|
|
558
|
+
│ └────────────┘ └────────────┘ └─────────────────────┘ │
|
|
559
|
+
└─────────────────────────────────────────────────────────────┘
|
|
560
|
+
│
|
|
561
|
+
▼
|
|
562
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
563
|
+
│ Orchestration Layer │
|
|
564
|
+
│ ┌────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
|
565
|
+
│ │ Supervisor │ │ Workflow │ │ Continuous │ │
|
|
566
|
+
│ │ │ │ Executor │ │ Server │ │
|
|
567
|
+
│ └────────────┘ └──────────────┘ └──────────────────┘ │
|
|
568
|
+
└─────────────────────────────────────────────────────────────┘
|
|
569
|
+
│
|
|
570
|
+
▼
|
|
571
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
572
|
+
│ Concurrency Layer │
|
|
573
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
|
574
|
+
│ │ Wrapped │ │ Work Queue │ │ Result │ │
|
|
575
|
+
│ │ Ractors │ │ Management │ │ Aggregator │ │
|
|
576
|
+
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
|
|
577
|
+
└─────────────────────────────────────────────────────────────┘
|
|
578
|
+
│
|
|
579
|
+
▼
|
|
580
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
581
|
+
│ Ruby Ractor Layer │
|
|
582
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
583
|
+
│ │ Ractor 1 │ │ Ractor 2 │ │ Ractor 3 │ │ Ractor N │ │
|
|
584
|
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
|
585
|
+
└─────────────────────────────────────────────────────────────┘
|
|
586
|
+
----
|
|
587
|
+
|
|
588
|
+
Where,
|
|
589
|
+
|
|
590
|
+
Application Layer:: Contains user-defined business logic, workflows, and work/worker definitions
|
|
591
|
+
Orchestration Layer:: Manages execution flow, work distribution, and lifecycle
|
|
592
|
+
Concurrency Layer:: Handles parallel execution, queue management, and result collection
|
|
593
|
+
Ruby Ractor Layer:: Provides true parallelism through Ruby's Ractor primitive
|
|
594
|
+
|
|
595
|
+
=== Component responsibilities
|
|
596
|
+
|
|
597
|
+
[cols="2,3,3"]
|
|
598
|
+
|===
|
|
599
|
+
|Component |Responsibility |Key Features
|
|
600
|
+
|
|
601
|
+
|Supervisor
|
|
602
|
+
|Orchestrates work distribution and collection
|
|
603
|
+
|Worker pool management, signal handling, graceful shutdown
|
|
604
|
+
|
|
605
|
+
|WrappedRactor
|
|
606
|
+
|Encapsulates individual Ractor lifecycle
|
|
607
|
+
|Message passing, error handling, cleanup
|
|
608
|
+
|
|
609
|
+
|Worker
|
|
610
|
+
|Executes business logic
|
|
611
|
+
|Stateless processing, error recovery, type safety
|
|
612
|
+
|
|
613
|
+
|WorkQueue
|
|
614
|
+
|Manages work item storage
|
|
615
|
+
|Thread-safe operations, batch processing, backpressure
|
|
616
|
+
|
|
617
|
+
|ResultAggregator
|
|
618
|
+
|Collects processing results
|
|
619
|
+
|Success/error separation, callback support
|
|
620
|
+
|
|
621
|
+
|Workflow
|
|
622
|
+
|Coordinates multi-step processing
|
|
623
|
+
|Dependency resolution, type checking, visualization
|
|
624
|
+
|
|
625
|
+
|ContinuousServer
|
|
626
|
+
|Manages long-running applications
|
|
627
|
+
|Threading, logging, graceful shutdown
|
|
628
|
+
|===
|
|
629
|
+
|
|
630
|
+
== Ractor concurrency model
|
|
631
|
+
|
|
632
|
+
=== True parallelism
|
|
633
|
+
|
|
634
|
+
Fractor leverages Ruby's Ractor feature to achieve true parallelism, eliminating the Global Interpreter Lock (GIL) limitations:
|
|
635
|
+
|
|
636
|
+
[source]
|
|
637
|
+
----
|
|
638
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
639
|
+
│ Traditional Threading │
|
|
640
|
+
│ (Limited by GIL - only one thread executes at a time) │
|
|
641
|
+
│ │
|
|
642
|
+
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
643
|
+
│ │Thread 1│ │Thread 2│ │Thread 3│ │Thread 4│ │
|
|
644
|
+
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │
|
|
645
|
+
│ │ │ │ │ │
|
|
646
|
+
│ └───────────┴───────────┴───────────┘ │
|
|
647
|
+
│ │ │
|
|
648
|
+
│ ┌──────▼──────┐ │
|
|
649
|
+
│ │ GIL │ │
|
|
650
|
+
│ │ (Serializes │ │
|
|
651
|
+
│ │ execution) │ │
|
|
652
|
+
│ └──────────────┘ │
|
|
653
|
+
└─────────────────────────────────────────────────────────────┘
|
|
654
|
+
|
|
655
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
656
|
+
│ Fractor with Ractors │
|
|
657
|
+
│ (True parallelism - all ractors execute simultaneously) │
|
|
658
|
+
│ │
|
|
659
|
+
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
660
|
+
│ │Ractor 1│ │Ractor 2│ │Ractor 3│ │Ractor 4│ │
|
|
661
|
+
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │
|
|
662
|
+
│ │ │ │ │ │
|
|
663
|
+
│ ▼ ▼ ▼ ▼ │
|
|
664
|
+
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
665
|
+
│ │ CPU 1 │ │ CPU 2 │ │ CPU 3 │ │ CPU 4 │ │
|
|
666
|
+
│ └────────┘ └────────┘ └────────┘ └────────┘ │
|
|
667
|
+
│ │
|
|
668
|
+
│ Each Ractor runs on its own CPU core in parallel │
|
|
669
|
+
└─────────────────────────────────────────────────────────────┘
|
|
670
|
+
----
|
|
671
|
+
|
|
672
|
+
=== Memory isolation
|
|
673
|
+
|
|
674
|
+
Ractors provide memory isolation to prevent race conditions:
|
|
675
|
+
|
|
676
|
+
[source]
|
|
677
|
+
----
|
|
678
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
679
|
+
│ Ractor A │ │ Ractor B │
|
|
680
|
+
│ │ │ │
|
|
681
|
+
│ ┌────────────┐ │ │ ┌────────────┐ │
|
|
682
|
+
│ │ Memory │ │ │ │ Memory │ │
|
|
683
|
+
│ │ │ │ │ │ │ │
|
|
684
|
+
│ │ Variables │ │ │ │ Variables │ │
|
|
685
|
+
│ │ Objects │ │ │ │ Objects │ │
|
|
686
|
+
│ │ State │ │ │ │ State │ │
|
|
687
|
+
│ └────────────┘ │ │ └────────────┘ │
|
|
688
|
+
│ │ │ │ │ │
|
|
689
|
+
│ ▼ │ │ ▼ │
|
|
690
|
+
│ Isolated and │ ❌ │ Cannot access │
|
|
691
|
+
│ thread-safe │ ──── │ each other's │
|
|
692
|
+
│ │ │ memory │
|
|
693
|
+
└──────────────────┘ └──────────────────┘
|
|
694
|
+
│ │
|
|
695
|
+
│ Message Passing ✓ │
|
|
696
|
+
└────────────┬──────────────┘
|
|
697
|
+
▼
|
|
698
|
+
┌──────────────┐
|
|
699
|
+
│ Shareable │
|
|
700
|
+
│ Objects │
|
|
701
|
+
│ (immutable) │
|
|
702
|
+
└──────────────┘
|
|
703
|
+
----
|
|
704
|
+
|
|
705
|
+
Key benefits:
|
|
706
|
+
|
|
707
|
+
* *No race conditions*: Each Ractor has isolated memory
|
|
708
|
+
* *No locks needed*: Message passing ensures coordination
|
|
709
|
+
* *Predictable behavior*: No shared mutable state
|
|
710
|
+
* *Crash isolation*: Errors in one Ractor don't affect others
|
|
711
|
+
|
|
712
|
+
=== Message passing protocol
|
|
713
|
+
|
|
714
|
+
Fractor uses a structured message passing protocol between Supervisor and Ractors:
|
|
715
|
+
|
|
716
|
+
[source,ruby]
|
|
717
|
+
----
|
|
718
|
+
# Message types sent from Ractor to Supervisor
|
|
719
|
+
{
|
|
720
|
+
type: :initialize, # Ractor ready for work
|
|
721
|
+
processor: "worker-1"
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
{
|
|
725
|
+
type: :result, # Work completed successfully
|
|
726
|
+
result: WorkResult,
|
|
727
|
+
processor: "worker-1"
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
{
|
|
731
|
+
type: :error, # Work failed with error
|
|
732
|
+
result: WorkResult, # Contains error details
|
|
733
|
+
processor: "worker-1"
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
{
|
|
737
|
+
type: :shutdown, # Ractor acknowledging shutdown
|
|
738
|
+
processor: "worker-1"
|
|
739
|
+
}
|
|
740
|
+
----
|
|
741
|
+
|
|
742
|
+
Message flow:
|
|
743
|
+
|
|
744
|
+
[source]
|
|
745
|
+
----
|
|
746
|
+
Supervisor Ractor
|
|
747
|
+
│ │
|
|
748
|
+
├─────── Start Ractor ──────────────────────>│
|
|
749
|
+
│ │
|
|
750
|
+
│<────── :initialize message ────────────────┤
|
|
751
|
+
│ │
|
|
752
|
+
├─────── Send Work object ──────────────────>│
|
|
753
|
+
│ │
|
|
754
|
+
│ [Processing]
|
|
755
|
+
│ │
|
|
756
|
+
│<────── :result message ────────────────────┤
|
|
757
|
+
│ │
|
|
758
|
+
├─────── Send next Work ────────────────────>│
|
|
759
|
+
│ │
|
|
760
|
+
├─────── :shutdown message ─────────────────>│
|
|
761
|
+
│ │
|
|
762
|
+
│<────── :shutdown acknowledgment ───────────┤
|
|
763
|
+
│ │
|
|
764
|
+
----
|
|
765
|
+
|
|
766
|
+
=== Thread safety guarantees
|
|
767
|
+
|
|
768
|
+
Fractor ensures thread safety through:
|
|
769
|
+
|
|
770
|
+
. *Immutable work objects*: [`Work`](../../lib/fractor/work.rb) objects are treated as immutable after creation
|
|
771
|
+
. *Copy-on-send*: Messages are copied when sent between Ractors
|
|
772
|
+
. *Shareable objects*: Only shareable objects (immutable or explicitly marked) can cross Ractor boundaries
|
|
773
|
+
. *Isolated state*: Each [`Worker`](../../lib/fractor/worker.rb) instance runs in its own memory space
|
|
774
|
+
|
|
775
|
+
[example]
|
|
776
|
+
====
|
|
777
|
+
[source,ruby]
|
|
778
|
+
----
|
|
779
|
+
# Work objects must be shareable (immutable or frozen)
|
|
780
|
+
class MyWork < Fractor::Work
|
|
781
|
+
def initialize(data)
|
|
782
|
+
super(data.freeze) # Freeze ensures shareability
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Workers maintain isolated state
|
|
787
|
+
class MyWorker < Fractor::Worker
|
|
788
|
+
def initialize(name: nil, **options)
|
|
789
|
+
super
|
|
790
|
+
@counter = 0 # Isolated per Ractor, no race conditions
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def process(work)
|
|
794
|
+
@counter += 1 # Safe to mutate - this is Ractor-local
|
|
795
|
+
# Process work...
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
----
|
|
799
|
+
====
|
|
800
|
+
|
|
801
|
+
== Data flow architecture
|
|
802
|
+
|
|
803
|
+
=== Pipeline mode data flow
|
|
804
|
+
|
|
805
|
+
In pipeline mode, data flows linearly through the system:
|
|
806
|
+
|
|
807
|
+
[source]
|
|
808
|
+
----
|
|
809
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
810
|
+
│ 1. Work Submission Phase │
|
|
811
|
+
└─────────────────────────────────────────────────────────────┘
|
|
812
|
+
Client Application
|
|
813
|
+
│
|
|
814
|
+
│ add_work_items([work1, work2, ...])
|
|
815
|
+
▼
|
|
816
|
+
┌──────────────┐
|
|
817
|
+
│ Supervisor │
|
|
818
|
+
│ Work Queue │
|
|
819
|
+
└──────────────┘
|
|
820
|
+
|
|
821
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
822
|
+
│ 2. Distribution Phase │
|
|
823
|
+
└─────────────────────────────────────────────────────────────┘
|
|
824
|
+
┌──────────────┐
|
|
825
|
+
│ Supervisor │
|
|
826
|
+
│ │─┐
|
|
827
|
+
└──────────────┘ │ Ractor.select(*ractors)
|
|
828
|
+
│ + work distribution
|
|
829
|
+
┌───────────┴────────────┬───────────────┐
|
|
830
|
+
▼ ▼ ▼
|
|
831
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
832
|
+
│Ractor 1 │ │Ractor 2 │ ... │Ractor N │
|
|
833
|
+
│Worker 1 │ │Worker 2 │ │Worker N │
|
|
834
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
835
|
+
│ │ │
|
|
836
|
+
│ Process │ Process │ Process
|
|
837
|
+
▼ ▼ ▼
|
|
838
|
+
|
|
839
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
840
|
+
│ 3. Collection Phase │
|
|
841
|
+
└─────────────────────────────────────────────────────────────┘
|
|
842
|
+
│ │ │
|
|
843
|
+
│ WorkResult │ WorkResult │ WorkResult
|
|
844
|
+
▼ ▼ ▼
|
|
845
|
+
┌──────────────────────────────────────────────────────┐
|
|
846
|
+
│ Result Aggregator │
|
|
847
|
+
│ ┌──────────────────┐ ┌──────────────────────────┐ │
|
|
848
|
+
│ │ Successful │ │ Failed Results │ │
|
|
849
|
+
│ │ Results │ │ (with error details) │ │
|
|
850
|
+
│ └──────────────────┘ └──────────────────────────┘ │
|
|
851
|
+
└──────────────────────────────────────────────────────┘
|
|
852
|
+
│
|
|
853
|
+
│ Access results/errors
|
|
854
|
+
▼
|
|
855
|
+
Client Application
|
|
856
|
+
----
|
|
857
|
+
|
|
858
|
+
Work distribution algorithm:
|
|
859
|
+
|
|
860
|
+
. Supervisor waits on `Ractor.select` for available Ractors
|
|
861
|
+
. When a Ractor signals availability (via `:initialize` or `:result`):
|
|
862
|
+
* Check if work queue is empty
|
|
863
|
+
* If work available, send next work item to Ractor
|
|
864
|
+
* If queue empty, mark Ractor as idle
|
|
865
|
+
. Continue until all work processed
|
|
866
|
+
|
|
867
|
+
=== Continuous mode data flow
|
|
868
|
+
|
|
869
|
+
In continuous mode, data flows perpetually with dynamic work sources:
|
|
870
|
+
|
|
871
|
+
[source]
|
|
872
|
+
----
|
|
873
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
874
|
+
│ Continuous Event Loop │
|
|
875
|
+
└─────────────────────────────────────────────────────────────┘
|
|
876
|
+
|
|
877
|
+
Work Sources Timer Thread
|
|
878
|
+
(Callbacks) │
|
|
879
|
+
│ │ Periodic wakeup (100ms)
|
|
880
|
+
▼ ▼
|
|
881
|
+
┌────────────────────────────────────────┐
|
|
882
|
+
│ Supervisor Main Loop │
|
|
883
|
+
│ while @running do │
|
|
884
|
+
│ 1. Poll work sources │
|
|
885
|
+
│ 2. Distribute to idle workers │
|
|
886
|
+
│ 3. Wait on Ractor.select │
|
|
887
|
+
│ 4. Process results │
|
|
888
|
+
│ end │
|
|
889
|
+
└────────────────────────────────────────┘
|
|
890
|
+
│ │
|
|
891
|
+
│ Work Items │ Results
|
|
892
|
+
▼ ▼
|
|
893
|
+
┌─────────┐ ┌──────────────┐
|
|
894
|
+
│ Worker │ │ Result │
|
|
895
|
+
│ Ractors │ │ Callbacks │
|
|
896
|
+
└─────────┘ └──────────────┘
|
|
897
|
+
|
|
898
|
+
Wakeup Ractor
|
|
899
|
+
│
|
|
900
|
+
│ Unblocks Ractor.select
|
|
901
|
+
│ when work available
|
|
902
|
+
▼
|
|
903
|
+
Main Event Loop
|
|
904
|
+
----
|
|
905
|
+
|
|
906
|
+
Key differences from pipeline mode:
|
|
907
|
+
|
|
908
|
+
* *Infinite loop*: Runs until explicitly stopped
|
|
909
|
+
* *Dynamic work*: Work arrives from callbacks, not pre-loaded
|
|
910
|
+
* *Graceful shutdown*: Completes in-progress work before stopping
|
|
911
|
+
* *Real-time processing*: Minimal latency between work arrival and processing
|
|
912
|
+
|
|
913
|
+
=== Work distribution algorithms
|
|
914
|
+
|
|
915
|
+
==== Round-robin distribution
|
|
916
|
+
|
|
917
|
+
Default distribution strategy in pipeline mode:
|
|
918
|
+
|
|
919
|
+
[source]
|
|
920
|
+
----
|
|
921
|
+
Work Queue: [W1, W2, W3, W4, W5, W6]
|
|
922
|
+
|
|
923
|
+
Worker 1 Worker 2 Worker 3 Worker 4
|
|
924
|
+
│ │ │ │
|
|
925
|
+
│<─ W1 │ │ │
|
|
926
|
+
│ │<─ W2 │ │
|
|
927
|
+
│ │ │<─ W3 │
|
|
928
|
+
│ │ │ │<─ W4
|
|
929
|
+
│<─ W5 │ │ │
|
|
930
|
+
│ │<─ W6 │ │
|
|
931
|
+
----
|
|
932
|
+
|
|
933
|
+
Characteristics:
|
|
934
|
+
|
|
935
|
+
* Fair distribution across all workers
|
|
936
|
+
* Simple and predictable
|
|
937
|
+
* No work stealing between workers
|
|
938
|
+
* Each worker processes items sequentially
|
|
939
|
+
|
|
940
|
+
==== Priority-based distribution
|
|
941
|
+
|
|
942
|
+
Using [`PriorityWorkQueue`](../../lib/fractor/priority_work_queue.rb):
|
|
943
|
+
|
|
944
|
+
[source]
|
|
945
|
+
----
|
|
946
|
+
Priority Queue:
|
|
947
|
+
Critical: [W1, W4]
|
|
948
|
+
High: [W2, W5]
|
|
949
|
+
Normal: [W3]
|
|
950
|
+
Low: [W6]
|
|
951
|
+
|
|
952
|
+
Distribution order: W1 → W4 → W2 → W5 → W3 → W6
|
|
953
|
+
|
|
954
|
+
With aging (60s threshold):
|
|
955
|
+
If W6 waits > 60s, effective priority increases
|
|
956
|
+
Prevents starvation of low-priority items
|
|
957
|
+
----
|
|
958
|
+
|
|
959
|
+
=== Result collection strategies
|
|
960
|
+
|
|
961
|
+
==== Immediate collection
|
|
962
|
+
|
|
963
|
+
Results collected as they arrive via `Ractor.select`:
|
|
964
|
+
|
|
965
|
+
[source,ruby]
|
|
966
|
+
----
|
|
967
|
+
loop do
|
|
968
|
+
ractor, message = Ractor.select(*active_ractors)
|
|
969
|
+
|
|
970
|
+
case message[:type]
|
|
971
|
+
when :result
|
|
972
|
+
results.add_result(message[:result])
|
|
973
|
+
send_next_work_if_available(ractor)
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
----
|
|
977
|
+
|
|
978
|
+
Benefits:
|
|
979
|
+
|
|
980
|
+
* Low memory usage (results processed immediately)
|
|
981
|
+
* Real-time progress tracking
|
|
982
|
+
* Enables streaming results
|
|
983
|
+
|
|
984
|
+
==== Callback-based collection
|
|
985
|
+
|
|
986
|
+
In continuous mode with [`ContinuousServer`](../../lib/fractor/continuous_server.rb):
|
|
987
|
+
|
|
988
|
+
[source,ruby]
|
|
989
|
+
----
|
|
990
|
+
server.on_result do |work_result|
|
|
991
|
+
# Process successful result immediately
|
|
992
|
+
puts "Processed: #{work_result.result}"
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
server.on_error do |error_result|
|
|
996
|
+
# Handle error immediately
|
|
997
|
+
log_error(error_result.error)
|
|
998
|
+
end
|
|
999
|
+
----
|
|
1000
|
+
|
|
1001
|
+
Benefits:
|
|
1002
|
+
|
|
1003
|
+
* Decouples result processing from work execution
|
|
1004
|
+
* Enables real-time notifications
|
|
1005
|
+
* Supports multiple result handlers
|
|
1006
|
+
|
|
1007
|
+
=== Error propagation paths
|
|
1008
|
+
|
|
1009
|
+
[source]
|
|
1010
|
+
----
|
|
1011
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1012
|
+
│ Error Origin: Worker.process(work) │
|
|
1013
|
+
└──────────────────────────────────────────────────────────┘
|
|
1014
|
+
│
|
|
1015
|
+
│ raise StandardError
|
|
1016
|
+
▼
|
|
1017
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1018
|
+
│ Ractor Rescue Block │
|
|
1019
|
+
│ rescue StandardError => e │
|
|
1020
|
+
│ error_result = WorkResult.new( │
|
|
1021
|
+
│ error: e.message, │
|
|
1022
|
+
│ work: work, │
|
|
1023
|
+
│ error_category: :auto_detected, │
|
|
1024
|
+
│ error_severity: :error │
|
|
1025
|
+
│ ) │
|
|
1026
|
+
│ Ractor.yield({ type: :error, result: error_result }) │
|
|
1027
|
+
└──────────────────────────────────────────────────────────┘
|
|
1028
|
+
│
|
|
1029
|
+
▼
|
|
1030
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1031
|
+
│ Supervisor.run Event Loop │
|
|
1032
|
+
│ when :error │
|
|
1033
|
+
│ results.add_result(error_result) │
|
|
1034
|
+
│ send_next_work_if_available(ractor) │
|
|
1035
|
+
└──────────────────────────────────────────────────────────┘
|
|
1036
|
+
│
|
|
1037
|
+
▼
|
|
1038
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1039
|
+
│ ResultAggregator │
|
|
1040
|
+
│ @errors << error_result │
|
|
1041
|
+
│ call error callbacks │
|
|
1042
|
+
└──────────────────────────────────────────────────────────┘
|
|
1043
|
+
│
|
|
1044
|
+
▼
|
|
1045
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1046
|
+
│ Client / Error Handlers │
|
|
1047
|
+
│ - Log errors │
|
|
1048
|
+
│ - Retry logic │
|
|
1049
|
+
│ - Dead letter queue │
|
|
1050
|
+
│ - Monitoring/alerting │
|
|
1051
|
+
└──────────────────────────────────────────────────────────┘
|
|
1052
|
+
----
|
|
1053
|
+
|
|
1054
|
+
Error handling guarantees:
|
|
1055
|
+
|
|
1056
|
+
* Errors never crash Ractors (isolated)
|
|
1057
|
+
* All errors captured in [`WorkResult`](../../lib/fractor/work_result.rb)
|
|
1058
|
+
* Original work preserved for retry
|
|
1059
|
+
* Error categorization enables smart handling
|
|
1060
|
+
* No silent failures
|
|
1061
|
+
|
|
1062
|
+
== Design patterns
|
|
1063
|
+
|
|
1064
|
+
=== Actor model
|
|
1065
|
+
|
|
1066
|
+
Fractor implements the Actor model for concurrent computation:
|
|
1067
|
+
|
|
1068
|
+
[source]
|
|
1069
|
+
----
|
|
1070
|
+
Actor = WrappedRactor + Worker
|
|
1071
|
+
|
|
1072
|
+
┌────────────────────────────────────────┐
|
|
1073
|
+
│ Actor (Ractor) │
|
|
1074
|
+
│ ┌──────────────────────────────────┐ │
|
|
1075
|
+
│ │ Isolated State │ │
|
|
1076
|
+
│ │ - Worker instance │ │
|
|
1077
|
+
│ │ - Local variables │ │
|
|
1078
|
+
│ │ - No shared memory │ │
|
|
1079
|
+
│ └──────────────────────────────────┘ │
|
|
1080
|
+
│ │ │
|
|
1081
|
+
│ ▼ │
|
|
1082
|
+
│ ┌──────────────────────────────────┐ │
|
|
1083
|
+
│ │ Message Processing │ │
|
|
1084
|
+
│ │ - Receive work │ │
|
|
1085
|
+
│ │ - Process sequentially │ │
|
|
1086
|
+
│ │ - Send results │ │
|
|
1087
|
+
│ └──────────────────────────────────┘ │
|
|
1088
|
+
│ │ │
|
|
1089
|
+
│ ▼ │
|
|
1090
|
+
│ ┌──────────────────────────────────┐ │
|
|
1091
|
+
│ │ Behavior (process method) │ │
|
|
1092
|
+
│ │ - User-defined logic │ │
|
|
1093
|
+
│ │ - Stateless or stateful │ │
|
|
1094
|
+
│ └──────────────────────────────────┘ │
|
|
1095
|
+
└────────────────────────────────────────┘
|
|
1096
|
+
----
|
|
1097
|
+
|
|
1098
|
+
Actor model benefits in Fractor:
|
|
1099
|
+
|
|
1100
|
+
* *Encapsulation*: Each actor maintains its own state
|
|
1101
|
+
* *Location transparency*: Actors communicate only via messages
|
|
1102
|
+
* *Fault isolation*: Failures contained within actors
|
|
1103
|
+
* *Scalability*: Easy to add more actors
|
|
1104
|
+
|
|
1105
|
+
=== Producer-consumer pattern
|
|
1106
|
+
|
|
1107
|
+
The [`Supervisor`](../../lib/fractor/supervisor.rb) acts as a producer-consumer coordinator:
|
|
1108
|
+
|
|
1109
|
+
[source]
|
|
1110
|
+
----
|
|
1111
|
+
Producers Queue Consumers
|
|
1112
|
+
(Work Sources) (Workers)
|
|
1113
|
+
|
|
1114
|
+
Work 1 ────────┐
|
|
1115
|
+
Work 2 ────────┤ ┌──────┐ ┌─────────┐
|
|
1116
|
+
Work 3 ────────┼────────>│ Work │────────>│Worker 1 │
|
|
1117
|
+
Work N ────────┘ │ Queue│ ├─────────┤
|
|
1118
|
+
└──────┘ │Worker 2 │
|
|
1119
|
+
Callbacks ──────────────────────────────────>├─────────┤
|
|
1120
|
+
- register_work_source() │Worker N │
|
|
1121
|
+
- Continuous polling └─────────┘
|
|
1122
|
+
----
|
|
1123
|
+
|
|
1124
|
+
Implementation:
|
|
1125
|
+
|
|
1126
|
+
[source,ruby]
|
|
1127
|
+
----
|
|
1128
|
+
# Producer: Add work to queue
|
|
1129
|
+
supervisor.add_work_items([work1, work2, work3])
|
|
1130
|
+
|
|
1131
|
+
# Or register dynamic producer
|
|
1132
|
+
supervisor.register_work_source do
|
|
1133
|
+
fetch_new_work_from_api # Returns work array or nil
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Consumers: Workers automatically pull from queue
|
|
1137
|
+
# Supervisor coordinates distribution via Ractor.select
|
|
1138
|
+
----
|
|
1139
|
+
|
|
1140
|
+
=== Supervisor pattern
|
|
1141
|
+
|
|
1142
|
+
The supervisor pattern manages worker lifecycle and fault tolerance:
|
|
1143
|
+
|
|
1144
|
+
[source]
|
|
1145
|
+
----
|
|
1146
|
+
Supervisor
|
|
1147
|
+
│
|
|
1148
|
+
┌─────────────┼─────────────┐
|
|
1149
|
+
│ │ │
|
|
1150
|
+
Manages Monitors Restarts
|
|
1151
|
+
│ │ │
|
|
1152
|
+
▼ ▼ ▼
|
|
1153
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
1154
|
+
│Worker 1 │ │Worker 2 │ │Worker 3 │
|
|
1155
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
1156
|
+
│ │ │
|
|
1157
|
+
Isolated Isolated Isolated
|
|
1158
|
+
State State State
|
|
1159
|
+
----
|
|
1160
|
+
|
|
1161
|
+
Supervisor responsibilities:
|
|
1162
|
+
|
|
1163
|
+
. *Lifecycle management*: Start, stop, monitor workers
|
|
1164
|
+
. *Work distribution*: Route work to available workers
|
|
1165
|
+
. *Result collection*: Aggregate results from all workers
|
|
1166
|
+
. *Signal handling*: Graceful shutdown on SIGINT/SIGTERM
|
|
1167
|
+
. *Error recovery*: Isolate failures, continue processing
|
|
1168
|
+
|
|
1169
|
+
.Supervisor pattern in action
|
|
1170
|
+
[example]
|
|
1171
|
+
====
|
|
1172
|
+
[source,ruby]
|
|
1173
|
+
----
|
|
1174
|
+
supervisor = Fractor::Supervisor.new(
|
|
1175
|
+
worker_pools: [
|
|
1176
|
+
{ worker_class: ProcessWorker, num_workers: 4 }
|
|
1177
|
+
]
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# Supervisor handles:
|
|
1181
|
+
# - Starting 4 worker Ractors
|
|
1182
|
+
# - Distributing work across them
|
|
1183
|
+
# - Collecting results
|
|
1184
|
+
# - Graceful shutdown on Ctrl+C
|
|
1185
|
+
|
|
1186
|
+
supervisor.run
|
|
1187
|
+
----
|
|
1188
|
+
====
|
|
1189
|
+
|
|
1190
|
+
=== Pipeline pattern
|
|
1191
|
+
|
|
1192
|
+
Workflows implement the pipeline pattern for multi-stage processing:
|
|
1193
|
+
|
|
1194
|
+
[source]
|
|
1195
|
+
----
|
|
1196
|
+
Stage 1 Stage 2 Stage 3 Stage 4
|
|
1197
|
+
│ │ │ │
|
|
1198
|
+
▼ ▼ ▼ ▼
|
|
1199
|
+
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
|
1200
|
+
│Parse │───>│Valid.│─────>│Trans.│───>│Store │
|
|
1201
|
+
└──────┘ └──────┘ └──────┘ └──────┘
|
|
1202
|
+
│ │ │ │
|
|
1203
|
+
Workers: Workers: Workers: Workers:
|
|
1204
|
+
3 2 4 2
|
|
1205
|
+
|
|
1206
|
+
Each stage processes output from previous stage
|
|
1207
|
+
Data flows left-to-right through pipeline
|
|
1208
|
+
Parallel workers within each stage
|
|
1209
|
+
----
|
|
1210
|
+
|
|
1211
|
+
Implementation with workflows:
|
|
1212
|
+
|
|
1213
|
+
[source,ruby]
|
|
1214
|
+
----
|
|
1215
|
+
class DataPipeline < Fractor::Workflow
|
|
1216
|
+
workflow "data-pipeline" do
|
|
1217
|
+
job "parse", ParseWorker
|
|
1218
|
+
job "validate", ValidateWorker, needs: "parse"
|
|
1219
|
+
job "transform", TransformWorker, needs: "validate"
|
|
1220
|
+
job "store", StoreWorker, needs: "transform"
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
----
|
|
1224
|
+
|
|
1225
|
+
== Performance characteristics
|
|
1226
|
+
|
|
1227
|
+
=== Parallelization overhead
|
|
1228
|
+
|
|
1229
|
+
Ractor creation and message passing have overhead:
|
|
1230
|
+
|
|
1231
|
+
[source]
|
|
1232
|
+
----
|
|
1233
|
+
Time per work item vs. number of workers:
|
|
1234
|
+
|
|
1235
|
+
Small tasks (< 1ms each):
|
|
1236
|
+
1 worker: 100 items in 100ms (no overhead)
|
|
1237
|
+
4 workers: 100 items in 35ms (overhead ~15ms)
|
|
1238
|
+
8 workers: 100 items in 25ms (overhead ~20ms)
|
|
1239
|
+
|
|
1240
|
+
Medium tasks (~10ms each):
|
|
1241
|
+
1 worker: 100 items in 1000ms
|
|
1242
|
+
4 workers: 100 items in 260ms (good scaling)
|
|
1243
|
+
8 workers: 100 items in 140ms (good scaling)
|
|
1244
|
+
|
|
1245
|
+
Large tasks (> 100ms each):
|
|
1246
|
+
1 worker: 100 items in 10000ms
|
|
1247
|
+
4 workers: 100 items in 2510ms (near-linear scaling)
|
|
1248
|
+
8 workers: 100 items in 1260ms (near-linear scaling)
|
|
1249
|
+
----
|
|
1250
|
+
|
|
1251
|
+
*Guideline*: Use Fractor when work items take > 5ms each
|
|
1252
|
+
|
|
1253
|
+
=== Optimal worker count
|
|
1254
|
+
|
|
1255
|
+
Optimal worker count depends on workload type:
|
|
1256
|
+
|
|
1257
|
+
[cols="2,2,3"]
|
|
1258
|
+
|===
|
|
1259
|
+
|Workload Type |Recommended Workers |Reasoning
|
|
1260
|
+
|
|
1261
|
+
|CPU-bound
|
|
1262
|
+
|Number of CPU cores
|
|
1263
|
+
|Avoids CPU oversubscription
|
|
1264
|
+
|
|
1265
|
+
|I/O-bound
|
|
1266
|
+
|2-4× CPU cores
|
|
1267
|
+
|Workers block on I/O, can handle more
|
|
1268
|
+
|
|
1269
|
+
|Mixed workload
|
|
1270
|
+
|1.5-2× CPU cores
|
|
1271
|
+
|Balance between CPU and I/O
|
|
1272
|
+
|
|
1273
|
+
|Memory-intensive
|
|
1274
|
+
|< Number of cores
|
|
1275
|
+
|Prevent memory exhaustion
|
|
1276
|
+
|===
|
|
1277
|
+
|
|
1278
|
+
Auto-detection (default):
|
|
1279
|
+
|
|
1280
|
+
[source,ruby]
|
|
1281
|
+
----
|
|
1282
|
+
# Fractor auto-detects CPU cores using Etc.nprocessors
|
|
1283
|
+
supervisor = Fractor::Supervisor.new(
|
|
1284
|
+
worker_pools: [{ worker_class: MyWorker }]
|
|
1285
|
+
# Defaults to Etc.nprocessors workers
|
|
1286
|
+
)
|
|
1287
|
+
----
|
|
1288
|
+
|
|
1289
|
+
Manual configuration:
|
|
1290
|
+
|
|
1291
|
+
[source,ruby]
|
|
1292
|
+
----
|
|
1293
|
+
supervisor = Fractor::Supervisor.new(
|
|
1294
|
+
worker_pools: [
|
|
1295
|
+
{ worker_class: CPUWorker, num_workers: 4 }, # CPU-bound
|
|
1296
|
+
{ worker_class: IOWorker, num_workers: 16 }, # I/O-bound
|
|
1297
|
+
{ worker_class: MixedWorker, num_workers: 8 } # Mixed
|
|
1298
|
+
]
|
|
1299
|
+
)
|
|
1300
|
+
----
|
|
1301
|
+
|
|
1302
|
+
=== Scalability limits
|
|
1303
|
+
|
|
1304
|
+
Factors limiting scalability:
|
|
1305
|
+
|
|
1306
|
+
. *Hardware constraints*
|
|
1307
|
+
* Number of CPU cores
|
|
1308
|
+
* Available memory per Ractor
|
|
1309
|
+
* Memory bandwidth
|
|
1310
|
+
|
|
1311
|
+
. *Ractor limits*
|
|
1312
|
+
* Each Ractor requires ~1MB base memory
|
|
1313
|
+
* Theoretical limit: thousands of Ractors
|
|
1314
|
+
* Practical limit: hundreds of Ractors
|
|
1315
|
+
|
|
1316
|
+
. *Work queue contention*
|
|
1317
|
+
* Queue operations are thread-safe (mutex)
|
|
1318
|
+
* High contention with many workers
|
|
1319
|
+
* Solution: Batch work distribution
|
|
1320
|
+
|
|
1321
|
+
. *Result aggregation overhead*
|
|
1322
|
+
* Results collected in main thread
|
|
1323
|
+
* Can become bottleneck with high throughput
|
|
1324
|
+
* Solution: Use callbacks for immediate processing
|
|
1325
|
+
|
|
1326
|
+
=== Memory usage patterns
|
|
1327
|
+
|
|
1328
|
+
Memory usage per component:
|
|
1329
|
+
|
|
1330
|
+
[source]
|
|
1331
|
+
----
|
|
1332
|
+
Per Ractor:
|
|
1333
|
+
Base overhead: ~1 MB
|
|
1334
|
+
Worker instance: ~10 KB - 1 MB (depends on worker)
|
|
1335
|
+
Work object: Varies (should be small)
|
|
1336
|
+
Total per Ractor: ~1-2 MB typical
|
|
1337
|
+
|
|
1338
|
+
Per Supervisor:
|
|
1339
|
+
Work queue: ~8 bytes × queue size
|
|
1340
|
+
Result aggregator: ~sizeof(WorkResult) × results
|
|
1341
|
+
Ractor management: ~1 KB × num_ractors
|
|
1342
|
+
|
|
1343
|
+
Example with 8 workers, 1000 items:
|
|
1344
|
+
8 Ractors: 8-16 MB
|
|
1345
|
+
Work queue (empty): ~8 KB
|
|
1346
|
+
Results (1000 items): ~100 KB - 1 MB
|
|
1347
|
+
Total: ~9-18 MB
|
|
1348
|
+
----
|
|
1349
|
+
|
|
1350
|
+
Memory optimization strategies:
|
|
1351
|
+
|
|
1352
|
+
. *Process results immediately*: Don't accumulate results
|
|
1353
|
+
. *Use streaming*: Process data in chunks
|
|
1354
|
+
. *Limit queue size*: Prevent memory exhaustion
|
|
1355
|
+
. *Clean up after processing*: Remove references to allow GC
|
|
1356
|
+
|
|
1357
|
+
=== GC implications
|
|
1358
|
+
|
|
1359
|
+
Ractor's isolated memory affects garbage collection:
|
|
1360
|
+
|
|
1361
|
+
. *Per-Ractor GC*: Each Ractor has independent GC
|
|
1362
|
+
* Parallel GC across Ractors
|
|
1363
|
+
* No global GC pause
|
|
1364
|
+
* Better responsiveness
|
|
1365
|
+
|
|
1366
|
+
. *Message copying*: Objects copied between Ractors
|
|
1367
|
+
* Creates garbage in both Ractors
|
|
1368
|
+
* Keep messages small and simple
|
|
1369
|
+
* Prefer immutable objects
|
|
1370
|
+
|
|
1371
|
+
. *GC tuning*: Ruby GC environment variables apply per-Ractor
|
|
1372
|
+
* `RUBY_GC_HEAP_GROWTH_FACTOR`
|
|
1373
|
+
* `RUBY_GC_HEAP_GROWTH_MAX_SLOTS`
|
|
1374
|
+
* `RUBY_GC_MALLOC_LIMIT`
|
|
1375
|
+
|
|
1376
|
+
Best practices:
|
|
1377
|
+
|
|
1378
|
+
* Minimize object allocations in hot paths
|
|
1379
|
+
* Reuse objects where possible (within Ractor)
|
|
1380
|
+
* Keep work objects small and simple
|
|
1381
|
+
* Monitor memory usage in production
|
|
1382
|
+
|
|
1383
|
+
== Next steps
|
|
1384
|
+
|
|
1385
|
+
* Learn about link:../guides/pipeline-mode/[Pipeline Mode] patterns and best practices
|
|
1386
|
+
* Learn about link:../guides/continuous-mode/[Continuous Mode] for long-running applications
|
|
1387
|
+
* Read link:../architecture/[Architecture Guide] for detailed system design
|
|
1388
|
+
* Read link:design-principles/[Design Principles] for philosophy and decisions
|
|
1389
|
+
* Try link:../features/workflows/[Workflows] for complex processing graphs
|
|
1390
|
+
* Monitor errors with link:../features/error-handling/[Error Handling] and analytics
|
|
1391
|
+
* Explore link:../reference/examples/[Examples] for real-world use cases
|
|
1392
|
+
* Reference link:../reference/api/[API Reference] for complete documentation
|