fractor 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,483 @@
1
+ = Conditional Workflow
2
+
3
+ == Purpose
4
+
5
+ Demonstrates runtime conditional job execution based on data validation results, showcasing how workflows can make dynamic decisions during execution.
6
+
7
+ == Focus
8
+
9
+ This example focuses on demonstrating:
10
+
11
+ * Conditional job execution using `if_condition`
12
+ * Multiple termination points with `terminates_workflow`
13
+ * Runtime decision making with context access
14
+ * Lambda expressions for condition evaluation
15
+ * Branching logic based on data validation
16
+ * Dynamic workflow paths based on input data
17
+
18
+ == Architecture
19
+
20
+ .Conditional Branching Decision Tree
21
+ [source]
22
+ ----
23
+ [Workflow Input]
24
+
25
+ │ NumberInput { value: X }
26
+
27
+ ┌─────────────────┐
28
+ │ Validate Job │ ◄─── Entry Point (start_with)
29
+ │ ValidatorWorker │ Always executes
30
+ └─────────────────┘
31
+
32
+ │ ValidationResult {
33
+ │ is_positive: boolean,
34
+ │ is_even: boolean
35
+ │ }
36
+
37
+ ├──────────────┬──────────────┬──────────────┐
38
+ │ │ │ │
39
+ │ if positive │ if even & │ if odd & │
40
+ │ │ !positive │ !positive │
41
+ ▼ ▼ ▼ │
42
+ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
43
+ │ Double │ │ Square │ │PassThru │ │
44
+ │ Worker │ │ Worker │ │ Worker │ │
45
+ └──────────┘ └──────────┘ └──────────┘ │
46
+ │ │ │ │
47
+ │ Doubles │ Squares │ Unchanged │
48
+ │ the value │ the value │ value │
49
+ ▼ ▼ ▼ │
50
+ ProcessedNumber ProcessedNumber ProcessedNumber │
51
+ { result: X*2, { result: X², { result: X, │
52
+ operation: operation: operation: │
53
+ "doubled" } "squared" } "unchanged" } │
54
+ │ │ │ │
55
+ └──────────────┴──────────────┴──────────────┘
56
+
57
+ │ ONE of the above (never multiple)
58
+
59
+ [Workflow Output] ◄─── Exit Points (end_with)
60
+ (multiple conditional exits)
61
+ ----
62
+
63
+ .Example Execution Paths
64
+ [source]
65
+ ----
66
+ Input: 5 (positive)
67
+ ────────────────────────────────────────
68
+ [validate] → [double] → Output: 10
69
+
70
+ Condition evaluation:
71
+ validate → is_positive = true
72
+ double → if_condition evaluates to true ✓
73
+ square → if_condition evaluates to false (skipped)
74
+ passthrough → if_condition evaluates to false (skipped)
75
+
76
+ ────────────────────────────────────────
77
+
78
+ Input: -4 (negative, even)
79
+ ────────────────────────────────────────
80
+ [validate] → [square] → Output: 16
81
+
82
+ Condition evaluation:
83
+ validate → is_even = true, is_positive = false
84
+ double → if_condition evaluates to false (skipped)
85
+ square → if_condition evaluates to true ✓
86
+ passthrough → if_condition evaluates to false (skipped)
87
+
88
+ ────────────────────────────────────────
89
+
90
+ Input: -3 (negative, odd)
91
+ ────────────────────────────────────────
92
+ [validate] → [passthrough] → Output: -3
93
+
94
+ Condition evaluation:
95
+ validate → is_even = false, is_positive = false
96
+ double → if_condition evaluates to false (skipped)
97
+ square → if_condition evaluates to false (skipped)
98
+ passthrough → if_condition evaluates to true ✓
99
+ ----
100
+
101
+ == Key Components
102
+
103
+ === Data Models
104
+
105
+ The workflow uses three data models:
106
+
107
+ [source,ruby]
108
+ ----
109
+ class NumberInput
110
+ attr_accessor :value
111
+
112
+ def initialize(value: 0)
113
+ @value = value
114
+ end
115
+ end
116
+
117
+ class ValidationResult
118
+ attr_accessor :is_positive, :is_even
119
+
120
+ def initialize(is_positive: false, is_even: false)
121
+ @is_positive = is_positive
122
+ @is_even = is_even
123
+ end
124
+ end
125
+
126
+ class ProcessedNumber
127
+ attr_accessor :result, :operation
128
+
129
+ def initialize(result: 0, operation: "")
130
+ @result = result
131
+ @operation = operation
132
+ end
133
+ end
134
+ ----
135
+
136
+ === Workers
137
+
138
+ Validator worker analyzes the input:
139
+
140
+ [source,ruby]
141
+ ----
142
+ class ValidatorWorker < Fractor::Worker
143
+ input_type NumberInput
144
+ output_type ValidationResult
145
+
146
+ def process(work)
147
+ input = work.input
148
+
149
+ output = ValidationResult.new(
150
+ is_positive: input.value > 0,
151
+ is_even: input.value.even?,
152
+ )
153
+
154
+ Fractor::WorkResult.new(result: output, work: work)
155
+ end
156
+ end
157
+ ----
158
+
159
+ Processing workers execute conditionally:
160
+
161
+ [source,ruby]
162
+ ----
163
+ class DoubleWorker < Fractor::Worker
164
+ input_type NumberInput
165
+ output_type ProcessedNumber
166
+
167
+ def process(work)
168
+ input = work.input
169
+ result = input.value * 2
170
+
171
+ output = ProcessedNumber.new(
172
+ result: result,
173
+ operation: "doubled",
174
+ )
175
+
176
+ Fractor::WorkResult.new(result: output, work: work)
177
+ end
178
+ end
179
+ ----
180
+
181
+ === Workflow Definition
182
+
183
+ The workflow defines conditional execution logic:
184
+
185
+ [source,ruby]
186
+ ----
187
+ class ConditionalWorkflow < Fractor::Workflow
188
+ workflow "conditional_example" do
189
+ input_type NumberInput
190
+ output_type ProcessedNumber
191
+
192
+ # Define workflow boundaries
193
+ start_with "validate" # <1>
194
+ end_with "double", on: :success # <2>
195
+ end_with "square", on: :success
196
+ end_with "passthrough", on: :success
197
+
198
+ # Job 1: Validate the number (always runs)
199
+ job "validate" do
200
+ runs_with ValidatorWorker
201
+ inputs_from_workflow
202
+ end
203
+
204
+ # Job 2: Double if positive (conditional)
205
+ job "double" do
206
+ runs_with DoubleWorker
207
+ needs "validate"
208
+ inputs_from_workflow # <3>
209
+ if_condition ->(context) { # <4>
210
+ validation = context.job_output("validate") # <5>
211
+ validation.is_positive # <6>
212
+ }
213
+ outputs_to_workflow
214
+ terminates_workflow # <7>
215
+ end
216
+
217
+ # Job 3: Square if even and not positive
218
+ job "square" do
219
+ runs_with SquareWorker
220
+ needs "validate"
221
+ inputs_from_workflow
222
+ if_condition ->(context) {
223
+ validation = context.job_output("validate")
224
+ validation.is_even && !validation.is_positive
225
+ }
226
+ outputs_to_workflow
227
+ terminates_workflow
228
+ end
229
+
230
+ # Job 4: Pass through if neither positive nor even
231
+ job "passthrough" do
232
+ runs_with PassThroughWorker
233
+ needs "validate"
234
+ inputs_from_workflow
235
+ if_condition ->(context) {
236
+ validation = context.job_output("validate")
237
+ !validation.is_positive && !validation.is_even
238
+ }
239
+ outputs_to_workflow
240
+ terminates_workflow
241
+ end
242
+ end
243
+ end
244
+ ----
245
+ <1> Validation job always executes first
246
+ <2> Multiple exit points, each conditional on success
247
+ <3> Conditional jobs still receive workflow input, not validation output
248
+ <4> Lambda expression for condition evaluation
249
+ <5> Access validation job's output from context
250
+ <6> Return boolean to determine if job should execute
251
+ <7> Job terminates workflow when it executes
252
+
253
+ == Key Features
254
+
255
+ === Conditional Execution
256
+
257
+ Jobs execute only when their condition evaluates to true:
258
+
259
+ [source,ruby]
260
+ ----
261
+ job "double" do
262
+ if_condition ->(context) {
263
+ validation = context.job_output("validate")
264
+ validation.is_positive # Returns true or false
265
+ }
266
+ end
267
+ ----
268
+
269
+ The lambda receives the workflow context and must return a boolean:
270
+
271
+ * `true`: Job executes
272
+ * `false`: Job skips, workflow continues to next job
273
+
274
+ === Context Access
275
+
276
+ The context object provides access to:
277
+
278
+ [source,ruby]
279
+ ----
280
+ context.job_output("job_name") # <1>
281
+ context.workflow_input # <2>
282
+ ----
283
+ <1> Output from a completed job
284
+ <2> Original workflow input
285
+
286
+ Example usage:
287
+
288
+ [source,ruby]
289
+ ----
290
+ if_condition ->(context) {
291
+ validation = context.job_output("validate")
292
+ input = context.workflow_input
293
+
294
+ # Make decision based on both
295
+ validation.is_positive && input.value > 10
296
+ }
297
+ ----
298
+
299
+ === Multiple Termination Points
300
+
301
+ Multiple jobs can terminate the workflow:
302
+
303
+ [source,ruby]
304
+ ----
305
+ job "double" do
306
+ terminates_workflow # <1>
307
+ end
308
+
309
+ job "square" do
310
+ terminates_workflow # <1>
311
+ end
312
+
313
+ job "passthrough" do
314
+ terminates_workflow # <1>
315
+ end
316
+ ----
317
+ <1> Any of these jobs can end the workflow
318
+
319
+ Only one will execute due to mutually exclusive conditions.
320
+
321
+ === Mutually Exclusive Conditions
322
+
323
+ Conditions are designed to be mutually exclusive:
324
+
325
+ [source,ruby]
326
+ ----
327
+ # Only ONE of these will be true for any input
328
+ if_condition ->(ctx) {
329
+ ctx.job_output("validate").is_positive # Positive numbers
330
+ }
331
+
332
+ if_condition ->(ctx) {
333
+ validation = ctx.job_output("validate")
334
+ validation.is_even && !validation.is_positive # Negative even
335
+ }
336
+
337
+ if_condition ->(ctx) {
338
+ validation = ctx.job_output("validate")
339
+ !validation.is_positive && !validation.is_even # Negative odd
340
+ }
341
+ ----
342
+
343
+ == Usage
344
+
345
+ Run the example from the project root:
346
+
347
+ [source,shell]
348
+ ----
349
+ ruby examples/workflow/conditional/conditional_workflow.rb
350
+ ----
351
+
352
+ == Expected Output
353
+
354
+ [example]
355
+ ====
356
+ [source]
357
+ ----
358
+ ============================================================
359
+ Conditional Workflow Example
360
+ ============================================================
361
+
362
+ Test Case 1: Positive number (should double)
363
+ ------------------------------------------------------------
364
+ Input: 5
365
+
366
+ [Validator] Checking number: 5
367
+ [Validator] Positive: true, Even: false
368
+ [DoubleWorker] Doubled 5 to 10
369
+
370
+ Results:
371
+ Status: SUCCESS
372
+ Execution Time: 0.001s
373
+ Completed Jobs: validate, double
374
+ Final Result: 10
375
+ Operation: doubled
376
+
377
+ ============================================================
378
+
379
+ Test Case 2: Negative even number (should square)
380
+ ------------------------------------------------------------
381
+ Input: -4
382
+
383
+ [Validator] Checking number: -4
384
+ [Validator] Positive: false, Even: true
385
+ [SquareWorker] Squared -4 to 16
386
+
387
+ Results:
388
+ Status: SUCCESS
389
+ Execution Time: 0.0s
390
+ Completed Jobs: validate, square
391
+ Final Result: 16
392
+ Operation: squared
393
+
394
+ ============================================================
395
+
396
+ Test Case 3: Negative odd number (should pass through)
397
+ ------------------------------------------------------------
398
+ Input: -3
399
+
400
+ [Validator] Checking number: -3
401
+ [Validator] Positive: false, Even: false
402
+ [PassThrough] Keeping original value: -3
403
+
404
+ Results:
405
+ Status: SUCCESS
406
+ Execution Time: 0.0s
407
+ Completed Jobs: validate, passthrough
408
+ Final Result: -3
409
+ Operation: unchanged
410
+
411
+ ============================================================
412
+ ----
413
+ ====
414
+
415
+ == Learning Points
416
+
417
+ === Conditional Execution
418
+
419
+ * Use `if_condition` with a lambda to control job execution
420
+ * Lambda receives workflow context for decision making
421
+ * Jobs with false conditions are skipped, not failed
422
+
423
+ === Context-Based Decisions
424
+
425
+ * Access previous job outputs via `context.job_output("name")`
426
+ * Access workflow input via `context.workflow_input`
427
+ * Combine multiple data sources for complex conditions
428
+
429
+ === Multiple Exit Points
430
+
431
+ * Multiple jobs can be marked with `terminates_workflow`
432
+ * Use `end_with "job", on: :success` for conditional exits
433
+ * Only one termination job executes per workflow run
434
+
435
+ === Branching Patterns
436
+
437
+ * Create decision trees based on validation results
438
+ * Design mutually exclusive conditions for clear flow
439
+ * Each branch can perform different operations
440
+
441
+ === Job Dependencies
442
+
443
+ * Conditional jobs still respect `needs` dependencies
444
+ * Validation job must complete before condition evaluation
445
+ * Skipped jobs don't block downstream jobs if not needed
446
+
447
+ == Design Considerations
448
+
449
+ === Condition Design
450
+
451
+ When designing conditions:
452
+
453
+ 1. **Make conditions mutually exclusive** - Only one path should execute
454
+ 2. **Handle all cases** - Ensure at least one condition will be true
455
+ 3. **Keep conditions simple** - Complex logic should be in workers
456
+ 4. **Document expected paths** - Comment which inputs trigger which paths
457
+
458
+ === Error Handling
459
+
460
+ * If no condition evaluates to true, workflow may complete without output
461
+ * Consider a default "catch-all" condition for safety
462
+ * Validation failures should be handled in the validation worker
463
+
464
+ === Testing Strategy
465
+
466
+ Test each conditional path:
467
+
468
+ [source,ruby]
469
+ ----
470
+ test_cases = [
471
+ { value: 5, expected_op: "doubled" }, # Positive
472
+ { value: -4, expected_op: "squared" }, # Negative even
473
+ { value: -3, expected_op: "unchanged" }, # Negative odd
474
+ ]
475
+ ----
476
+
477
+ == Next Steps
478
+
479
+ After understanding conditional workflows, explore:
480
+
481
+ * link:../simple_linear/README.adoc[Simple Linear Workflow] - Sequential processing basics
482
+ * link:../fan_out/README.adoc[Fan-Out Workflow] - Parallel processing patterns
483
+ * link:../README.adoc[Workflow Overview] - Complete workflow system documentation
@@ -0,0 +1,215 @@
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 NumberInput
8
+ attr_accessor :value
9
+
10
+ def initialize(value: 0)
11
+ @value = value
12
+ end
13
+ end
14
+
15
+ class ValidationResult
16
+ attr_accessor :is_positive, :is_even
17
+
18
+ def initialize(is_positive: false, is_even: false)
19
+ @is_positive = is_positive
20
+ @is_even = is_even
21
+ end
22
+ end
23
+
24
+ class ProcessedNumber
25
+ attr_accessor :result, :operation
26
+
27
+ def initialize(result: 0, operation: "")
28
+ @result = result
29
+ @operation = operation
30
+ end
31
+ end
32
+
33
+ module ConditionalExample
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 validates the number
42
+ class ValidatorWorker < Fractor::Worker
43
+ input_type NumberInput
44
+ output_type ValidationResult
45
+
46
+ def process(work)
47
+ input = work.input
48
+ puts "[Validator] Checking number: #{input.value}" if ConditionalExample.debug_output
49
+
50
+ output = ValidationResult.new(
51
+ is_positive: input.value > 0,
52
+ is_even: input.value.even?,
53
+ )
54
+
55
+ puts "[Validator] Positive: #{output.is_positive}, Even: #{output.is_even}" if ConditionalExample.debug_output
56
+ Fractor::WorkResult.new(result: output, work: work)
57
+ end
58
+ end
59
+
60
+ # Worker that doubles positive numbers
61
+ class DoubleWorker < Fractor::Worker
62
+ input_type NumberInput
63
+ output_type ProcessedNumber
64
+
65
+ def process(work)
66
+ input = work.input
67
+ result = input.value * 2
68
+ puts "[DoubleWorker] Doubled #{input.value} to #{result}" if ConditionalExample.debug_output
69
+
70
+ output = ProcessedNumber.new(
71
+ result: result,
72
+ operation: "doubled",
73
+ )
74
+
75
+ Fractor::WorkResult.new(result: output, work: work)
76
+ end
77
+ end
78
+
79
+ # Worker that squares even numbers
80
+ class SquareWorker < Fractor::Worker
81
+ input_type NumberInput
82
+ output_type ProcessedNumber
83
+
84
+ def process(work)
85
+ input = work.input
86
+ result = input.value**2
87
+ puts "[SquareWorker] Squared #{input.value} to #{result}" if ConditionalExample.debug_output
88
+
89
+ output = ProcessedNumber.new(
90
+ result: result,
91
+ operation: "squared",
92
+ )
93
+
94
+ Fractor::WorkResult.new(result: output, work: work)
95
+ end
96
+ end
97
+
98
+ # Worker that returns original for non-positive, non-even numbers
99
+ class PassThroughWorker < Fractor::Worker
100
+ input_type NumberInput
101
+ output_type ProcessedNumber
102
+
103
+ def process(work)
104
+ input = work.input
105
+ puts "[PassThrough] Keeping original value: #{input.value}" if ConditionalExample.debug_output
106
+
107
+ output = ProcessedNumber.new(
108
+ result: input.value,
109
+ operation: "unchanged",
110
+ )
111
+
112
+ Fractor::WorkResult.new(result: output, work: work)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Define the conditional workflow
118
+ class ConditionalWorkflow < Fractor::Workflow
119
+ workflow "conditional_example" do
120
+ input_type NumberInput
121
+ output_type ProcessedNumber
122
+
123
+ # Define workflow start and end
124
+ start_with "validate"
125
+ end_with "double", on: :success
126
+ end_with "square", on: :success
127
+ end_with "passthrough", on: :success
128
+
129
+ # Job 1: Validate the number
130
+ job "validate" do
131
+ runs_with ConditionalExample::ValidatorWorker
132
+ inputs_from_workflow
133
+ end
134
+
135
+ # Job 2: Double if positive (conditional)
136
+ job "double" do
137
+ runs_with ConditionalExample::DoubleWorker
138
+ needs "validate"
139
+ inputs_from_workflow
140
+ if_condition ->(context) {
141
+ validation = context.job_output("validate")
142
+ validation.is_positive
143
+ }
144
+ outputs_to_workflow
145
+ terminates_workflow
146
+ end
147
+
148
+ # Job 3: Square if even (conditional)
149
+ job "square" do
150
+ runs_with ConditionalExample::SquareWorker
151
+ needs "validate"
152
+ inputs_from_workflow
153
+ if_condition ->(context) {
154
+ validation = context.job_output("validate")
155
+ validation.is_even && !validation.is_positive
156
+ }
157
+ outputs_to_workflow
158
+ terminates_workflow
159
+ end
160
+
161
+ # Job 4: Pass through if neither positive nor even (conditional)
162
+ job "passthrough" do
163
+ runs_with ConditionalExample::PassThroughWorker
164
+ needs "validate"
165
+ inputs_from_workflow
166
+ if_condition ->(context) {
167
+ validation = context.job_output("validate")
168
+ !validation.is_positive && !validation.is_even
169
+ }
170
+ outputs_to_workflow
171
+ terminates_workflow
172
+ end
173
+ end
174
+ end
175
+
176
+ # Only run the example when this file is executed directly
177
+ if __FILE__ == $PROGRAM_NAME
178
+ # Execute the workflow with different inputs
179
+ puts "=" * 60
180
+ puts "Conditional Workflow Example"
181
+ puts "=" * 60
182
+ puts ""
183
+
184
+ # Enable debug output for demonstration
185
+ ConditionalExample.debug_output = true
186
+
187
+ test_cases = [
188
+ { value: 5, description: "Positive number (should double)" },
189
+ { value: -4, description: "Negative even number (should square)" },
190
+ { value: -3, description: "Negative odd number (should pass through)" },
191
+ ]
192
+
193
+ test_cases.each_with_index do |test_case, index|
194
+ puts "Test Case #{index + 1}: #{test_case[:description]}"
195
+ puts "-" * 60
196
+
197
+ input = NumberInput.new(value: test_case[:value])
198
+ puts "Input: #{input.value}"
199
+ puts ""
200
+
201
+ workflow = ConditionalWorkflow.new
202
+ result = workflow.execute(input: input)
203
+
204
+ puts ""
205
+ puts "Results:"
206
+ puts " Status: #{result.success? ? 'SUCCESS' : 'FAILED'}"
207
+ puts " Execution Time: #{result.execution_time.round(3)}s"
208
+ puts " Completed Jobs: #{result.completed_jobs.join(', ')}"
209
+ puts " Final Result: #{result.output.result}"
210
+ puts " Operation: #{result.output.operation}"
211
+ puts ""
212
+ puts "=" * 60
213
+ puts ""
214
+ end
215
+ end