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
|
@@ -42,7 +42,7 @@ module ScatterGather
|
|
|
42
42
|
error = ArgumentError.new("Unknown source: #{work.source}")
|
|
43
43
|
return Fractor::WorkResult.new(
|
|
44
44
|
error: error,
|
|
45
|
-
work: work
|
|
45
|
+
work: work,
|
|
46
46
|
)
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -53,9 +53,9 @@ module ScatterGather
|
|
|
53
53
|
query: work.query,
|
|
54
54
|
hits: result[:hits],
|
|
55
55
|
metadata: result[:metadata],
|
|
56
|
-
timing: result[:timing]
|
|
56
|
+
timing: result[:timing],
|
|
57
57
|
},
|
|
58
|
-
work: work
|
|
58
|
+
work: work,
|
|
59
59
|
)
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -72,12 +72,12 @@ module ScatterGather
|
|
|
72
72
|
|
|
73
73
|
# Generate simulated records
|
|
74
74
|
record_count = rand(3..10)
|
|
75
|
-
hits = record_count
|
|
75
|
+
hits = Array.new(record_count) do |i|
|
|
76
76
|
{
|
|
77
77
|
id: "db-#{i + 1}",
|
|
78
78
|
title: "Database Result #{i + 1} for '#{work.query}'",
|
|
79
79
|
content: "This is database content for #{work.query}",
|
|
80
|
-
relevance: rand(0.1..1.0).round(2)
|
|
80
|
+
relevance: rand(0.1..1.0).round(2),
|
|
81
81
|
}
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -86,9 +86,9 @@ module ScatterGather
|
|
|
86
86
|
metadata: {
|
|
87
87
|
source_type: "PostgreSQL Database",
|
|
88
88
|
total_available: record_count + rand(10..50),
|
|
89
|
-
query_type: "Full-text search"
|
|
89
|
+
query_type: "Full-text search",
|
|
90
90
|
},
|
|
91
|
-
timing: rand(0.01..0.3).round(3)
|
|
91
|
+
timing: rand(0.01..0.3).round(3),
|
|
92
92
|
}
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -98,12 +98,12 @@ module ScatterGather
|
|
|
98
98
|
|
|
99
99
|
# Generate simulated API results
|
|
100
100
|
record_count = rand(2..8)
|
|
101
|
-
hits = record_count
|
|
101
|
+
hits = Array.new(record_count) do |i|
|
|
102
102
|
{
|
|
103
103
|
id: "api-#{i + 1}",
|
|
104
104
|
title: "API Result #{i + 1} for '#{work.query}'",
|
|
105
105
|
content: "This is API content for #{work.query}",
|
|
106
|
-
relevance: rand(0.1..1.0).round(2)
|
|
106
|
+
relevance: rand(0.1..1.0).round(2),
|
|
107
107
|
}
|
|
108
108
|
end
|
|
109
109
|
|
|
@@ -112,9 +112,9 @@ module ScatterGather
|
|
|
112
112
|
metadata: {
|
|
113
113
|
source_type: "External REST API",
|
|
114
114
|
provider: %w[Google Bing DuckDuckGo].sample,
|
|
115
|
-
response_code: 200
|
|
115
|
+
response_code: 200,
|
|
116
116
|
},
|
|
117
|
-
timing: rand(0.1..0.5).round(3)
|
|
117
|
+
timing: rand(0.1..0.5).round(3),
|
|
118
118
|
}
|
|
119
119
|
end
|
|
120
120
|
|
|
@@ -128,12 +128,12 @@ module ScatterGather
|
|
|
128
128
|
if cache_hit
|
|
129
129
|
# Cache hit - return cached results
|
|
130
130
|
record_count = rand(1..5)
|
|
131
|
-
hits = record_count
|
|
131
|
+
hits = Array.new(record_count) do |i|
|
|
132
132
|
{
|
|
133
133
|
id: "cache-#{i + 1}",
|
|
134
134
|
title: "Cached Result #{i + 1} for '#{work.query}'",
|
|
135
135
|
content: "This is cached content for #{work.query}",
|
|
136
|
-
relevance: rand(0.1..1.0).round(2)
|
|
136
|
+
relevance: rand(0.1..1.0).round(2),
|
|
137
137
|
}
|
|
138
138
|
end
|
|
139
139
|
|
|
@@ -142,9 +142,9 @@ module ScatterGather
|
|
|
142
142
|
metadata: {
|
|
143
143
|
source_type: "In-memory Cache",
|
|
144
144
|
cache_hit: true,
|
|
145
|
-
age: rand(1..3600)
|
|
145
|
+
age: rand(1..3600),
|
|
146
146
|
},
|
|
147
|
-
timing: rand(0.001..0.05).round(3)
|
|
147
|
+
timing: rand(0.001..0.05).round(3),
|
|
148
148
|
}
|
|
149
149
|
else
|
|
150
150
|
# Cache miss
|
|
@@ -152,9 +152,9 @@ module ScatterGather
|
|
|
152
152
|
hits: [],
|
|
153
153
|
metadata: {
|
|
154
154
|
source_type: "In-memory Cache",
|
|
155
|
-
cache_hit: false
|
|
155
|
+
cache_hit: false,
|
|
156
156
|
},
|
|
157
|
-
timing: rand(0.001..0.01).round(3)
|
|
157
|
+
timing: rand(0.001..0.01).round(3),
|
|
158
158
|
}
|
|
159
159
|
end
|
|
160
160
|
end
|
|
@@ -165,13 +165,13 @@ module ScatterGather
|
|
|
165
165
|
|
|
166
166
|
# Generate simulated file results
|
|
167
167
|
record_count = rand(1..12)
|
|
168
|
-
hits = record_count
|
|
168
|
+
hits = Array.new(record_count) do |i|
|
|
169
169
|
{
|
|
170
170
|
id: "file-#{i + 1}",
|
|
171
171
|
title: "File Result #{i + 1} for '#{work.query}'",
|
|
172
172
|
path: "/path/to/file_#{i + 1}.txt",
|
|
173
173
|
content: "This is file content matching #{work.query}",
|
|
174
|
-
relevance: rand(0.1..1.0).round(2)
|
|
174
|
+
relevance: rand(0.1..1.0).round(2),
|
|
175
175
|
}
|
|
176
176
|
end
|
|
177
177
|
|
|
@@ -180,9 +180,9 @@ module ScatterGather
|
|
|
180
180
|
metadata: {
|
|
181
181
|
source_type: "File System",
|
|
182
182
|
directories_searched: rand(5..20),
|
|
183
|
-
files_scanned: rand(50..500)
|
|
183
|
+
files_scanned: rand(50..500),
|
|
184
184
|
},
|
|
185
|
-
timing: rand(0.01..0.2).round(3)
|
|
185
|
+
timing: rand(0.01..0.2).round(3),
|
|
186
186
|
}
|
|
187
187
|
end
|
|
188
188
|
end
|
|
@@ -194,8 +194,8 @@ module ScatterGather
|
|
|
194
194
|
def initialize(worker_count = 4)
|
|
195
195
|
@supervisor = Fractor::Supervisor.new(
|
|
196
196
|
worker_pools: [
|
|
197
|
-
{ worker_class: SearchWorker, num_workers: worker_count }
|
|
198
|
-
]
|
|
197
|
+
{ worker_class: SearchWorker, num_workers: worker_count },
|
|
198
|
+
],
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
@merged_results = nil
|
|
@@ -204,10 +204,11 @@ module ScatterGather
|
|
|
204
204
|
def search(query, sources = nil)
|
|
205
205
|
# Define search sources with their parameters
|
|
206
206
|
sources ||= [
|
|
207
|
-
{ source: :database,
|
|
207
|
+
{ source: :database,
|
|
208
|
+
params: { max_results: 50, include_archived: false } },
|
|
208
209
|
{ source: :api, params: { format: "json", timeout: 5 } },
|
|
209
210
|
{ source: :cache, params: { max_age: 3600 } },
|
|
210
|
-
{ source: :filesystem, params: { extensions: %w[txt md pdf] } }
|
|
211
|
+
{ source: :filesystem, params: { extensions: %w[txt md pdf] } },
|
|
211
212
|
]
|
|
212
213
|
|
|
213
214
|
start_time = Time.now
|
|
@@ -262,7 +263,7 @@ module ScatterGather
|
|
|
262
263
|
content: hit[:content],
|
|
263
264
|
source: source,
|
|
264
265
|
original_relevance: hit[:relevance],
|
|
265
|
-
weighted_relevance: hit[:relevance] * source_weight
|
|
266
|
+
weighted_relevance: hit[:relevance] * source_weight,
|
|
266
267
|
}
|
|
267
268
|
end
|
|
268
269
|
end
|
|
@@ -277,7 +278,7 @@ module ScatterGather
|
|
|
277
278
|
execution_time: total_time,
|
|
278
279
|
sources: results_by_source.keys,
|
|
279
280
|
ranked_results: ranked_hits,
|
|
280
|
-
source_details: results_by_source
|
|
281
|
+
source_details: results_by_source,
|
|
281
282
|
}
|
|
282
283
|
end
|
|
283
284
|
end
|
|
@@ -309,7 +310,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
309
310
|
puts "Query: #{results[:query]}"
|
|
310
311
|
puts "Total hits: #{results[:total_hits]}"
|
|
311
312
|
puts "Total execution time: #{results[:execution_time].round(3)} seconds"
|
|
312
|
-
puts "Sources searched: #{results[:sources].join(
|
|
313
|
+
puts "Sources searched: #{results[:sources].join(', ')}"
|
|
313
314
|
puts
|
|
314
315
|
|
|
315
316
|
puts "Top 5 Results (by relevance):"
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
= Simple Example - Getting Started with Fractor
|
|
2
|
+
|
|
3
|
+
== Purpose
|
|
4
|
+
|
|
5
|
+
Provides the most basic introduction to Fractor, demonstrating fundamental concepts with minimal complexity to help new users get started quickly.
|
|
6
|
+
|
|
7
|
+
== Focus
|
|
8
|
+
|
|
9
|
+
This example focuses on the essential building blocks:
|
|
10
|
+
|
|
11
|
+
* Creating custom `Work` classes to encapsulate work items
|
|
12
|
+
* Creating custom `Worker` classes to process work
|
|
13
|
+
* Setting up a `Supervisor` to manage parallel processing
|
|
14
|
+
* Adding work items and running the supervisor
|
|
15
|
+
* Processing results and handling errors
|
|
16
|
+
* Auto-detection of available processors
|
|
17
|
+
|
|
18
|
+
== Architecture
|
|
19
|
+
|
|
20
|
+
.Basic Fractor Architecture
|
|
21
|
+
[source]
|
|
22
|
+
----
|
|
23
|
+
Main Program
|
|
24
|
+
│
|
|
25
|
+
├─→ Create Work Items (MyWork)
|
|
26
|
+
│ │
|
|
27
|
+
│ └─→ MyWork { value: 1 }
|
|
28
|
+
│ MyWork { value: 2 }
|
|
29
|
+
│ ...
|
|
30
|
+
│ MyWork { value: 10 }
|
|
31
|
+
│
|
|
32
|
+
├─→ Create Supervisor
|
|
33
|
+
│ │
|
|
34
|
+
│ └─→ Worker Pool Configuration
|
|
35
|
+
│ │
|
|
36
|
+
│ └─→ MyWorker (auto-detect CPU count)
|
|
37
|
+
│
|
|
38
|
+
├─→ Add Work Items
|
|
39
|
+
│ │
|
|
40
|
+
│ └─→ supervisor.add_work_items(work_items)
|
|
41
|
+
│
|
|
42
|
+
├─→ Run Supervisor
|
|
43
|
+
│ │
|
|
44
|
+
│ └─→ Parallel Processing
|
|
45
|
+
│ │
|
|
46
|
+
│ ├─→ Ractor 1 (MyWorker) ─→ Process work
|
|
47
|
+
│ ├─→ Ractor 2 (MyWorker) ─→ Process work
|
|
48
|
+
│ ├─→ Ractor 3 (MyWorker) ─→ Process work
|
|
49
|
+
│ └─→ Ractor N (MyWorker) ─→ Process work
|
|
50
|
+
│ │
|
|
51
|
+
│ └─→ Results Aggregator
|
|
52
|
+
│ │
|
|
53
|
+
│ ├─→ Successful Results
|
|
54
|
+
│ └─→ Error Results
|
|
55
|
+
│
|
|
56
|
+
└─→ Display Results
|
|
57
|
+
│
|
|
58
|
+
├─→ supervisor.results (all results)
|
|
59
|
+
└─→ supervisor.results.errors (failed items)
|
|
60
|
+
----
|
|
61
|
+
|
|
62
|
+
.Data Flow
|
|
63
|
+
[source]
|
|
64
|
+
----
|
|
65
|
+
Work Items Worker Processing Results
|
|
66
|
+
──────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
MyWork(1) ─┐
|
|
69
|
+
MyWork(2) ─┤
|
|
70
|
+
MyWork(3) ─┤
|
|
71
|
+
MyWork(4) ─┼─→ Work Queue ─→ Ractor Pool ─→ WorkResult(2)
|
|
72
|
+
MyWork(5) ─┤ (Supervisor) (Workers) WorkResult(4)
|
|
73
|
+
MyWork(6) ─┤ WorkResult(6)
|
|
74
|
+
MyWork(7) ─┤ WorkResult(8)
|
|
75
|
+
MyWork(8) ─┤ ...
|
|
76
|
+
MyWork(9) ─┤ WorkError(5)
|
|
77
|
+
MyWork(10) ─┘ WorkResult(20)
|
|
78
|
+
----
|
|
79
|
+
|
|
80
|
+
== Key Components
|
|
81
|
+
|
|
82
|
+
=== Work Class
|
|
83
|
+
|
|
84
|
+
The `Work` class encapsulates input data for processing:
|
|
85
|
+
|
|
86
|
+
[source,ruby]
|
|
87
|
+
----
|
|
88
|
+
class MyWork < Fractor::Work
|
|
89
|
+
def initialize(value)
|
|
90
|
+
super({ value: value }) # <1>
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def value
|
|
94
|
+
input[:value] # <2>
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_s
|
|
98
|
+
"MyWork: #{value}" # <3>
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
----
|
|
102
|
+
<1> Store data in input hash using `super`
|
|
103
|
+
<2> Access stored data via `input` hash
|
|
104
|
+
<3> Provide string representation for debugging
|
|
105
|
+
|
|
106
|
+
Key points:
|
|
107
|
+
|
|
108
|
+
* Inherit from `Fractor::Work`
|
|
109
|
+
* Store all data in the `input` hash
|
|
110
|
+
* Provide accessor methods for convenience
|
|
111
|
+
* Include `to_s` method for debugging
|
|
112
|
+
|
|
113
|
+
=== Worker Class
|
|
114
|
+
|
|
115
|
+
The `Worker` class processes work items:
|
|
116
|
+
|
|
117
|
+
[source,ruby]
|
|
118
|
+
----
|
|
119
|
+
class MyWorker < Fractor::Worker
|
|
120
|
+
def process(work) # <1>
|
|
121
|
+
# Check work type
|
|
122
|
+
if work.is_a?(MyWork)
|
|
123
|
+
# Handle known error case
|
|
124
|
+
if work.value == 5
|
|
125
|
+
error = StandardError.new('Cannot process value 5')
|
|
126
|
+
return Fractor::WorkResult.new(error: error, work: work) # <2>
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Process successfully
|
|
130
|
+
calculated = work.value * 2
|
|
131
|
+
Fractor::WorkResult.new(result: calculated, work: work) # <3>
|
|
132
|
+
|
|
133
|
+
else
|
|
134
|
+
# Handle unexpected work type
|
|
135
|
+
error = TypeError.new("Unsupported work type: #{work.class}")
|
|
136
|
+
Fractor::WorkResult.new(error: error, work: work)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
----
|
|
141
|
+
<1> Implement `process(work)` method - called by Ractor
|
|
142
|
+
<2> Return `WorkResult` with error for failures
|
|
143
|
+
<3> Return `WorkResult` with result for success
|
|
144
|
+
|
|
145
|
+
Key points:
|
|
146
|
+
|
|
147
|
+
* Inherit from `Fractor::Worker`
|
|
148
|
+
* Implement `process(work)` method
|
|
149
|
+
* Return `Fractor::WorkResult` objects
|
|
150
|
+
* Handle errors gracefully (return error results, don't raise)
|
|
151
|
+
* Support multiple work types if needed
|
|
152
|
+
|
|
153
|
+
=== Supervisor Setup
|
|
154
|
+
|
|
155
|
+
The `Supervisor` manages worker Ractors and distributes work:
|
|
156
|
+
|
|
157
|
+
[source,ruby]
|
|
158
|
+
----
|
|
159
|
+
# Create supervisor with worker pool
|
|
160
|
+
supervisor = Fractor::Supervisor.new(
|
|
161
|
+
worker_pools: [
|
|
162
|
+
{ worker_class: MyWorker } # <1>
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Create and add work items
|
|
167
|
+
work_items = (1..10).map { |i| MyWork.new(i) } # <2>
|
|
168
|
+
supervisor.add_work_items(work_items) # <3>
|
|
169
|
+
|
|
170
|
+
# Run the supervisor
|
|
171
|
+
supervisor.run # <4>
|
|
172
|
+
|
|
173
|
+
# Access results
|
|
174
|
+
supervisor.results # <5>
|
|
175
|
+
supervisor.results.errors # <6>
|
|
176
|
+
----
|
|
177
|
+
<1> Define worker pool (auto-detects CPU count)
|
|
178
|
+
<2> Create array of work items
|
|
179
|
+
<3> Add work items to supervisor
|
|
180
|
+
<4> Start processing (blocks until complete)
|
|
181
|
+
<5> Access all results (successful and failed)
|
|
182
|
+
<6> Access only failed results
|
|
183
|
+
|
|
184
|
+
Key points:
|
|
185
|
+
|
|
186
|
+
* Worker pools default to auto-detected CPU count
|
|
187
|
+
* Can specify `num_workers` explicitly if needed
|
|
188
|
+
* `run` blocks until all work is complete
|
|
189
|
+
* Results are aggregated in `supervisor.results`
|
|
190
|
+
|
|
191
|
+
== Usage
|
|
192
|
+
|
|
193
|
+
Run the example from the project root:
|
|
194
|
+
|
|
195
|
+
[source,shell]
|
|
196
|
+
----
|
|
197
|
+
ruby examples/simple/sample.rb
|
|
198
|
+
|
|
199
|
+
# With debug output
|
|
200
|
+
FRACTOR_DEBUG=1 ruby examples/simple/sample.rb
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
== Expected Output
|
|
204
|
+
|
|
205
|
+
[example]
|
|
206
|
+
====
|
|
207
|
+
[source]
|
|
208
|
+
----
|
|
209
|
+
Processing complete.
|
|
210
|
+
Final Aggregated Results:
|
|
211
|
+
#<Fractor::ResultAggregator:0x... @results=[
|
|
212
|
+
#<Fractor::WorkResult @result=2, @work=#<MyWork: 1>>,
|
|
213
|
+
#<Fractor::WorkResult @result=4, @work=#<MyWork: 2>>,
|
|
214
|
+
#<Fractor::WorkResult @result=6, @work=#<MyWork: 3>>,
|
|
215
|
+
#<Fractor::WorkResult @result=8, @work=#<MyWork: 4>>,
|
|
216
|
+
#<Fractor::WorkResult @result=12, @work=#<MyWork: 6>>,
|
|
217
|
+
#<Fractor::WorkResult @result=14, @work=#<MyWork: 7>>,
|
|
218
|
+
#<Fractor::WorkResult @result=16, @work=#<MyWork: 8>>,
|
|
219
|
+
#<Fractor::WorkResult @result=18, @work=#<MyWork: 9>>,
|
|
220
|
+
#<Fractor::WorkResult @result=20, @work=#<MyWork: 10>>
|
|
221
|
+
], @errors=[...]>
|
|
222
|
+
|
|
223
|
+
Failed Work Items (1):
|
|
224
|
+
Work: MyWork: 5
|
|
225
|
+
Error: StandardError: Cannot process value 5
|
|
226
|
+
----
|
|
227
|
+
====
|
|
228
|
+
|
|
229
|
+
== Learning Points
|
|
230
|
+
|
|
231
|
+
=== Parallel Processing
|
|
232
|
+
|
|
233
|
+
* Fractor automatically distributes work across multiple Ractors
|
|
234
|
+
* Number of Ractors defaults to available CPU cores
|
|
235
|
+
* Work is processed in parallel, improving throughput
|
|
236
|
+
* Order of completion is non-deterministic
|
|
237
|
+
|
|
238
|
+
=== Work Encapsulation
|
|
239
|
+
|
|
240
|
+
* Each work item is a separate object with its input data
|
|
241
|
+
* Work items are isolated from each other
|
|
242
|
+
* Workers process one work item at a time
|
|
243
|
+
* Work items can be of different types (polymorphic)
|
|
244
|
+
|
|
245
|
+
=== Error Handling
|
|
246
|
+
|
|
247
|
+
* Errors don't stop the entire processing
|
|
248
|
+
* Failed work items are tracked separately
|
|
249
|
+
* Workers return error results, not exceptions
|
|
250
|
+
* System continues processing remaining work
|
|
251
|
+
|
|
252
|
+
=== Auto-Detection
|
|
253
|
+
|
|
254
|
+
* When `num_workers` is not specified, Fractor auto-detects CPU count
|
|
255
|
+
* Uses `Etc.nprocessors` to determine available cores
|
|
256
|
+
* Optimal for CPU-bound tasks
|
|
257
|
+
* Can be overridden if needed:
|
|
258
|
+
+
|
|
259
|
+
[source,ruby]
|
|
260
|
+
----
|
|
261
|
+
supervisor = Fractor::Supervisor.new(
|
|
262
|
+
worker_pools: [
|
|
263
|
+
{ worker_class: MyWorker, num_workers: 4 }
|
|
264
|
+
]
|
|
265
|
+
)
|
|
266
|
+
----
|
|
267
|
+
|
|
268
|
+
=== Result Aggregation
|
|
269
|
+
|
|
270
|
+
* All results are collected in `supervisor.results`
|
|
271
|
+
* Successful results accessible via `results.results`
|
|
272
|
+
* Failed results accessible via `results.errors`
|
|
273
|
+
* Each result contains both the output and original work item
|
|
274
|
+
|
|
275
|
+
== Common Patterns
|
|
276
|
+
|
|
277
|
+
=== Multiple Work Types
|
|
278
|
+
|
|
279
|
+
Process different types of work with the same worker:
|
|
280
|
+
|
|
281
|
+
[source,ruby]
|
|
282
|
+
----
|
|
283
|
+
class MyWorker < Fractor::Worker
|
|
284
|
+
def process(work)
|
|
285
|
+
if work.is_a?(MyWork)
|
|
286
|
+
# Process MyWork
|
|
287
|
+
Fractor::WorkResult.new(result: work.value * 2, work: work)
|
|
288
|
+
elsif work.is_a?(OtherWork)
|
|
289
|
+
# Process OtherWork differently
|
|
290
|
+
Fractor::WorkResult.new(result: "Processed: #{work.value}", work: work)
|
|
291
|
+
else
|
|
292
|
+
# Handle unknown types
|
|
293
|
+
error = TypeError.new("Unsupported work type: #{work.class}")
|
|
294
|
+
Fractor::WorkResult.new(error: error, work: work)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
----
|
|
299
|
+
|
|
300
|
+
=== Conditional Processing
|
|
301
|
+
|
|
302
|
+
Make decisions based on work item data:
|
|
303
|
+
|
|
304
|
+
[source,ruby]
|
|
305
|
+
----
|
|
306
|
+
def process(work)
|
|
307
|
+
if work.value < 0
|
|
308
|
+
error = ArgumentError.new('Value must be positive')
|
|
309
|
+
return Fractor::WorkResult.new(error: error, work: work)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if work.value > 100
|
|
313
|
+
# Heavy processing for large values
|
|
314
|
+
result = complex_calculation(work.value)
|
|
315
|
+
else
|
|
316
|
+
# Simple processing for small values
|
|
317
|
+
result = work.value * 2
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
321
|
+
end
|
|
322
|
+
----
|
|
323
|
+
|
|
324
|
+
=== Debugging
|
|
325
|
+
|
|
326
|
+
Use `ENV['FRACTOR_DEBUG']` to enable debug output:
|
|
327
|
+
|
|
328
|
+
[source,ruby]
|
|
329
|
+
----
|
|
330
|
+
def process(work)
|
|
331
|
+
puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
|
|
332
|
+
|
|
333
|
+
# Processing logic...
|
|
334
|
+
|
|
335
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
336
|
+
end
|
|
337
|
+
----
|
|
338
|
+
|
|
339
|
+
== Next Steps
|
|
340
|
+
|
|
341
|
+
After understanding the basics, explore more advanced examples:
|
|
342
|
+
|
|
343
|
+
* link:../auto_detection/README.adoc[Auto Detection] - CPU auto-detection in detail
|
|
344
|
+
* link:../multi_work_type/README.adoc[Multi Work Type] - Handling multiple work types
|
|
345
|
+
* link:../specialized_workers/README.adoc[Specialized Workers] - Worker pools for different tasks
|
|
346
|
+
* link:../pipeline_processing/README.adoc[Pipeline Processing] - Sequential processing stages
|
|
347
|
+
* link:../workflow/README.adoc[Workflow System] - GitHub Actions-style workflows
|
data/examples/simple/sample.rb
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
#
|
|
33
33
|
# =============================================================================
|
|
34
34
|
|
|
35
|
-
require_relative
|
|
35
|
+
require_relative '../../lib/fractor'
|
|
36
36
|
|
|
37
37
|
# Client-specific work item implementation inheriting from Fractor::Work
|
|
38
38
|
class MyWork < Fractor::Work
|
|
@@ -72,14 +72,14 @@ class MyWorker < Fractor::Worker
|
|
|
72
72
|
# It should return a Fractor::WorkResult object
|
|
73
73
|
def process(work)
|
|
74
74
|
# Only print debug information if FRACTOR_DEBUG is enabled
|
|
75
|
-
puts "Working on '#{work.inspect}'" if ENV[
|
|
75
|
+
puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
|
|
76
76
|
|
|
77
77
|
# Check work type and handle accordingly
|
|
78
78
|
if work.is_a?(MyWork)
|
|
79
79
|
if work.value == 5
|
|
80
80
|
# Return a Fractor::WorkResult for errors
|
|
81
81
|
# Store the error object, not just the string
|
|
82
|
-
error = StandardError.new(
|
|
82
|
+
error = StandardError.new('Cannot process value 5')
|
|
83
83
|
return Fractor::WorkResult.new(error: error, work: work)
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -116,8 +116,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
116
116
|
# Run the supervisor to start processing work
|
|
117
117
|
supervisor.run
|
|
118
118
|
|
|
119
|
-
puts
|
|
120
|
-
puts
|
|
119
|
+
puts 'Processing complete.'
|
|
120
|
+
puts 'Final Aggregated Results:'
|
|
121
121
|
# Access the results aggregator from the supervisor
|
|
122
122
|
puts supervisor.results.inspect
|
|
123
123
|
|