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