fractor 0.1.4 → 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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. 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