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.
- checksums.yaml +4 -4
- data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- 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/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- 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/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- 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/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- 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 +183 -0
- 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 +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- 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 +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- 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 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- 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
|