fractor 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
= Fan-Out Workflow
|
|
2
|
+
|
|
3
|
+
== Purpose
|
|
4
|
+
|
|
5
|
+
Demonstrates parallel processing patterns with fan-out (one-to-many) and fan-in (many-to-one) job execution using Fractor's workflow system.
|
|
6
|
+
|
|
7
|
+
== Focus
|
|
8
|
+
|
|
9
|
+
This example focuses on demonstrating:
|
|
10
|
+
|
|
11
|
+
* Fan-out pattern: one job feeding multiple parallel jobs
|
|
12
|
+
* Fan-in pattern: multiple jobs aggregating into one job
|
|
13
|
+
* Parallel job execution with shared dependencies
|
|
14
|
+
* Multiple input aggregation using `inputs_from_multiple`
|
|
15
|
+
* Input mapping syntax for aggregating multiple job outputs
|
|
16
|
+
* Workflow execution order with parallelizable jobs
|
|
17
|
+
|
|
18
|
+
== Architecture
|
|
19
|
+
|
|
20
|
+
.Fan-Out and Fan-In Pattern
|
|
21
|
+
[source]
|
|
22
|
+
----
|
|
23
|
+
[Workflow Input]
|
|
24
|
+
│
|
|
25
|
+
│ TextInput { text: "Hello Fractor!" }
|
|
26
|
+
▼
|
|
27
|
+
┌─────────────────┐
|
|
28
|
+
│ Split Job │ ◄─── Entry Point (start_with)
|
|
29
|
+
│ TextSplitter │
|
|
30
|
+
└─────────────────┘
|
|
31
|
+
│
|
|
32
|
+
│ TextInput { text: "Hello Fractor!" }
|
|
33
|
+
├──────────────┬──────────────┐
|
|
34
|
+
│ │ │
|
|
35
|
+
▼ ▼ ▼
|
|
36
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
37
|
+
│Uppercase │ │Lowercase │ │ Reverse │ ◄─── Fan-Out
|
|
38
|
+
│ Worker │ │ Worker │ │ Worker │ (parallel)
|
|
39
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
40
|
+
│ │ │
|
|
41
|
+
│ │ │
|
|
42
|
+
▼ ▼ ▼
|
|
43
|
+
ProcessedText ProcessedText ProcessedText
|
|
44
|
+
{ result: { result: { result:
|
|
45
|
+
"HELLO..." } "hello..." } "!rotcarF..." }
|
|
46
|
+
│ │ │
|
|
47
|
+
└──────────────┴──────────────┘
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
┌─────────────────┐
|
|
51
|
+
│ Combine Job │ ◄─── Fan-In
|
|
52
|
+
│ ResultCombiner │ (aggregation)
|
|
53
|
+
└─────────────────┘
|
|
54
|
+
│
|
|
55
|
+
│ CombinedResult {
|
|
56
|
+
│ uppercase: "HELLO FRACTOR!",
|
|
57
|
+
│ lowercase: "hello fractor!",
|
|
58
|
+
│ reversed: "!rotcarF olleH"
|
|
59
|
+
│ }
|
|
60
|
+
▼
|
|
61
|
+
[Workflow Output] ◄─── Exit Point (end_with)
|
|
62
|
+
----
|
|
63
|
+
|
|
64
|
+
.Workflow Execution Timeline
|
|
65
|
+
[source]
|
|
66
|
+
----
|
|
67
|
+
Time Job Execution
|
|
68
|
+
────────────────────────────────────────────────────────────
|
|
69
|
+
0 [split] ───────────► Complete
|
|
70
|
+
│
|
|
71
|
+
1 ├─► [uppercase] ──► Complete
|
|
72
|
+
│
|
|
73
|
+
2 ├─► [lowercase] ──► Complete
|
|
74
|
+
│
|
|
75
|
+
3 ├─► [reverse] ────► Complete
|
|
76
|
+
│
|
|
77
|
+
4 └─► [combine] ─────► Complete
|
|
78
|
+
(waits for uppercase, lowercase, reverse)
|
|
79
|
+
|
|
80
|
+
Note: Jobs with same dependencies (uppercase, lowercase, reverse)
|
|
81
|
+
execute sequentially to avoid Ractor coordination complexity,
|
|
82
|
+
but are architecturally designed to be parallelizable.
|
|
83
|
+
----
|
|
84
|
+
|
|
85
|
+
== Key Components
|
|
86
|
+
|
|
87
|
+
=== Data Models
|
|
88
|
+
|
|
89
|
+
The workflow uses three data models:
|
|
90
|
+
|
|
91
|
+
[source,ruby]
|
|
92
|
+
----
|
|
93
|
+
class TextInput
|
|
94
|
+
attr_accessor :text
|
|
95
|
+
|
|
96
|
+
def initialize(text: "")
|
|
97
|
+
@text = text
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ProcessedText
|
|
102
|
+
attr_accessor :result
|
|
103
|
+
|
|
104
|
+
def initialize(result: "")
|
|
105
|
+
@result = result
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class CombinedResult
|
|
110
|
+
attr_accessor :uppercase, :lowercase, :reversed
|
|
111
|
+
|
|
112
|
+
def initialize(uppercase: "", lowercase: "", reversed: "")
|
|
113
|
+
@uppercase = uppercase
|
|
114
|
+
@lowercase = lowercase
|
|
115
|
+
@reversed = reversed
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
----
|
|
119
|
+
|
|
120
|
+
=== Workers
|
|
121
|
+
|
|
122
|
+
Fan-out workers process the same input independently:
|
|
123
|
+
|
|
124
|
+
[source,ruby]
|
|
125
|
+
----
|
|
126
|
+
class UppercaseWorker < Fractor::Worker
|
|
127
|
+
input_type TextInput
|
|
128
|
+
output_type ProcessedText
|
|
129
|
+
|
|
130
|
+
def process(work)
|
|
131
|
+
input = work.input
|
|
132
|
+
result = input.text.upcase
|
|
133
|
+
|
|
134
|
+
output = ProcessedText.new(result: result)
|
|
135
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
----
|
|
139
|
+
|
|
140
|
+
Fan-in worker aggregates multiple inputs:
|
|
141
|
+
|
|
142
|
+
[source,ruby]
|
|
143
|
+
----
|
|
144
|
+
class ResultCombiner < Fractor::Worker
|
|
145
|
+
input_type CombinedResult # <1>
|
|
146
|
+
output_type CombinedResult
|
|
147
|
+
|
|
148
|
+
def process(work)
|
|
149
|
+
input = work.input
|
|
150
|
+
# Input already contains all aggregated results
|
|
151
|
+
puts "Uppercase: #{input.uppercase}"
|
|
152
|
+
puts "Lowercase: #{input.lowercase}"
|
|
153
|
+
puts "Reversed: #{input.reversed}"
|
|
154
|
+
|
|
155
|
+
Fractor::WorkResult.new(result: input, work: work)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
----
|
|
159
|
+
<1> Input type contains all aggregated fields
|
|
160
|
+
|
|
161
|
+
=== Workflow Definition
|
|
162
|
+
|
|
163
|
+
The workflow defines fan-out and fan-in patterns:
|
|
164
|
+
|
|
165
|
+
[source,ruby]
|
|
166
|
+
----
|
|
167
|
+
class FanOutWorkflow < Fractor::Workflow
|
|
168
|
+
workflow "fan_out_example" do
|
|
169
|
+
input_type TextInput
|
|
170
|
+
output_type CombinedResult
|
|
171
|
+
|
|
172
|
+
start_with "split" # <1>
|
|
173
|
+
end_with "combine" # <2>
|
|
174
|
+
|
|
175
|
+
# Entry point
|
|
176
|
+
job "split" do
|
|
177
|
+
runs_with TextSplitter
|
|
178
|
+
inputs_from_workflow
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Fan-out: three jobs with same dependency
|
|
182
|
+
job "uppercase" do
|
|
183
|
+
runs_with UppercaseWorker
|
|
184
|
+
needs "split" # <3>
|
|
185
|
+
inputs_from_job "split"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
job "lowercase" do
|
|
189
|
+
runs_with LowercaseWorker
|
|
190
|
+
needs "split" # <3>
|
|
191
|
+
inputs_from_job "split"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
job "reverse" do
|
|
195
|
+
runs_with ReverseWorker
|
|
196
|
+
needs "split" # <3>
|
|
197
|
+
inputs_from_job "split"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Fan-in: aggregate multiple inputs
|
|
201
|
+
job "combine" do
|
|
202
|
+
runs_with ResultCombiner
|
|
203
|
+
needs "uppercase", "lowercase", "reverse" # <4>
|
|
204
|
+
inputs_from_multiple( # <5>
|
|
205
|
+
"uppercase" => { uppercase: :result }, # <6>
|
|
206
|
+
"lowercase" => { lowercase: :result },
|
|
207
|
+
"reverse" => { reversed: :result }
|
|
208
|
+
)
|
|
209
|
+
outputs_to_workflow
|
|
210
|
+
terminates_workflow
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
----
|
|
215
|
+
<1> Workflow starts with the split job
|
|
216
|
+
<2> Workflow ends with the combine job
|
|
217
|
+
<3> All three jobs depend on split (enables parallel execution)
|
|
218
|
+
<4> Combine job depends on all three processing jobs
|
|
219
|
+
<5> Aggregate inputs from multiple jobs
|
|
220
|
+
<6> Map source attribute to target attribute
|
|
221
|
+
|
|
222
|
+
== Key Features
|
|
223
|
+
|
|
224
|
+
=== Fan-Out Pattern
|
|
225
|
+
|
|
226
|
+
Multiple jobs share the same dependency and receive the same input:
|
|
227
|
+
|
|
228
|
+
[source,ruby]
|
|
229
|
+
----
|
|
230
|
+
job "split" do
|
|
231
|
+
runs_with TextSplitter
|
|
232
|
+
inputs_from_workflow
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# These three jobs all depend on "split"
|
|
236
|
+
# They can potentially execute in parallel
|
|
237
|
+
job "uppercase" do
|
|
238
|
+
needs "split"
|
|
239
|
+
inputs_from_job "split" # Same input
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
job "lowercase" do
|
|
243
|
+
needs "split"
|
|
244
|
+
inputs_from_job "split" # Same input
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
job "reverse" do
|
|
248
|
+
needs "split"
|
|
249
|
+
inputs_from_job "split" # Same input
|
|
250
|
+
end
|
|
251
|
+
----
|
|
252
|
+
|
|
253
|
+
=== Fan-In Pattern with inputs_from_multiple
|
|
254
|
+
|
|
255
|
+
The `inputs_from_multiple` method aggregates outputs from multiple jobs:
|
|
256
|
+
|
|
257
|
+
[source,ruby]
|
|
258
|
+
----
|
|
259
|
+
job "combine" do
|
|
260
|
+
needs "uppercase", "lowercase", "reverse"
|
|
261
|
+
inputs_from_multiple(
|
|
262
|
+
"uppercase" => { uppercase: :result }, # <1>
|
|
263
|
+
"lowercase" => { lowercase: :result }, # <2>
|
|
264
|
+
"reverse" => { reversed: :result } # <3>
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
----
|
|
268
|
+
<1> Maps `uppercase` job's `result` attribute to `uppercase` attribute
|
|
269
|
+
<2> Maps `lowercase` job's `result` attribute to `lowercase` attribute
|
|
270
|
+
<3> Maps `reverse` job's `result` attribute to `reversed` attribute
|
|
271
|
+
|
|
272
|
+
The mapping syntax is:
|
|
273
|
+
|
|
274
|
+
[source]
|
|
275
|
+
----
|
|
276
|
+
"source_job_name" => { target_attribute: :source_attribute }
|
|
277
|
+
----
|
|
278
|
+
|
|
279
|
+
This creates a `CombinedResult` object with:
|
|
280
|
+
|
|
281
|
+
[source,ruby]
|
|
282
|
+
----
|
|
283
|
+
CombinedResult.new(
|
|
284
|
+
uppercase: uppercase_job_output.result,
|
|
285
|
+
lowercase: lowercase_job_output.result,
|
|
286
|
+
reversed: reverse_job_output.result
|
|
287
|
+
)
|
|
288
|
+
----
|
|
289
|
+
|
|
290
|
+
== Usage
|
|
291
|
+
|
|
292
|
+
Run the example from the project root:
|
|
293
|
+
|
|
294
|
+
[source,shell]
|
|
295
|
+
----
|
|
296
|
+
ruby examples/workflow/fan_out/fan_out_workflow.rb
|
|
297
|
+
----
|
|
298
|
+
|
|
299
|
+
== Expected Output
|
|
300
|
+
|
|
301
|
+
[example]
|
|
302
|
+
====
|
|
303
|
+
[source]
|
|
304
|
+
----
|
|
305
|
+
============================================================
|
|
306
|
+
Fan-Out Workflow Example
|
|
307
|
+
============================================================
|
|
308
|
+
|
|
309
|
+
Input: Hello Fractor!
|
|
310
|
+
|
|
311
|
+
[TextSplitter] Processing: Hello Fractor!
|
|
312
|
+
[UppercaseWorker] Result: HELLO FRACTOR!
|
|
313
|
+
[LowercaseWorker] Result: hello fractor!
|
|
314
|
+
[ReverseWorker] Result: !rotcarF olleH
|
|
315
|
+
[ResultCombiner] Combining results:
|
|
316
|
+
Uppercase: HELLO FRACTOR!
|
|
317
|
+
Lowercase: hello fractor!
|
|
318
|
+
Reversed: !rotcarF olleH
|
|
319
|
+
|
|
320
|
+
============================================================
|
|
321
|
+
Workflow Results:
|
|
322
|
+
------------------------------------------------------------
|
|
323
|
+
Status: SUCCESS
|
|
324
|
+
Execution Time: 0.002s
|
|
325
|
+
Completed Jobs: split, uppercase, lowercase, reverse, combine
|
|
326
|
+
|
|
327
|
+
Final Output:
|
|
328
|
+
Uppercase: HELLO FRACTOR!
|
|
329
|
+
Lowercase: hello fractor!
|
|
330
|
+
Reversed: !rotcarF olleH
|
|
331
|
+
============================================================
|
|
332
|
+
----
|
|
333
|
+
====
|
|
334
|
+
|
|
335
|
+
== Learning Points
|
|
336
|
+
|
|
337
|
+
=== Parallel Execution
|
|
338
|
+
|
|
339
|
+
* Jobs with the same dependencies can execute in parallel
|
|
340
|
+
* Current implementation executes sequentially to avoid Ractor coordination complexity
|
|
341
|
+
* Architecture supports parallel execution for future optimization
|
|
342
|
+
|
|
343
|
+
=== Fan-Out Design
|
|
344
|
+
|
|
345
|
+
* One job produces output consumed by multiple downstream jobs
|
|
346
|
+
* All downstream jobs receive the same input
|
|
347
|
+
* Enables independent parallel processing of the same data
|
|
348
|
+
|
|
349
|
+
=== Fan-In Aggregation
|
|
350
|
+
|
|
351
|
+
* `inputs_from_multiple` collects outputs from multiple jobs
|
|
352
|
+
* Mapping syntax: `"job" => { target_attr: :source_attr }`
|
|
353
|
+
* Creates a single aggregated input object for the consuming job
|
|
354
|
+
|
|
355
|
+
=== Dependency Management
|
|
356
|
+
|
|
357
|
+
* Workflow automatically determines execution order
|
|
358
|
+
* Jobs wait for all dependencies before executing
|
|
359
|
+
* Topological sort ensures correct execution sequence
|
|
360
|
+
|
|
361
|
+
=== Input Mapping
|
|
362
|
+
|
|
363
|
+
* `inputs_from_workflow`: Direct workflow input
|
|
364
|
+
* `inputs_from_job "name"`: Single job's output
|
|
365
|
+
* `inputs_from_multiple(...)`: Multiple jobs' outputs aggregated
|
|
366
|
+
|
|
367
|
+
== Performance Considerations
|
|
368
|
+
|
|
369
|
+
* Current implementation executes fan-out jobs sequentially
|
|
370
|
+
* This avoids Ractor threading complexity and coordination overhead
|
|
371
|
+
* For CPU-intensive tasks, sequential execution may be preferred
|
|
372
|
+
* For I/O-bound tasks, parallel execution would provide better performance
|
|
373
|
+
* Future optimization: implement true parallel execution for independent jobs
|
|
374
|
+
|
|
375
|
+
== Next Steps
|
|
376
|
+
|
|
377
|
+
After understanding fan-out patterns, explore:
|
|
378
|
+
|
|
379
|
+
* link:../simple_linear/README.adoc[Simple Linear Workflow] - Sequential processing basics
|
|
380
|
+
* link:../conditional/README.adoc[Conditional Workflow] - Runtime conditional execution
|
|
381
|
+
* link:../README.adoc[Workflow Overview] - Complete workflow system documentation
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../../lib/fractor"
|
|
5
|
+
|
|
6
|
+
# Input and output data types
|
|
7
|
+
class TextInput
|
|
8
|
+
attr_accessor :text
|
|
9
|
+
|
|
10
|
+
def initialize(text: "")
|
|
11
|
+
@text = text
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ProcessedText
|
|
16
|
+
attr_accessor :result
|
|
17
|
+
|
|
18
|
+
def initialize(result: "")
|
|
19
|
+
@result = result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class CombinedResult
|
|
24
|
+
attr_accessor :uppercase, :lowercase, :reversed
|
|
25
|
+
|
|
26
|
+
def initialize(uppercase: "", lowercase: "", reversed: "")
|
|
27
|
+
@uppercase = uppercase
|
|
28
|
+
@lowercase = lowercase
|
|
29
|
+
@reversed = reversed
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module FanOutExample
|
|
34
|
+
# Enable/disable debug output from workers
|
|
35
|
+
@debug_output = false
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
attr_accessor :debug_output
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Worker that splits text for parallel processing
|
|
42
|
+
class TextSplitter < Fractor::Worker
|
|
43
|
+
input_type TextInput
|
|
44
|
+
output_type TextInput
|
|
45
|
+
|
|
46
|
+
def process(work)
|
|
47
|
+
input = work.input
|
|
48
|
+
puts "[TextSplitter] Processing: #{input.text}" if FanOutExample.debug_output
|
|
49
|
+
|
|
50
|
+
output = TextInput.new(text: input.text)
|
|
51
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Worker that converts to uppercase
|
|
56
|
+
class UppercaseWorker < Fractor::Worker
|
|
57
|
+
input_type TextInput
|
|
58
|
+
output_type ProcessedText
|
|
59
|
+
|
|
60
|
+
def process(work)
|
|
61
|
+
input = work.input
|
|
62
|
+
result = input.text.upcase
|
|
63
|
+
puts "[UppercaseWorker] Result: #{result}" if FanOutExample.debug_output
|
|
64
|
+
|
|
65
|
+
output = ProcessedText.new(result: result)
|
|
66
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Worker that converts to lowercase
|
|
71
|
+
class LowercaseWorker < Fractor::Worker
|
|
72
|
+
input_type TextInput
|
|
73
|
+
output_type ProcessedText
|
|
74
|
+
|
|
75
|
+
def process(work)
|
|
76
|
+
input = work.input
|
|
77
|
+
result = input.text.downcase
|
|
78
|
+
puts "[LowercaseWorker] Result: #{result}" if FanOutExample.debug_output
|
|
79
|
+
|
|
80
|
+
output = ProcessedText.new(result: result)
|
|
81
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Worker that reverses the text
|
|
86
|
+
class ReverseWorker < Fractor::Worker
|
|
87
|
+
input_type TextInput
|
|
88
|
+
output_type ProcessedText
|
|
89
|
+
|
|
90
|
+
def process(work)
|
|
91
|
+
input = work.input
|
|
92
|
+
result = input.text.reverse
|
|
93
|
+
puts "[ReverseWorker] Result: #{result}" if FanOutExample.debug_output
|
|
94
|
+
|
|
95
|
+
output = ProcessedText.new(result: result)
|
|
96
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Worker that combines all results
|
|
101
|
+
class ResultCombiner < Fractor::Worker
|
|
102
|
+
input_type CombinedResult
|
|
103
|
+
output_type CombinedResult
|
|
104
|
+
|
|
105
|
+
def process(work)
|
|
106
|
+
input = work.input
|
|
107
|
+
if FanOutExample.debug_output
|
|
108
|
+
puts "[ResultCombiner] Combining results:"
|
|
109
|
+
puts " Uppercase: #{input.uppercase}"
|
|
110
|
+
puts " Lowercase: #{input.lowercase}"
|
|
111
|
+
puts " Reversed: #{input.reversed}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Fractor::WorkResult.new(result: input, work: work)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Define the fan-out workflow
|
|
120
|
+
class FanOutWorkflow < Fractor::Workflow
|
|
121
|
+
workflow "fan_out_example" do
|
|
122
|
+
input_type TextInput
|
|
123
|
+
output_type CombinedResult
|
|
124
|
+
|
|
125
|
+
# Define workflow start and end
|
|
126
|
+
start_with "split"
|
|
127
|
+
end_with "combine"
|
|
128
|
+
|
|
129
|
+
# Entry point: splits text
|
|
130
|
+
job "split" do
|
|
131
|
+
runs_with FanOutExample::TextSplitter
|
|
132
|
+
inputs_from_workflow
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Fan-out: three jobs processing the same input
|
|
136
|
+
# Note: These jobs could run in parallel but are executed sequentially
|
|
137
|
+
# to avoid Ractor threading complexity in this example
|
|
138
|
+
job "uppercase" do
|
|
139
|
+
runs_with FanOutExample::UppercaseWorker
|
|
140
|
+
needs "split"
|
|
141
|
+
inputs_from_job "split"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
job "lowercase" do
|
|
145
|
+
runs_with FanOutExample::LowercaseWorker
|
|
146
|
+
needs "split"
|
|
147
|
+
inputs_from_job "split"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
job "reverse" do
|
|
151
|
+
runs_with FanOutExample::ReverseWorker
|
|
152
|
+
needs "split"
|
|
153
|
+
inputs_from_job "split"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Fan-in: combine results from all jobs
|
|
157
|
+
job "combine" do
|
|
158
|
+
runs_with FanOutExample::ResultCombiner
|
|
159
|
+
needs "uppercase", "lowercase", "reverse"
|
|
160
|
+
inputs_from_multiple(
|
|
161
|
+
"uppercase" => { uppercase: :result },
|
|
162
|
+
"lowercase" => { lowercase: :result },
|
|
163
|
+
"reverse" => { reversed: :result }
|
|
164
|
+
)
|
|
165
|
+
outputs_to_workflow
|
|
166
|
+
terminates_workflow
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Only run the example when this file is executed directly
|
|
172
|
+
if __FILE__ == $PROGRAM_NAME
|
|
173
|
+
# Execute the workflow
|
|
174
|
+
puts "=" * 60
|
|
175
|
+
puts "Fan-Out Workflow Example"
|
|
176
|
+
puts "=" * 60
|
|
177
|
+
puts ""
|
|
178
|
+
|
|
179
|
+
# Enable debug output for demonstration
|
|
180
|
+
FanOutExample.debug_output = true
|
|
181
|
+
|
|
182
|
+
input = TextInput.new(text: "Hello Fractor!")
|
|
183
|
+
puts "Input: #{input.text}"
|
|
184
|
+
puts ""
|
|
185
|
+
|
|
186
|
+
workflow = FanOutWorkflow.new
|
|
187
|
+
result = workflow.execute(input: input)
|
|
188
|
+
|
|
189
|
+
puts ""
|
|
190
|
+
puts "=" * 60
|
|
191
|
+
puts "Workflow Results:"
|
|
192
|
+
puts "-" * 60
|
|
193
|
+
puts "Status: #{result.success? ? 'SUCCESS' : 'FAILED'}"
|
|
194
|
+
puts "Execution Time: #{result.execution_time.round(3)}s"
|
|
195
|
+
puts "Completed Jobs: #{result.completed_jobs.join(', ')}"
|
|
196
|
+
puts ""
|
|
197
|
+
puts "Final Output:"
|
|
198
|
+
puts " Uppercase: #{result.output.uppercase}"
|
|
199
|
+
puts " Lowercase: #{result.output.lowercase}"
|
|
200
|
+
puts " Reversed: #{result.output.reversed}"
|
|
201
|
+
puts "=" * 60
|
|
202
|
+
end
|