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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ class Workflow
5
+ # Generates visual representations of workflows.
6
+ # Supports Mermaid, DOT/Graphviz, and ASCII art formats.
7
+ class Visualizer
8
+ def initialize(workflow_class)
9
+ @workflow_class = workflow_class
10
+ @jobs = workflow_class.jobs
11
+ end
12
+
13
+ # Generate Mermaid flowchart diagram
14
+ #
15
+ # @return [String] Mermaid diagram syntax
16
+ def to_mermaid
17
+ lines = ["flowchart TD"]
18
+ lines << " Start([Start: #{@workflow_class.workflow_name}])"
19
+
20
+ # Add job nodes
21
+ @jobs.each do |name, job|
22
+ label = escape_mermaid(name)
23
+ worker = escape_mermaid(job.worker_class.name.split("::").last)
24
+
25
+ # Different shapes based on job type
26
+ lines << if job.terminates
27
+ " #{node_id(name)}[/#{label}<br/>#{worker}/]"
28
+ else
29
+ " #{node_id(name)}[#{label}<br/>#{worker}]"
30
+ end
31
+ end
32
+
33
+ lines << " End([End])"
34
+
35
+ # Add edges
36
+ @jobs.each do |name, job|
37
+ if job.dependencies.empty?
38
+ # Start job
39
+ lines << " Start --> #{node_id(name)}"
40
+ else
41
+ # Dependencies
42
+ job.dependencies.each do |dep|
43
+ edge_label = ""
44
+ if job.condition_proc
45
+ edge_label = "|conditional|"
46
+ end
47
+ lines << " #{node_id(dep)} -->#{edge_label} #{node_id(name)}"
48
+ end
49
+ end
50
+
51
+ # Terminating jobs
52
+ if job.terminates
53
+ lines << " #{node_id(name)} --> End"
54
+ end
55
+ end
56
+
57
+ # Add styling
58
+ lines << ""
59
+ lines << " classDef terminating fill:#f9f,stroke:#333,stroke-width:2px"
60
+ @jobs.each do |name, job|
61
+ lines << " class #{node_id(name)} terminating" if job.terminates
62
+ end
63
+
64
+ lines.join("\n")
65
+ end
66
+
67
+ # Generate DOT/Graphviz diagram
68
+ #
69
+ # @return [String] DOT syntax
70
+ def to_dot
71
+ lines = ["digraph #{dot_id(@workflow_class.workflow_name)} {"]
72
+ lines << " rankdir=TD;"
73
+ lines << " node [shape=box, style=rounded];"
74
+ lines << ""
75
+
76
+ # Start node
77
+ lines << ' start [label="Start", shape=ellipse];'
78
+
79
+ # Job nodes
80
+ @jobs.each do |name, job|
81
+ worker = job.worker_class.name
82
+ label = "#{name}\\n(#{worker})"
83
+
84
+ lines << if job.terminates
85
+ " #{dot_id(name)} [label=\"#{label}\", " \
86
+ "style=\"rounded,filled\", fillcolor=lightpink];"
87
+ else
88
+ " #{dot_id(name)} [label=\"#{label}\"];"
89
+ end
90
+ end
91
+
92
+ # End node
93
+ lines << ' end [label="End", shape=ellipse];'
94
+ lines << ""
95
+
96
+ # Edges
97
+ @jobs.each do |name, job|
98
+ if job.dependencies.empty?
99
+ lines << " start -> #{dot_id(name)};"
100
+ else
101
+ job.dependencies.each do |dep|
102
+ lines << if job.condition_proc
103
+ " #{dot_id(dep)} -> #{dot_id(name)} " \
104
+ "[label=\"conditional\", style=dashed];"
105
+ else
106
+ " #{dot_id(dep)} -> #{dot_id(name)};"
107
+ end
108
+ end
109
+ end
110
+
111
+ if job.terminates
112
+ lines << " #{dot_id(name)} -> end;"
113
+ end
114
+ end
115
+
116
+ lines << "}"
117
+ lines.join("\n")
118
+ end
119
+
120
+ # Generate ASCII art diagram
121
+ #
122
+ # @return [String] ASCII art representation
123
+ def to_ascii
124
+ lines = []
125
+ lines << "┌─────────────────────────────────────────┐"
126
+ lines << "│ Workflow: #{@workflow_class.workflow_name.ljust(27)} │"
127
+ lines << "└─────────────────────────────────────────┘"
128
+ lines << ""
129
+
130
+ # Compute execution order
131
+ order = compute_execution_order
132
+
133
+ order.each_with_index do |job_group, index|
134
+ if job_group.size == 1
135
+ # Single job
136
+ job = @jobs[job_group.first]
137
+ lines << " ┌─────────────────────────┐"
138
+ lines << " │ #{job_group.first.ljust(23)} │"
139
+ lines << " │ (#{job.worker_class.name.split('::').last.ljust(21)}) │"
140
+ lines << " └─────────────────────────┘"
141
+ else
142
+ # Parallel jobs
143
+ lines << " ╔═════════════════════════╗"
144
+ lines << " ║ PARALLEL EXECUTION ║"
145
+ lines << " ╚═════════════════════════╝"
146
+ job_group.each do |job_name|
147
+ job = @jobs[job_name]
148
+ lines << " ├─ #{job_name}"
149
+ lines << " │ (#{job.worker_class.name.split('::').last})"
150
+ end
151
+ end
152
+
153
+ # Arrow to next group
154
+ if index < order.size - 1
155
+ lines << " │"
156
+ lines << " ▼"
157
+ end
158
+ end
159
+
160
+ lines << ""
161
+ lines << "Legend: Regular jobs │ Parallel jobs ╔═══╗"
162
+
163
+ lines.join("\n")
164
+ end
165
+
166
+ # Print ASCII diagram to stdout
167
+ def print
168
+ puts to_ascii
169
+ end
170
+
171
+ private
172
+
173
+ def node_id(name)
174
+ name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
175
+ end
176
+
177
+ def dot_id(name)
178
+ name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
179
+ end
180
+
181
+ def escape_mermaid(text)
182
+ text.to_s.gsub(/["\[\]()]/, "")
183
+ end
184
+
185
+ def compute_execution_order
186
+ # Topological sort
187
+ jobs = @jobs
188
+ order = []
189
+ remaining = jobs.keys.to_set
190
+ processed = Set.new
191
+
192
+ until remaining.empty?
193
+ ready = remaining.select do |job_name|
194
+ job = jobs[job_name]
195
+ job.dependencies.all? { |dep| processed.include?(dep) }
196
+ end
197
+
198
+ break if ready.empty?
199
+
200
+ order << ready
201
+ ready.each do |job_name|
202
+ processed.add(job_name)
203
+ remaining.delete(job_name)
204
+ end
205
+ end
206
+
207
+ order
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ class Workflow
5
+ # Manages data flow and state during workflow execution.
6
+ # Stores workflow inputs, job outputs, and provides resolution of data dependencies.
7
+ class WorkflowContext
8
+ attr_reader :workflow_input, :job_outputs, :correlation_id, :logger
9
+
10
+ def initialize(workflow_input, correlation_id: nil, logger: nil)
11
+ @workflow_input = workflow_input
12
+ @job_outputs = {}
13
+ @correlation_id = correlation_id || generate_correlation_id
14
+ @logger = logger || WorkflowLogger.new(correlation_id: @correlation_id)
15
+ end
16
+
17
+ # Store the output of a completed job.
18
+ #
19
+ # @param job_name [String] The job name
20
+ # @param output [Lutaml::Model::Serializable] The job's output
21
+ def store_job_output(job_name, output)
22
+ @job_outputs[job_name] = output
23
+ end
24
+
25
+ # Get the output of a completed job.
26
+ #
27
+ # @param job_name [String] The job name
28
+ # @return [Lutaml::Model::Serializable, nil] The job's output
29
+ def job_output(job_name)
30
+ @job_outputs[job_name]
31
+ end
32
+
33
+ # Build input for a job based on its input mappings.
34
+ #
35
+ # @param job [Job] The job to build input for
36
+ # @return [Lutaml::Model::Serializable] The constructed input
37
+ def build_job_input(job)
38
+ return @workflow_input if job.input_mappings[:workflow]
39
+
40
+ input_type = job.input_type
41
+ unless input_type
42
+ raise "Job '#{job.name}' has no input_type defined in its worker"
43
+ end
44
+
45
+ # Collect attributes from all mapped sources
46
+ input_attrs = {}
47
+
48
+ job.input_mappings.each do |source_job_name, attr_mappings|
49
+ source_output = job_output(source_job_name)
50
+ unless source_output
51
+ raise "Job '#{job.name}' depends on '#{source_job_name}' but its output is not available"
52
+ end
53
+
54
+ if attr_mappings == :all
55
+ # Map all attributes from source to input
56
+ copy_all_attributes(source_output, input_attrs, input_type)
57
+ else
58
+ # Map specific attributes
59
+ attr_mappings.each do |target_attr, source_attr|
60
+ target_attr = target_attr.to_sym
61
+ source_attr = source_attr.to_sym
62
+
63
+ # Get value from source output
64
+ value = if source_output.respond_to?(source_attr)
65
+ source_output.send(source_attr)
66
+ elsif source_output.respond_to?(:[])
67
+ source_output[source_attr]
68
+ else
69
+ raise "Source output from '#{source_job_name}' does not have attribute '#{source_attr}'"
70
+ end
71
+
72
+ input_attrs[target_attr] = value
73
+ end
74
+ end
75
+ end
76
+
77
+ # Create input instance
78
+ input_type.new(**input_attrs)
79
+ end
80
+
81
+ # Check if a job's output is available.
82
+ #
83
+ # @param job_name [String] The job name
84
+ # @return [Boolean] Whether the output is available
85
+ def job_completed?(job_name)
86
+ @job_outputs.key?(job_name)
87
+ end
88
+
89
+ # Convert context to hash for debugging/logging
90
+ def to_h
91
+ {
92
+ correlation_id: @correlation_id,
93
+ workflow_input: @workflow_input.class.name,
94
+ completed_jobs: @job_outputs.keys,
95
+ }
96
+ end
97
+
98
+ private
99
+
100
+ def generate_correlation_id
101
+ require "securerandom"
102
+ "wf-#{SecureRandom.hex(8)}"
103
+ end
104
+
105
+ def copy_all_attributes(source, target_hash, input_type)
106
+ # Copy all compatible attributes from source to target
107
+ if defined?(Lutaml::Model::Serializable) &&
108
+ source.is_a?(Lutaml::Model::Serializable) &&
109
+ input_type.respond_to?(:attributes)
110
+ # Lutaml::Model path
111
+ source.class.attributes.each_key do |attr_name|
112
+ if input_type.attributes.key?(attr_name)
113
+ target_hash[attr_name] = source.send(attr_name)
114
+ end
115
+ end
116
+ else
117
+ # Fallback for plain Ruby classes
118
+ # Copy all instance variables from source that exist in target
119
+ source.instance_variables.each do |var|
120
+ attr_name = var.to_s.delete("@").to_sym
121
+
122
+ # Check if target class has this attribute (via attr_accessor/reader)
123
+ if input_type.instance_methods.include?(attr_name) ||
124
+ input_type.instance_methods.include?("#{attr_name}=".to_sym)
125
+ target_hash[attr_name] = source.instance_variable_get(var)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end