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.
- 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,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../lib/fractor"
|
|
4
|
+
|
|
5
|
+
# This example demonstrates a simple linear workflow with three sequential jobs.
|
|
6
|
+
# Each job processes data and passes it to the next job.
|
|
7
|
+
|
|
8
|
+
# ============================================
|
|
9
|
+
# DATA MODELS
|
|
10
|
+
# ============================================
|
|
11
|
+
|
|
12
|
+
module SimpleLinear
|
|
13
|
+
# Simple data models (in production, use Lutaml::Model::Serializable)
|
|
14
|
+
class TextData
|
|
15
|
+
attr_accessor :text
|
|
16
|
+
|
|
17
|
+
def initialize(text:)
|
|
18
|
+
@text = text
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class UppercaseOutput
|
|
23
|
+
attr_accessor :uppercased_text, :char_count
|
|
24
|
+
|
|
25
|
+
def initialize(uppercased_text:, char_count:)
|
|
26
|
+
@uppercased_text = uppercased_text
|
|
27
|
+
@char_count = char_count
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ReversedOutput
|
|
32
|
+
attr_accessor :reversed_text, :word_count
|
|
33
|
+
|
|
34
|
+
def initialize(reversed_text:, word_count:)
|
|
35
|
+
@reversed_text = reversed_text
|
|
36
|
+
@word_count = word_count
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class FinalOutput
|
|
41
|
+
attr_accessor :result, :total_operations
|
|
42
|
+
|
|
43
|
+
def initialize(result:, total_operations:)
|
|
44
|
+
@result = result
|
|
45
|
+
@total_operations = total_operations
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ============================================
|
|
51
|
+
# WORKERS
|
|
52
|
+
# ============================================
|
|
53
|
+
|
|
54
|
+
module SimpleLinearExample
|
|
55
|
+
class UppercaseWorker < Fractor::Worker
|
|
56
|
+
input_type SimpleLinear::TextData
|
|
57
|
+
output_type SimpleLinear::UppercaseOutput
|
|
58
|
+
|
|
59
|
+
def process(work)
|
|
60
|
+
input = work.input
|
|
61
|
+
uppercased = input.text.upcase
|
|
62
|
+
|
|
63
|
+
output = SimpleLinear::UppercaseOutput.new(
|
|
64
|
+
uppercased_text: uppercased,
|
|
65
|
+
char_count: uppercased.length,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class ReverseWorker < Fractor::Worker
|
|
73
|
+
input_type SimpleLinear::UppercaseOutput
|
|
74
|
+
output_type SimpleLinear::ReversedOutput
|
|
75
|
+
|
|
76
|
+
def process(work)
|
|
77
|
+
input = work.input
|
|
78
|
+
reversed = input.uppercased_text.reverse
|
|
79
|
+
|
|
80
|
+
output = SimpleLinear::ReversedOutput.new(
|
|
81
|
+
reversed_text: reversed,
|
|
82
|
+
word_count: reversed.split.size,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class FinalizeWorker < Fractor::Worker
|
|
90
|
+
input_type SimpleLinear::ReversedOutput
|
|
91
|
+
output_type SimpleLinear::FinalOutput
|
|
92
|
+
|
|
93
|
+
def process(work)
|
|
94
|
+
input = work.input
|
|
95
|
+
|
|
96
|
+
output = SimpleLinear::FinalOutput.new(
|
|
97
|
+
result: input.reversed_text,
|
|
98
|
+
total_operations: 3,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ============================================
|
|
107
|
+
# WORKFLOW DEFINITION
|
|
108
|
+
# ============================================
|
|
109
|
+
|
|
110
|
+
class SimpleLinearWorkflow < Fractor::Workflow
|
|
111
|
+
workflow "simple-linear" do
|
|
112
|
+
# Define workflow input and output types
|
|
113
|
+
input_type SimpleLinear::TextData
|
|
114
|
+
output_type SimpleLinear::FinalOutput
|
|
115
|
+
|
|
116
|
+
# Define start and end points
|
|
117
|
+
start_with "uppercase"
|
|
118
|
+
end_with "finalize"
|
|
119
|
+
|
|
120
|
+
# Job 1: Uppercase the text
|
|
121
|
+
job "uppercase" do
|
|
122
|
+
runs_with SimpleLinearExample::UppercaseWorker
|
|
123
|
+
inputs_from_workflow
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Job 2: Reverse the uppercased text
|
|
127
|
+
job "reverse" do
|
|
128
|
+
needs "uppercase"
|
|
129
|
+
runs_with SimpleLinearExample::ReverseWorker
|
|
130
|
+
inputs_from_job "uppercase"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Job 3: Finalize the result
|
|
134
|
+
job "finalize" do
|
|
135
|
+
needs "reverse"
|
|
136
|
+
runs_with SimpleLinearExample::FinalizeWorker
|
|
137
|
+
inputs_from_job "reverse"
|
|
138
|
+
outputs_to_workflow
|
|
139
|
+
terminates_workflow
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ============================================
|
|
145
|
+
# USAGE
|
|
146
|
+
# ============================================
|
|
147
|
+
|
|
148
|
+
if __FILE__ == $PROGRAM_NAME
|
|
149
|
+
puts "Simple Linear Workflow Example"
|
|
150
|
+
puts "=" * 50
|
|
151
|
+
puts
|
|
152
|
+
|
|
153
|
+
# Create input
|
|
154
|
+
input = SimpleLinear::TextData.new(text: "hello world from fractor")
|
|
155
|
+
puts "Input: #{input.text}"
|
|
156
|
+
puts
|
|
157
|
+
|
|
158
|
+
# Execute workflow
|
|
159
|
+
workflow = SimpleLinearWorkflow.new
|
|
160
|
+
result = workflow.execute(input: input)
|
|
161
|
+
|
|
162
|
+
# Display results
|
|
163
|
+
puts "Workflow Results:"
|
|
164
|
+
puts "-" * 50
|
|
165
|
+
puts "Status: #{result.success? ? 'SUCCESS' : 'FAILED'}"
|
|
166
|
+
puts "Execution Time: #{result.execution_time.round(3)}s"
|
|
167
|
+
puts "Completed Jobs: #{result.completed_jobs.join(', ')}"
|
|
168
|
+
puts
|
|
169
|
+
puts "Final Output:"
|
|
170
|
+
puts " Result: #{result.output.result}"
|
|
171
|
+
puts " Total Operations: #{result.output.total_operations}"
|
|
172
|
+
puts
|
|
173
|
+
|
|
174
|
+
# Expected output: "ROTCARF MORF DLROW OLLEH"
|
|
175
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
= Simplified Workflow Syntax
|
|
2
|
+
|
|
3
|
+
This example demonstrates the simplified workflow syntax that reduces boilerplate and makes workflow definition more intuitive.
|
|
4
|
+
|
|
5
|
+
== Overview
|
|
6
|
+
|
|
7
|
+
The simplified syntax provides three major improvements:
|
|
8
|
+
|
|
9
|
+
1. **Smart Auto-Wiring**: Automatically infers input sources from dependencies
|
|
10
|
+
2. **Smart Defaults**: Auto-detects start and end jobs
|
|
11
|
+
3. **Shorthand Syntax**: Concise job definitions with keyword parameters
|
|
12
|
+
|
|
13
|
+
Additionally, two new APIs are provided:
|
|
14
|
+
|
|
15
|
+
4. **Workflow.define**: Create workflows without inheritance
|
|
16
|
+
5. **Chain API**: Fluent interface for linear workflows
|
|
17
|
+
|
|
18
|
+
== Running the Example
|
|
19
|
+
|
|
20
|
+
[source,shell]
|
|
21
|
+
----
|
|
22
|
+
ruby examples/workflow/simplified/simplified_workflow.rb
|
|
23
|
+
----
|
|
24
|
+
|
|
25
|
+
== Approach 1: Shorthand Syntax with Auto-Wiring
|
|
26
|
+
|
|
27
|
+
=== General
|
|
28
|
+
|
|
29
|
+
The shorthand syntax allows defining jobs concisely while smart defaults handle boilerplate configuration.
|
|
30
|
+
|
|
31
|
+
=== Benefits
|
|
32
|
+
|
|
33
|
+
* No need for `start_with` and `end_with` declarations
|
|
34
|
+
* No need for explicit `inputs_from_*` calls for single dependencies
|
|
35
|
+
* Automatic detection of workflow entry and exit points
|
|
36
|
+
|
|
37
|
+
=== Usage
|
|
38
|
+
|
|
39
|
+
[source,ruby]
|
|
40
|
+
----
|
|
41
|
+
class MyWorkflow < Fractor::Workflow
|
|
42
|
+
workflow "example" do
|
|
43
|
+
job "uppercase", UppercaseWorker
|
|
44
|
+
job "reverse", ReverseWorker, needs: "uppercase"
|
|
45
|
+
job "finalize", FinalizeWorker, needs: "reverse"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
----
|
|
49
|
+
|
|
50
|
+
Instead of the verbose form:
|
|
51
|
+
|
|
52
|
+
[source,ruby]
|
|
53
|
+
----
|
|
54
|
+
class MyWorkflow < Fractor::Workflow
|
|
55
|
+
workflow "example" do
|
|
56
|
+
start_with "uppercase"
|
|
57
|
+
end_with "finalize"
|
|
58
|
+
|
|
59
|
+
job "uppercase" do
|
|
60
|
+
runs_with UppercaseWorker
|
|
61
|
+
inputs_from_workflow
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
job "reverse" do
|
|
65
|
+
needs "uppercase"
|
|
66
|
+
runs_with ReverseWorker
|
|
67
|
+
inputs_from_job "uppercase"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
job "finalize" do
|
|
71
|
+
needs "reverse"
|
|
72
|
+
runs_with FinalizeWorker
|
|
73
|
+
inputs_from_job "reverse"
|
|
74
|
+
outputs_to_workflow
|
|
75
|
+
terminates_workflow
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
----
|
|
80
|
+
|
|
81
|
+
== Approach 2: Workflow.define (No Inheritance)
|
|
82
|
+
|
|
83
|
+
=== General
|
|
84
|
+
|
|
85
|
+
Create workflow classes without explicit inheritance, useful for dynamic workflow generation or storing workflows in variables.
|
|
86
|
+
|
|
87
|
+
=== Benefits
|
|
88
|
+
|
|
89
|
+
* No need to create a class definition
|
|
90
|
+
* Can be stored in constants or variables
|
|
91
|
+
* Returns workflow class directly
|
|
92
|
+
|
|
93
|
+
=== Usage
|
|
94
|
+
|
|
95
|
+
[source,ruby]
|
|
96
|
+
----
|
|
97
|
+
MyWorkflow = Fractor::Workflow.define("example") do
|
|
98
|
+
job "uppercase", UppercaseWorker
|
|
99
|
+
job "reverse", ReverseWorker, needs: "uppercase"
|
|
100
|
+
job "finalize", FinalizeWorker, needs: "reverse"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Use it the same way
|
|
104
|
+
workflow = MyWorkflow.new
|
|
105
|
+
result = workflow.execute(input: data)
|
|
106
|
+
----
|
|
107
|
+
|
|
108
|
+
== Approach 3: Chain API (Fluent Interface)
|
|
109
|
+
|
|
110
|
+
=== General
|
|
111
|
+
|
|
112
|
+
The Chain API provides a fluent interface specifically optimized for linear (sequential) workflows.
|
|
113
|
+
|
|
114
|
+
=== Benefits
|
|
115
|
+
|
|
116
|
+
* Most concise syntax for linear workflows
|
|
117
|
+
* Chainable method calls
|
|
118
|
+
* No explicit dependency management needed
|
|
119
|
+
* Reads naturally: step → step → step
|
|
120
|
+
|
|
121
|
+
=== Usage
|
|
122
|
+
|
|
123
|
+
[source,ruby]
|
|
124
|
+
----
|
|
125
|
+
workflow_class = Fractor::Workflow.chain("pipeline")
|
|
126
|
+
.step("uppercase", UppercaseWorker)
|
|
127
|
+
.step("reverse", ReverseWorker)
|
|
128
|
+
.step("finalize", FinalizeWorker)
|
|
129
|
+
.build
|
|
130
|
+
|
|
131
|
+
workflow = workflow_class.new
|
|
132
|
+
result = workflow.execute(input: data)
|
|
133
|
+
----
|
|
134
|
+
|
|
135
|
+
== Smart Auto-Wiring Rules
|
|
136
|
+
|
|
137
|
+
The auto-wiring system follows these rules:
|
|
138
|
+
|
|
139
|
+
=== Single Dependency
|
|
140
|
+
|
|
141
|
+
Jobs with exactly one dependency automatically wire inputs from that dependency:
|
|
142
|
+
|
|
143
|
+
[source,ruby]
|
|
144
|
+
----
|
|
145
|
+
job "process", ProcessWorker, needs: "validate"
|
|
146
|
+
# Automatically adds: inputs_from_job "validate"
|
|
147
|
+
----
|
|
148
|
+
|
|
149
|
+
=== No Dependencies (Start Jobs)
|
|
150
|
+
|
|
151
|
+
Jobs with no dependencies automatically wire inputs from the workflow:
|
|
152
|
+
|
|
153
|
+
[source,ruby]
|
|
154
|
+
----
|
|
155
|
+
job "start", StartWorker
|
|
156
|
+
# Automatically adds: inputs_from_workflow
|
|
157
|
+
----
|
|
158
|
+
|
|
159
|
+
=== Multiple Dependencies
|
|
160
|
+
|
|
161
|
+
Jobs with multiple dependencies require explicit input configuration:
|
|
162
|
+
|
|
163
|
+
[source,ruby]
|
|
164
|
+
----
|
|
165
|
+
job "merge", MergeWorker, needs: %w[process1 process2],
|
|
166
|
+
inputs: { "process1" => { data: :data1 }, "process2" => { data: :data2 } }
|
|
167
|
+
----
|
|
168
|
+
|
|
169
|
+
== Smart Defaults
|
|
170
|
+
|
|
171
|
+
=== Start Job Detection
|
|
172
|
+
|
|
173
|
+
When only one job has no dependencies, it is automatically set as the start job.
|
|
174
|
+
|
|
175
|
+
=== End Job Detection
|
|
176
|
+
|
|
177
|
+
Jobs with no dependents (leaf nodes) are automatically marked as end jobs with:
|
|
178
|
+
|
|
179
|
+
* `outputs_to_workflow`
|
|
180
|
+
* `terminates_workflow`
|
|
181
|
+
|
|
182
|
+
=== Multiple Start Jobs
|
|
183
|
+
|
|
184
|
+
When multiple jobs have no dependencies, you must explicitly specify `start_with`:
|
|
185
|
+
|
|
186
|
+
[source,ruby]
|
|
187
|
+
----
|
|
188
|
+
workflow "multi-start" do
|
|
189
|
+
start_with "primary" # Required when ambiguous
|
|
190
|
+
|
|
191
|
+
job "primary", PrimaryWorker
|
|
192
|
+
job "secondary", SecondaryWorker
|
|
193
|
+
job "merge", MergeWorker, needs: %w[primary secondary]
|
|
194
|
+
end
|
|
195
|
+
----
|
|
196
|
+
|
|
197
|
+
== Shorthand Job Syntax
|
|
198
|
+
|
|
199
|
+
=== Worker Class Parameter
|
|
200
|
+
|
|
201
|
+
Pass the worker class as the second parameter:
|
|
202
|
+
|
|
203
|
+
[source,ruby]
|
|
204
|
+
----
|
|
205
|
+
job "process", ProcessWorker
|
|
206
|
+
----
|
|
207
|
+
|
|
208
|
+
=== Keyword Parameters
|
|
209
|
+
|
|
210
|
+
All job configuration can be passed as keyword parameters:
|
|
211
|
+
|
|
212
|
+
[source,ruby]
|
|
213
|
+
----
|
|
214
|
+
job "process", ProcessWorker,
|
|
215
|
+
needs: "validate", # Dependencies
|
|
216
|
+
inputs: :workflow, # Input source
|
|
217
|
+
outputs: :workflow, # Output destination
|
|
218
|
+
workers: 3, # Parallel workers
|
|
219
|
+
condition: ->(ctx) { ... } # Conditional execution
|
|
220
|
+
----
|
|
221
|
+
|
|
222
|
+
=== Mixing with DSL Block
|
|
223
|
+
|
|
224
|
+
Shorthand syntax can be combined with DSL blocks:
|
|
225
|
+
|
|
226
|
+
[source,ruby]
|
|
227
|
+
----
|
|
228
|
+
job "process", ProcessWorker, needs: "validate" do
|
|
229
|
+
parallel_workers 5
|
|
230
|
+
# Additional DSL configuration
|
|
231
|
+
end
|
|
232
|
+
----
|
|
233
|
+
|
|
234
|
+
== Comparison: Before vs After
|
|
235
|
+
|
|
236
|
+
=== Linear Workflow (3 jobs)
|
|
237
|
+
|
|
238
|
+
==== Before (Verbose - 20 lines)
|
|
239
|
+
|
|
240
|
+
[source,ruby]
|
|
241
|
+
----
|
|
242
|
+
class Pipeline < Fractor::Workflow
|
|
243
|
+
workflow "pipeline" do
|
|
244
|
+
input_type InputData
|
|
245
|
+
output_type OutputData
|
|
246
|
+
start_with "step1"
|
|
247
|
+
end_with "step3"
|
|
248
|
+
|
|
249
|
+
job "step1" do
|
|
250
|
+
runs_with Worker1
|
|
251
|
+
inputs_from_workflow
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
job "step2" do
|
|
255
|
+
needs "step1"
|
|
256
|
+
runs_with Worker2
|
|
257
|
+
inputs_from_job "step1"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
job "step3" do
|
|
261
|
+
needs "step2"
|
|
262
|
+
runs_with Worker3
|
|
263
|
+
inputs_from_job "step2"
|
|
264
|
+
outputs_to_workflow
|
|
265
|
+
terminates_workflow
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
----
|
|
270
|
+
|
|
271
|
+
==== After (Shorthand - 6 lines)
|
|
272
|
+
|
|
273
|
+
[source,ruby]
|
|
274
|
+
----
|
|
275
|
+
class Pipeline < Fractor::Workflow
|
|
276
|
+
workflow "pipeline" do
|
|
277
|
+
job "step1", Worker1
|
|
278
|
+
job "step2", Worker2, needs: "step1"
|
|
279
|
+
job "step3", Worker3, needs: "step2"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
----
|
|
283
|
+
|
|
284
|
+
==== After (Chain API - 5 lines)
|
|
285
|
+
|
|
286
|
+
[source,ruby]
|
|
287
|
+
----
|
|
288
|
+
Pipeline = Fractor::Workflow.chain("pipeline")
|
|
289
|
+
.step("step1", Worker1)
|
|
290
|
+
.step("step2", Worker2)
|
|
291
|
+
.step("step3", Worker3)
|
|
292
|
+
.build
|
|
293
|
+
----
|
|
294
|
+
|
|
295
|
+
== Backward Compatibility
|
|
296
|
+
|
|
297
|
+
All existing verbose syntax continues to work. The simplified syntax is purely additive:
|
|
298
|
+
|
|
299
|
+
* Explicit `start_with` and `end_with` override auto-detection
|
|
300
|
+
* Explicit `inputs_from_*` overrides auto-wiring
|
|
301
|
+
* DSL blocks still work as before
|
|
302
|
+
* All validation rules remain the same
|
|
303
|
+
|
|
304
|
+
== When to Use Each Approach
|
|
305
|
+
|
|
306
|
+
=== Use Shorthand Syntax When:
|
|
307
|
+
|
|
308
|
+
* You have simple linear or branching workflows
|
|
309
|
+
* Dependencies are straightforward
|
|
310
|
+
* You want to keep class-based definition
|
|
311
|
+
|
|
312
|
+
=== Use Workflow.define When:
|
|
313
|
+
|
|
314
|
+
* You need to generate workflows dynamically
|
|
315
|
+
* You want to store workflows in variables
|
|
316
|
+
* You prefer functional style over inheritance
|
|
317
|
+
|
|
318
|
+
=== Use Chain API When:
|
|
319
|
+
|
|
320
|
+
* You have strictly linear workflows
|
|
321
|
+
* You want maximum conciseness
|
|
322
|
+
* Dependencies are purely sequential
|
|
323
|
+
|
|
324
|
+
=== Use Verbose Syntax When:
|
|
325
|
+
|
|
326
|
+
* You have complex input mappings
|
|
327
|
+
* You need fine-grained control
|
|
328
|
+
* Multiple jobs depend on multiple sources
|
|
329
|
+
* Documentation/clarity is more important than conciseness
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../lib/fractor"
|
|
4
|
+
|
|
5
|
+
# This example demonstrates the simplified workflow syntax options:
|
|
6
|
+
# 1. Shorthand syntax with auto-wiring
|
|
7
|
+
# 2. Workflow.define (no inheritance)
|
|
8
|
+
# 3. Chain API for linear workflows
|
|
9
|
+
|
|
10
|
+
# ============================================
|
|
11
|
+
# DATA MODELS
|
|
12
|
+
# ============================================
|
|
13
|
+
|
|
14
|
+
class TextData
|
|
15
|
+
attr_accessor :text
|
|
16
|
+
|
|
17
|
+
def initialize(text:)
|
|
18
|
+
@text = text
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class UppercaseOutput
|
|
23
|
+
attr_accessor :uppercased_text
|
|
24
|
+
|
|
25
|
+
def initialize(uppercased_text:)
|
|
26
|
+
@uppercased_text = uppercased_text
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ReversedOutput
|
|
31
|
+
attr_accessor :reversed_text
|
|
32
|
+
|
|
33
|
+
def initialize(reversed_text:)
|
|
34
|
+
@reversed_text = reversed_text
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class FinalOutput
|
|
39
|
+
attr_accessor :result
|
|
40
|
+
|
|
41
|
+
def initialize(result:)
|
|
42
|
+
@result = result
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ============================================
|
|
47
|
+
# WORKERS
|
|
48
|
+
# ============================================
|
|
49
|
+
|
|
50
|
+
module SimplifiedExample
|
|
51
|
+
class UppercaseWorker < Fractor::Worker
|
|
52
|
+
input_type TextData
|
|
53
|
+
output_type UppercaseOutput
|
|
54
|
+
|
|
55
|
+
def process(work)
|
|
56
|
+
input = work.input
|
|
57
|
+
output = UppercaseOutput.new(uppercased_text: input.text.upcase)
|
|
58
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class ReverseWorker < Fractor::Worker
|
|
63
|
+
input_type UppercaseOutput
|
|
64
|
+
output_type ReversedOutput
|
|
65
|
+
|
|
66
|
+
def process(work)
|
|
67
|
+
input = work.input
|
|
68
|
+
output = ReversedOutput.new(reversed_text: input.uppercased_text.reverse)
|
|
69
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class FinalizeWorker < Fractor::Worker
|
|
74
|
+
input_type ReversedOutput
|
|
75
|
+
output_type FinalOutput
|
|
76
|
+
|
|
77
|
+
def process(work)
|
|
78
|
+
input = work.input
|
|
79
|
+
output = FinalOutput.new(result: input.reversed_text)
|
|
80
|
+
Fractor::WorkResult.new(result: output, work: work)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ============================================
|
|
86
|
+
# APPROACH 1: SHORTHAND SYNTAX WITH AUTO-WIRING
|
|
87
|
+
# ============================================
|
|
88
|
+
# Benefits:
|
|
89
|
+
# - Reduced boilerplate
|
|
90
|
+
# - Auto-infers inputs from dependencies
|
|
91
|
+
# - Auto-detects start/end jobs
|
|
92
|
+
# - Still uses class inheritance
|
|
93
|
+
|
|
94
|
+
class ShorthandWorkflow < Fractor::Workflow
|
|
95
|
+
workflow "shorthand-example" do
|
|
96
|
+
# No need for start_with/end_with - auto-detected!
|
|
97
|
+
# No need for inputs_from_* - auto-wired from dependencies!
|
|
98
|
+
|
|
99
|
+
job "uppercase", SimplifiedExample::UppercaseWorker
|
|
100
|
+
job "reverse", SimplifiedExample::ReverseWorker, needs: "uppercase"
|
|
101
|
+
job "finalize", SimplifiedExample::FinalizeWorker, needs: "reverse"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ============================================
|
|
106
|
+
# APPROACH 2: WORKFLOW.DEFINE (NO INHERITANCE)
|
|
107
|
+
# ============================================
|
|
108
|
+
# Benefits:
|
|
109
|
+
# - No need to create a class
|
|
110
|
+
# - Returns workflow class directly
|
|
111
|
+
# - Can be stored in variables
|
|
112
|
+
|
|
113
|
+
SimplifiedWorkflow = Fractor::Workflow.define("simplified-example") do
|
|
114
|
+
job "uppercase", SimplifiedExample::UppercaseWorker
|
|
115
|
+
job "reverse", SimplifiedExample::ReverseWorker, needs: "uppercase"
|
|
116
|
+
job "finalize", SimplifiedExample::FinalizeWorker, needs: "reverse"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ============================================
|
|
120
|
+
# APPROACH 3: CHAIN API (FLUENT)
|
|
121
|
+
# ============================================
|
|
122
|
+
# Benefits:
|
|
123
|
+
# - Most concise for linear workflows
|
|
124
|
+
# - Fluent/chainable API
|
|
125
|
+
# - No explicit dependency management needed
|
|
126
|
+
|
|
127
|
+
ChainWorkflow = Fractor::Workflow.chain("chain-example")
|
|
128
|
+
.step("uppercase", SimplifiedExample::UppercaseWorker)
|
|
129
|
+
.step("reverse", SimplifiedExample::ReverseWorker)
|
|
130
|
+
.step("finalize", SimplifiedExample::FinalizeWorker)
|
|
131
|
+
.build
|
|
132
|
+
|
|
133
|
+
# ============================================
|
|
134
|
+
# COMPARISON: BEFORE VS AFTER
|
|
135
|
+
# ============================================
|
|
136
|
+
|
|
137
|
+
# BEFORE (Verbose - from simple_linear example):
|
|
138
|
+
# class SimpleLinearWorkflow < Fractor::Workflow
|
|
139
|
+
# workflow "simple-linear" do
|
|
140
|
+
# input_type TextData
|
|
141
|
+
# output_type FinalOutput
|
|
142
|
+
# start_with "uppercase"
|
|
143
|
+
# end_with "finalize"
|
|
144
|
+
#
|
|
145
|
+
# job "uppercase" do
|
|
146
|
+
# runs_with SimpleLinearExample::UppercaseWorker
|
|
147
|
+
# inputs_from_workflow
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# job "reverse" do
|
|
151
|
+
# needs "uppercase"
|
|
152
|
+
# runs_with SimpleLinearExample::ReverseWorker
|
|
153
|
+
# inputs_from_job "uppercase"
|
|
154
|
+
# end
|
|
155
|
+
#
|
|
156
|
+
# job "finalize" do
|
|
157
|
+
# needs "reverse"
|
|
158
|
+
# runs_with SimpleLinearExample::FinalizeWorker
|
|
159
|
+
# inputs_from_job "reverse"
|
|
160
|
+
# outputs_to_workflow
|
|
161
|
+
# terminates_workflow
|
|
162
|
+
# end
|
|
163
|
+
# end
|
|
164
|
+
# end
|
|
165
|
+
|
|
166
|
+
# AFTER (Shorthand):
|
|
167
|
+
# class ShorthandWorkflow < Fractor::Workflow
|
|
168
|
+
# workflow "shorthand-example" do
|
|
169
|
+
# job "uppercase", SimplifiedExample::UppercaseWorker
|
|
170
|
+
# job "reverse", SimplifiedExample::ReverseWorker, needs: "uppercase"
|
|
171
|
+
# job "finalize", SimplifiedExample::FinalizeWorker, needs: "reverse"
|
|
172
|
+
# end
|
|
173
|
+
# end
|
|
174
|
+
|
|
175
|
+
# AFTER (Workflow.define):
|
|
176
|
+
# SimplifiedWorkflow = Fractor::Workflow.define("simplified-example") do
|
|
177
|
+
# job "uppercase", SimplifiedExample::UppercaseWorker
|
|
178
|
+
# job "reverse", SimplifiedExample::ReverseWorker, needs: "uppercase"
|
|
179
|
+
# job "finalize", SimplifiedExample::FinalizeWorker, needs: "reverse"
|
|
180
|
+
# end
|
|
181
|
+
|
|
182
|
+
# AFTER (Chain API):
|
|
183
|
+
# ChainWorkflow = Fractor::Workflow.chain("chain-example")
|
|
184
|
+
# .step("uppercase", SimplifiedExample::UppercaseWorker)
|
|
185
|
+
# .step("reverse", SimplifiedExample::ReverseWorker)
|
|
186
|
+
# .step("finalize", SimplifiedExample::FinalizeWorker)
|
|
187
|
+
# .build
|
|
188
|
+
|
|
189
|
+
# ============================================
|
|
190
|
+
# USAGE
|
|
191
|
+
# ============================================
|
|
192
|
+
|
|
193
|
+
if __FILE__ == $PROGRAM_NAME
|
|
194
|
+
puts "Simplified Workflow Syntax Examples"
|
|
195
|
+
puts "=" * 60
|
|
196
|
+
puts
|
|
197
|
+
|
|
198
|
+
input = TextData.new(text: "hello world")
|
|
199
|
+
|
|
200
|
+
# Test all three approaches
|
|
201
|
+
[
|
|
202
|
+
["Shorthand Syntax", ShorthandWorkflow],
|
|
203
|
+
["Workflow.define", SimplifiedWorkflow],
|
|
204
|
+
["Chain API", ChainWorkflow],
|
|
205
|
+
].each do |name, workflow_class|
|
|
206
|
+
puts "#{name}:"
|
|
207
|
+
puts "-" * 60
|
|
208
|
+
|
|
209
|
+
workflow = workflow_class.new
|
|
210
|
+
result = workflow.execute(input: input)
|
|
211
|
+
|
|
212
|
+
puts "Status: #{result.success? ? 'SUCCESS' : 'FAILED'}"
|
|
213
|
+
puts "Result: #{result.output.result}"
|
|
214
|
+
puts "Jobs: #{result.completed_jobs.join(' → ')}"
|
|
215
|
+
puts
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Show visualization
|
|
219
|
+
puts "Workflow Diagram (ASCII):"
|
|
220
|
+
puts "-" * 60
|
|
221
|
+
ShorthandWorkflow.print_diagram
|
|
222
|
+
end
|