fractor 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +183 -0
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,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
|