fractor 0.1.6 → 0.1.8

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,730 @@
1
+ ---
2
+ layout: default
3
+ title: Pipeline mode (batch processing)
4
+ nav_order: 3
5
+ ---
6
+ == Pipeline mode (batch processing)
7
+
8
+ == General
9
+
10
+ Pipeline mode is designed for processing a defined set of work items with a clear beginning and end.
11
+
12
+ Characteristics:
13
+
14
+ * Processes a predetermined batch of work items
15
+ * Stops automatically when all work is completed
16
+ * Results are collected and accessed after processing completes
17
+ * Ideal for one-time computations or periodic batch jobs
18
+
19
+ Common use cases:
20
+
21
+ * Processing a file or dataset
22
+ * Batch data transformations
23
+ * One-time parallel computations
24
+ * Scheduled batch jobs
25
+ * Hierarchical or multi-stage processing
26
+
27
+ == Quick start
28
+
29
+ === General
30
+
31
+ This quick start guide shows the minimum steps needed to get parallel batch processing working with Fractor.
32
+
33
+ === Step 1: Create a minimal Work class
34
+
35
+ The Work class represents a unit of work to be processed by a Worker. It encapsulates the input data needed for processing.
36
+
37
+ [source,ruby]
38
+ ----
39
+ require 'fractor'
40
+
41
+ class MyWork < Fractor::Work
42
+ # Store all properties in the input hash
43
+ def initialize(value)
44
+ super({ value: value })
45
+ end
46
+
47
+ # Accessor method for the stored value
48
+ def value
49
+ input[:value]
50
+ end
51
+
52
+ def to_s
53
+ "MyWork: #{value}"
54
+ end
55
+ end
56
+ ----
57
+
58
+ A Work is instantiated with the input data it will process this way:
59
+
60
+ [source,ruby]
61
+ ----
62
+ work_item = MyWork.new(42)
63
+ puts work_item.to_s # Output: MyWork: 42
64
+ ----
65
+
66
+ === Step 2: Create a minimal Worker class
67
+
68
+ The Worker class defines the processing logic for work items. Each Worker instance runs within its own Ractor and processes Work objects sent to it.
69
+
70
+ It must implement the `process(work)` method, which takes a Work object as input and returns a `Fractor::WorkResult` object.
71
+
72
+ The `process` method should handle both successful processing and error conditions.
73
+
74
+ [source,ruby]
75
+ ----
76
+ class MyWorker < Fractor::Worker
77
+ def process(work)
78
+ # Your processing logic here
79
+ result = work.input * 2
80
+
81
+ # Return a success result
82
+ Fractor::WorkResult.new(result: result, work: work)
83
+ rescue => e
84
+ # Return an error result if something goes wrong
85
+ Fractor::WorkResult.new(error: e.message, work: work)
86
+ end
87
+ end
88
+ ----
89
+
90
+ The `process` method can perform any computation you need. In this example, it multiplies the input by 2. If an error occurs, it catches the exception and returns an error result.
91
+
92
+ === Step 3: Set up and run the Supervisor
93
+
94
+ The Supervisor class orchestrates the entire framework, managing worker Ractors, distributing work, and collecting results.
95
+
96
+ [source,ruby]
97
+ ----
98
+ # Create the supervisor with auto-detected number of workers
99
+ supervisor = Fractor::Supervisor.new(
100
+ worker_pools: [
101
+ { worker_class: MyWorker } # Number of workers auto-detected
102
+ ]
103
+ )
104
+
105
+ # Add work items (instances of Work subclasses)
106
+ supervisor.add_work_items([
107
+ MyWork.new(1),
108
+ MyWork.new(2),
109
+ MyWork.new(3),
110
+ MyWork.new(4),
111
+ MyWork.new(5)
112
+ ])
113
+
114
+ # Run the processing
115
+ supervisor.run
116
+
117
+ # Access results after completion
118
+ puts "Results: #{supervisor.results.results.map(&:result)}"
119
+ puts "Errors: #{supervisor.results.errors.size}"
120
+ ----
121
+
122
+ That's it! With these three simple steps, you have a working parallel processing system using Fractor in pipeline mode.
123
+
124
+ == Pipeline mode components
125
+
126
+ === General
127
+
128
+ This section describes the components and their detailed usage specifically for pipeline mode (batch processing). For continuous mode, see the link:continuous-mode/[Continuous Mode] documentation.
129
+
130
+ Pipeline mode uses only the core components without any additional primitives.
131
+
132
+ === Work class
133
+
134
+ ==== Purpose and responsibilities
135
+
136
+ The `Fractor::Work` class represents a unit of work to be processed by a Worker. Its primary responsibility is to encapsulate the input data needed for processing.
137
+
138
+ ==== Implementation requirements
139
+
140
+ At minimum, your Work subclass should:
141
+
142
+ . Inherit from `Fractor::Work`
143
+ . Pass the input data to the superclass constructor
144
+
145
+ [source,ruby]
146
+ ----
147
+ class MyWork < Fractor::Work
148
+ def initialize(input)
149
+ super(input) # This stores input in @input
150
+ # Add any additional initialization if needed
151
+ end
152
+ end
153
+ ----
154
+
155
+ ==== Advanced usage
156
+
157
+ You can extend your Work class to include additional data or methods:
158
+
159
+ [source,ruby]
160
+ ----
161
+ class ComplexWork < Fractor::Work
162
+ attr_reader :options
163
+
164
+ def initialize(input, options = {})
165
+ super(input)
166
+ @options = options
167
+ end
168
+
169
+ def high_priority?
170
+ @options[:priority] == :high
171
+ end
172
+
173
+ def to_s
174
+ "ComplexWork: #{@input} (#{@options[:priority]} priority)"
175
+ end
176
+ end
177
+ ----
178
+
179
+ [TIP]
180
+ ====
181
+ * Keep Work objects lightweight and serializable since they will be passed between Ractors
182
+ * Implement a meaningful `to_s` method for better debugging
183
+ * Consider adding validation in the initializer to catch issues early
184
+ * Use module namespacing to avoid class name collisions in larger applications
185
+ ====
186
+
187
+ ===== Module namespacing best practices
188
+
189
+ When building larger applications or libraries with Fractor, wrap your Work and Worker classes in modules to avoid naming collisions:
190
+
191
+ [source,ruby]
192
+ ----
193
+ module ImageProcessor
194
+ class ImageWork < Fractor::Work
195
+ def initialize(image_path, options = {})
196
+ super({ path: image_path, options: options })
197
+ end
198
+
199
+ def path
200
+ input[:path]
201
+ end
202
+ end
203
+
204
+ class ImageProcessorWorker < Fractor::Worker
205
+ def initialize(name: nil)
206
+ # Worker initialization
207
+ end
208
+
209
+ def process(work)
210
+ # Process the image
211
+ result = process_image(work.path)
212
+ Fractor::WorkResult.new(result: result, work: work)
213
+ end
214
+ end
215
+ end
216
+
217
+ # Usage
218
+ supervisor = Fractor::Supervisor.new(
219
+ worker_pools: [
220
+ { worker_class: ImageProcessor::ImageProcessorWorker }
221
+ ]
222
+ )
223
+
224
+ supervisor.add_work_items([
225
+ ImageProcessor::ImageWork.new("photo.jpg", { resize: "800x600" })
226
+ ])
227
+ ----
228
+
229
+ === Worker class
230
+
231
+ ==== Purpose and responsibilities
232
+
233
+ The `Fractor::Worker` class defines the processing logic for work items. Each Worker instance runs within its own Ractor and processes Work objects sent to it.
234
+
235
+ ==== Implementation requirements
236
+
237
+ Your Worker subclass must:
238
+
239
+ . Inherit from `Fractor::Worker`
240
+ . Implement the `process(work)` method
241
+ . Return a `Fractor::WorkResult` object from the `process` method
242
+ . Handle both successful processing and error conditions
243
+
244
+ [TIP]
245
+ ====
246
+ Workers are instantiated inside Ractors by the framework. The `initialize` method receives an optional `name:` parameter. You can use this for debugging or worker identification:
247
+
248
+ [source,ruby]
249
+ ----
250
+ class MyWorker < Fractor::Worker
251
+ def initialize(name: nil) # Accept optional name parameter
252
+ @worker_name = name
253
+ end
254
+
255
+ def process(work)
256
+ puts "Worker #{@worker_name} processing #{work.input}" if @worker_name
257
+ result = work.input * 2
258
+ Fractor::WorkResult.new(result: result, work: work)
259
+ end
260
+ end
261
+ ----
262
+ ====
263
+
264
+ A complete Worker example with error handling:
265
+
266
+ [source,ruby]
267
+ ----
268
+ class MyWorker < Fractor::Worker
269
+ def initialize(name: nil) # Accept optional name parameter
270
+ @debug = name ? true : false
271
+ end
272
+
273
+ def process(work)
274
+ if work.input < 0
275
+ return Fractor::WorkResult.new(
276
+ error: "Cannot process negative numbers",
277
+ work: work
278
+ )
279
+ end
280
+
281
+ # Normal processing...
282
+ result = work.input * 2
283
+
284
+ # Return a WorkResult
285
+ Fractor::WorkResult.new(result: result, work: work)
286
+ end
287
+ end
288
+ ----
289
+
290
+ ==== Error handling
291
+
292
+ The Worker class should handle two types of errors.
293
+
294
+ ===== Handled errors
295
+
296
+ These are expected error conditions that your code explicitly checks for.
297
+
298
+ [source,ruby]
299
+ ----
300
+ def process(work)
301
+ if work.input < 0
302
+ return Fractor::WorkResult.new(
303
+ error: "Cannot process negative numbers",
304
+ work: work
305
+ )
306
+ end
307
+
308
+ # Normal processing...
309
+ Fractor::WorkResult.new(result: calculated_value, work: work)
310
+ end
311
+ ----
312
+
313
+ ===== Unexpected errors caught by rescue
314
+
315
+ These are unexpected exceptions that may occur during processing. You should catch these and convert them into error results.
316
+
317
+ [source,ruby]
318
+ ----
319
+ def process(work)
320
+ # Processing that might raise exceptions
321
+ result = complex_calculation(work.input)
322
+
323
+ Fractor::WorkResult.new(result: result, work: work)
324
+ rescue StandardError => e
325
+ # Catch and convert any unexpected exceptions to error results
326
+ Fractor::WorkResult.new(
327
+ error: "An unexpected error occurred: #{e.message}",
328
+ work: work
329
+ )
330
+ end
331
+ ----
332
+
333
+ [TIP]
334
+ ====
335
+ * Keep the `process` method focused on a single responsibility
336
+ * Use meaningful error messages that help diagnose issues
337
+ * Consider adding logging within the `process` method for debugging
338
+ * Ensure all paths return a valid `WorkResult` object
339
+ ====
340
+
341
+ === Supervisor class for pipeline mode
342
+
343
+ ==== Purpose and responsibilities
344
+
345
+ The `Fractor::Supervisor` class orchestrates the entire framework, managing worker Ractors, distributing work, and collecting results.
346
+
347
+ ==== Configuration options
348
+
349
+ When creating a Supervisor for pipeline mode, configure worker pools:
350
+
351
+ [source,ruby]
352
+ ----
353
+ supervisor = Fractor::Supervisor.new(
354
+ worker_pools: [
355
+ # Pool 1 - for general data processing
356
+ { worker_class: MyWorker, num_workers: 4 },
357
+
358
+ # Pool 2 - for specialized image processing
359
+ { worker_class: ImageWorker, num_workers: 2 }
360
+ ]
361
+ # Note: continuous_mode defaults to false for pipeline mode
362
+ )
363
+ ----
364
+
365
+ ==== Worker auto-detection
366
+
367
+ Fractor automatically detects the number of available processors on your system and uses that value when `num_workers` is not specified. This provides optimal resource utilization across different deployment environments without requiring manual configuration.
368
+
369
+ [source,ruby]
370
+ ----
371
+ # Auto-detect number of workers (recommended for most cases)
372
+ supervisor = Fractor::Supervisor.new(
373
+ worker_pools: [
374
+ { worker_class: MyWorker } # Will use number of available processors
375
+ ]
376
+ )
377
+
378
+ # Explicitly set number of workers (useful for specific requirements)
379
+ supervisor = Fractor::Supervisor.new(
380
+ worker_pools: [
381
+ { worker_class: MyWorker, num_workers: 4 } # Always use exactly 4 workers
382
+ ]
383
+ )
384
+
385
+ # Mix auto-detection and explicit configuration
386
+ supervisor = Fractor::Supervisor.new(
387
+ worker_pools: [
388
+ { worker_class: FastWorker }, # Auto-detected
389
+ { worker_class: HeavyWorker, num_workers: 2 } # Explicitly 2 workers
390
+ ]
391
+ )
392
+ ----
393
+
394
+ The auto-detection uses Ruby's `Etc.nprocessors` which returns the number of available processors. If detection fails for any reason, it falls back to 2 workers.
395
+
396
+ [TIP]
397
+ ====
398
+ * Use auto-detection for portable code that adapts to different environments
399
+ * Explicitly set `num_workers` when you need precise control over resource usage
400
+ * Consider system load and other factors when choosing explicit values
401
+ ====
402
+
403
+ ==== Adding work
404
+
405
+ You can add work items individually or in batches:
406
+
407
+ [source,ruby]
408
+ ----
409
+ # Add a single item
410
+ supervisor.add_work_item(MyWork.new(42))
411
+
412
+ # Add multiple items
413
+ supervisor.add_work_items([
414
+ MyWork.new(1),
415
+ MyWork.new(2),
416
+ MyWork.new(3),
417
+ MyWork.new(4),
418
+ MyWork.new(5)
419
+ ])
420
+
421
+ # Add items of different work types
422
+ supervisor.add_work_items([
423
+ TextWork.new("Process this text"),
424
+ ImageWork.new({ width: 800, height: 600 })
425
+ ])
426
+ ----
427
+
428
+ The Supervisor can handle any Work object that inherits from Fractor::Work. Workers must check the type of Work they receive and process it accordingly.
429
+
430
+ ==== Running and monitoring
431
+
432
+ To start processing:
433
+
434
+ [source,ruby]
435
+ ----
436
+ # Start processing and block until complete
437
+ supervisor.run
438
+ ----
439
+
440
+ The Supervisor automatically handles:
441
+
442
+ * Starting the worker Ractors
443
+ * Distributing work items to available workers
444
+ * Collecting results and errors
445
+ * Graceful shutdown on completion or interruption (Ctrl+C)
446
+
447
+ === ResultAggregator for pipeline mode
448
+
449
+ ==== Purpose and responsibilities
450
+
451
+ The `Fractor::ResultAggregator` collects and organizes all results from the workers, separating successful results from errors.
452
+
453
+ In pipeline mode, results are collected throughout processing and accessed after the supervisor finishes running.
454
+
455
+ ==== Accessing results
456
+
457
+ After processing completes:
458
+
459
+ [source,ruby]
460
+ ----
461
+ # Get the ResultAggregator
462
+ aggregator = supervisor.results
463
+
464
+ # Check counts
465
+ puts "Processed #{aggregator.results.size} items successfully"
466
+ puts "Encountered #{aggregator.errors.size} errors"
467
+
468
+ # Access successful results
469
+ aggregator.results.each do |result|
470
+ puts "Work item #{result.work.input} produced #{result.result}"
471
+ end
472
+
473
+ # Access errors
474
+ aggregator.errors.each do |error_result|
475
+ puts "Work item #{error_result.work.input} failed: #{error_result.error}"
476
+ end
477
+ ----
478
+
479
+ To access successful results:
480
+
481
+ [source,ruby]
482
+ ----
483
+ # Get all successful results
484
+ successful_results = supervisor.results.results
485
+
486
+ # Extract just the result values
487
+ result_values = successful_results.map(&:result)
488
+ ----
489
+
490
+ To access errors:
491
+
492
+ [source,ruby]
493
+ ----
494
+ # Get all error results
495
+ error_results = supervisor.results.errors
496
+
497
+ # Extract error messages
498
+ error_messages = error_results.map(&:error)
499
+
500
+ # Get the work items that failed
501
+ failed_work_items = error_results.map(&:work)
502
+ ----
503
+
504
+ [TIP]
505
+ ====
506
+ * Check both successful results and errors after processing completes
507
+ * Consider implementing custom reporting based on the aggregated results
508
+ ====
509
+
510
+ == Pipeline mode patterns
511
+
512
+ === Custom work distribution
513
+
514
+ For more complex scenarios, you might want to prioritize certain work items:
515
+
516
+ [source,ruby]
517
+ ----
518
+ # Create Work objects for high priority items
519
+ high_priority_works = high_priority_items.map { |item| MyWork.new(item) }
520
+
521
+ # Add high-priority items first
522
+ supervisor.add_work_items(high_priority_works)
523
+
524
+ # Run with just enough workers for high-priority items
525
+ supervisor.run
526
+
527
+ # Create Work objects for lower priority items
528
+ low_priority_works = low_priority_items.map { |item| MyWork.new(item) }
529
+
530
+ # Add and process lower-priority items
531
+ supervisor.add_work_items(low_priority_works)
532
+ supervisor.run
533
+ ----
534
+
535
+ === Handling large datasets
536
+
537
+ For very large datasets, consider processing in batches:
538
+
539
+ [source,ruby]
540
+ ----
541
+ large_dataset.each_slice(1000) do |batch|
542
+ # Convert batch items to Work objects
543
+ work_batch = batch.map { |item| MyWork.new(item) }
544
+
545
+ supervisor.add_work_items(work_batch)
546
+ supervisor.run
547
+
548
+ # Process this batch's results before continuing
549
+ process_batch_results(supervisor.results)
550
+ end
551
+ ----
552
+
553
+ === Multi-work type processing
554
+
555
+ The Multi-Work Type pattern demonstrates how a single supervisor and worker can handle multiple types of work items.
556
+
557
+ [source,ruby]
558
+ ----
559
+ class UniversalWorker < Fractor::Worker
560
+ def process(work)
561
+ case work
562
+ when TextWork
563
+ process_text(work)
564
+ when ImageWork
565
+ process_image(work)
566
+ else
567
+ Fractor::WorkResult.new(
568
+ error: "Unknown work type: #{work.class}",
569
+ work: work
570
+ )
571
+ end
572
+ end
573
+
574
+ private
575
+
576
+ def process_text(work)
577
+ result = work.text.upcase
578
+ Fractor::WorkResult.new(result: result, work: work)
579
+ end
580
+
581
+ def process_image(work)
582
+ result = { width: work.width * 2, height: work.height * 2 }
583
+ Fractor::WorkResult.new(result: result, work: work)
584
+ end
585
+ end
586
+
587
+ # Add different types of work
588
+ supervisor.add_work_items([
589
+ TextWork.new("hello"),
590
+ ImageWork.new(width: 100, height: 100),
591
+ TextWork.new("world")
592
+ ])
593
+ ----
594
+
595
+ === Hierarchical work processing
596
+
597
+ The Producer/Subscriber pattern showcases processing that generates sub-work:
598
+
599
+ [source,ruby]
600
+ ----
601
+ # First pass: Process documents
602
+ supervisor.add_work_items(documents.map { |doc| DocumentWork.new(doc) })
603
+ supervisor.run
604
+
605
+ # Collect sections generated from documents
606
+ sections = supervisor.results.results.flat_map do |result|
607
+ result.result[:sections]
608
+ end
609
+
610
+ # Second pass: Process sections
611
+ supervisor.add_work_items(sections.map { |section| SectionWork.new(section) })
612
+ supervisor.run
613
+ ----
614
+
615
+ === Pipeline stages
616
+
617
+ The Pipeline Processing pattern implements multi-stage transformation:
618
+
619
+ [source,ruby]
620
+ ----
621
+ # Stage 1: Extract data
622
+ supervisor1 = Fractor::Supervisor.new(
623
+ worker_pools: [{ worker_class: ExtractionWorker }]
624
+ )
625
+ supervisor1.add_work_items(raw_data.map { |d| ExtractionWork.new(d) })
626
+ supervisor1.run
627
+ extracted = supervisor1.results.results.map(&:result)
628
+
629
+ # Stage 2: Transform data
630
+ supervisor2 = Fractor::Supervisor.new(
631
+ worker_pools: [{ worker_class: TransformWorker }]
632
+ )
633
+ supervisor2.add_work_items(extracted.map { |e| TransformWork.new(e) })
634
+ supervisor2.run
635
+ transformed = supervisor2.results.results.map(&:result)
636
+
637
+ # Stage 3: Load data
638
+ supervisor3 = Fractor::Supervisor.new(
639
+ worker_pools: [{ worker_class: LoadWorker }]
640
+ )
641
+ supervisor3.add_work_items(transformed.map { |t| LoadWork.new(t) })
642
+ supervisor3.run
643
+ ----
644
+
645
+ == Pipeline mode examples
646
+
647
+ === Simple example
648
+
649
+ The Simple Example (link:../examples/simple/[examples/simple/]) demonstrates the basic usage of the Fractor framework. It shows how to create a simple Work class, a Worker class, and a Supervisor to manage the processing of work items in parallel.
650
+
651
+ Key features:
652
+
653
+ * Basic Work and Worker class implementation
654
+ * Simple Supervisor setup
655
+ * Parallel processing of work items
656
+ * Error handling and result aggregation
657
+ * Auto-detection of available processors
658
+ * Graceful shutdown on completion
659
+
660
+ === Auto-detection example
661
+
662
+ The Auto-Detection Example (link:../examples/auto_detection/[examples/auto_detection/]) demonstrates Fractor's automatic worker detection feature. It shows how to use auto-detection, explicit configuration, and mixed approaches for controlling the number of workers.
663
+
664
+ Key features:
665
+
666
+ * Automatic detection of available processors
667
+ * Comparison of auto-detection vs explicit configuration
668
+ * Mixed configuration with multiple worker pools
669
+ * Best practices for worker configuration
670
+ * Portable code that adapts to different environments
671
+
672
+ === Hierarchical hasher
673
+
674
+ The Hierarchical Hasher example (link:../examples/hierarchical_hasher/[examples/hierarchical_hasher/]) demonstrates how to use the Fractor framework to process a file in parallel by breaking it into chunks, hashing each chunk independently, and then combining the results into a final hash.
675
+
676
+ Key features:
677
+
678
+ * Parallel data chunking for large files
679
+ * Independent processing of data segments
680
+ * Aggregation of results to form a final output
681
+
682
+ === Multi-work type
683
+
684
+ The Multi-Work Type example (link:../examples/multi_work_type/[examples/multi_work_type/]) demonstrates how a single Fractor supervisor and worker can handle multiple types of work items (e.g., `TextWork` and `ImageWork`).
685
+
686
+ Key features:
687
+
688
+ * Support for multiple `Fractor::Work` subclasses
689
+ * Polymorphic worker processing based on work type
690
+ * Unified workflow for diverse tasks
691
+
692
+ === Pipeline processing
693
+
694
+ The Pipeline Processing example (link:../examples/pipeline_processing/[examples/pipeline_processing/]) implements a multi-stage processing pipeline where data flows sequentially through a series of transformations.
695
+
696
+ Key features:
697
+
698
+ * Sequential data flow through multiple processing stages
699
+ * Concurrent execution of different pipeline stages
700
+ * Data transformation at each step of the pipeline
701
+
702
+ === Producer/subscriber
703
+
704
+ The Producer/Subscriber example (link:../examples/producer_subscriber/[examples/producer_subscriber/]) showcases a multi-stage document processing system where initial work generates additional sub-work items.
705
+
706
+ Key features:
707
+
708
+ * Implementation of producer-consumer patterns
709
+ * Dynamic generation of sub-work based on initial processing
710
+ * Construction of hierarchical result structures
711
+
712
+ === Scatter/gather
713
+
714
+ The Scatter/Gather example (link:../examples/scatter_gather/[examples/scatter_gather/]) illustrates how a large task is broken down (scattered) into smaller, independent subtasks that are processed in parallel.
715
+
716
+ Key features:
717
+
718
+ * Distribution of a large task into smaller, parallelizable subtasks
719
+ * Concurrent processing of subtasks
720
+ * Aggregation of partial results into a final result
721
+
722
+ === Specialized workers
723
+
724
+ The Specialized Workers example (link:../examples/specialized_workers/[examples/specialized_workers/]) demonstrates creating distinct worker types, each tailored to handle specific kinds of tasks.
725
+
726
+ Key features:
727
+
728
+ * Creation of worker classes for specific processing domains
729
+ * Routing of work items to appropriately specialized workers
730
+ * Optimization of resources and logic per task type