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
data/exe/fractor
ADDED
data/lib/fractor/cli.rb
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Fractor
|
|
6
|
+
# Main Fractor CLI
|
|
7
|
+
class Cli < Thor
|
|
8
|
+
class_option :verbose, type: :boolean, aliases: "-v",
|
|
9
|
+
desc: "Enable verbose output"
|
|
10
|
+
class_option :debug, type: :boolean, aliases: "-d",
|
|
11
|
+
desc: "Enable debug logging"
|
|
12
|
+
|
|
13
|
+
# Validate command
|
|
14
|
+
desc "validate FILE", "Validate a workflow definition file"
|
|
15
|
+
def validate(file)
|
|
16
|
+
setup_logging
|
|
17
|
+
|
|
18
|
+
unless File.exist?(file)
|
|
19
|
+
warn "Error: File not found: #{file}"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
# Load and validate the workflow
|
|
25
|
+
workflow = load_workflow_class(file)
|
|
26
|
+
|
|
27
|
+
puts "✓ Valid workflow: #{workflow.workflow_name}"
|
|
28
|
+
puts " Mode: #{workflow.workflow_mode}"
|
|
29
|
+
puts " Jobs: #{workflow.jobs.size}"
|
|
30
|
+
|
|
31
|
+
# Validate each job
|
|
32
|
+
workflow.jobs.each do |name, job|
|
|
33
|
+
puts " - #{name} (#{job.worker_class})"
|
|
34
|
+
puts " Input: #{job.input_type}" if job.input_type
|
|
35
|
+
puts " Output: #{job.output_type}" if job.output_type
|
|
36
|
+
puts " Needs: #{job.needs.join(', ')}" if job.needs.any?
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
warn "Error: #{e.class}: #{e.message}"
|
|
40
|
+
warn e.backtrace.first(5) if options[:verbose]
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Visualize command
|
|
46
|
+
desc "visualize FILE", "Visualize a workflow definition"
|
|
47
|
+
method_option :format, type: :string, default: "ascii", aliases: "-f",
|
|
48
|
+
desc: "Output format: ascii, mermaid, dot"
|
|
49
|
+
method_option :output, type: :string, aliases: "-o",
|
|
50
|
+
desc: "Output file (default: stdout)"
|
|
51
|
+
|
|
52
|
+
def visualize(file)
|
|
53
|
+
setup_logging
|
|
54
|
+
|
|
55
|
+
unless File.exist?(file)
|
|
56
|
+
warn "Error: File not found: #{file}"
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
workflow = load_workflow_class(file)
|
|
62
|
+
visualizer = Fractor::Workflow::Visualizer.new(workflow)
|
|
63
|
+
|
|
64
|
+
output = case options[:format].to_sym
|
|
65
|
+
when :mermaid
|
|
66
|
+
visualizer.to_mermaid
|
|
67
|
+
when :dot
|
|
68
|
+
visualizer.to_dot
|
|
69
|
+
else
|
|
70
|
+
visualizer.to_ascii
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if options[:output]
|
|
74
|
+
File.write(options[:output], output)
|
|
75
|
+
puts "Visualization written to: #{options[:output]}"
|
|
76
|
+
else
|
|
77
|
+
puts output
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
warn "Error: #{e.class}: #{e.message}"
|
|
81
|
+
warn e.backtrace.first(5) if options[:verbose]
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Execute command
|
|
87
|
+
desc "execute FILE", "Execute a workflow with optional input data"
|
|
88
|
+
method_option :input, type: :string, aliases: "-i",
|
|
89
|
+
desc: "Input data (JSON string or file path)"
|
|
90
|
+
method_option :workers, type: :numeric, aliases: "-w",
|
|
91
|
+
desc: "Number of workers to use"
|
|
92
|
+
method_option :continuous, type: :boolean, aliases: "-c",
|
|
93
|
+
desc: "Run in continuous mode"
|
|
94
|
+
|
|
95
|
+
def execute(file)
|
|
96
|
+
setup_logging
|
|
97
|
+
|
|
98
|
+
unless File.exist?(file)
|
|
99
|
+
warn "Error: File not found: #{file}"
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
workflow = load_workflow_class(file)
|
|
105
|
+
input_data = parse_input_data
|
|
106
|
+
|
|
107
|
+
instance = workflow.new
|
|
108
|
+
|
|
109
|
+
puts "Running workflow: #{workflow.workflow_name}"
|
|
110
|
+
puts "Mode: #{workflow.workflow_mode}"
|
|
111
|
+
puts "Input: #{input_data.inspect}" if options[:verbose]
|
|
112
|
+
|
|
113
|
+
start_time = Time.now
|
|
114
|
+
result = instance.execute(input: input_data)
|
|
115
|
+
elapsed = Time.now - start_time
|
|
116
|
+
|
|
117
|
+
puts "\nWorkflow completed in #{elapsed.round(3)}s"
|
|
118
|
+
|
|
119
|
+
if result.success?
|
|
120
|
+
puts "Status: ✓ SUCCESS"
|
|
121
|
+
puts "Result: #{result.result.inspect}" if result.result
|
|
122
|
+
else
|
|
123
|
+
puts "Status: ✗ FAILED"
|
|
124
|
+
puts "Error: #{result.error}" if result.error
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
puts "Jobs completed: #{result.jobs_completed}" if result.jobs_completed
|
|
128
|
+
puts "Jobs failed: #{result.jobs_failed}" if result.jobs_failed
|
|
129
|
+
|
|
130
|
+
exit(result.success? ? 0 : 1)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn "Error: #{e.class}: #{e.message}"
|
|
133
|
+
warn e.backtrace.first(5) if options[:verbose]
|
|
134
|
+
exit 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Supervisor command
|
|
139
|
+
desc "supervisor WORKER_CLASS [INPUTS]",
|
|
140
|
+
"Run work items using Supervisor mode"
|
|
141
|
+
method_option :workers, type: :numeric, aliases: "-w", default: 4,
|
|
142
|
+
desc: "Number of workers to use"
|
|
143
|
+
method_option :input, type: :string, aliases: "-i",
|
|
144
|
+
desc: "Input data file (JSON)"
|
|
145
|
+
method_option :continuous, type: :boolean, aliases: "-c",
|
|
146
|
+
desc: "Run in continuous mode"
|
|
147
|
+
method_option :metrics, type: :boolean, aliases: "-m",
|
|
148
|
+
desc: "Show performance metrics"
|
|
149
|
+
|
|
150
|
+
def supervisor(worker_class, *inputs)
|
|
151
|
+
setup_logging
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
# Load the worker class
|
|
155
|
+
worker = load_worker_class(worker_class)
|
|
156
|
+
|
|
157
|
+
# Parse input data if provided
|
|
158
|
+
work_items = if options[:input]
|
|
159
|
+
parse_input_file(options[:input])
|
|
160
|
+
elsif inputs.any?
|
|
161
|
+
inputs.map { |input| Fractor::Work.new(input) }
|
|
162
|
+
else
|
|
163
|
+
warn "Error: No input data provided. Use --input FILE or provide INPUTS"
|
|
164
|
+
exit 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
num_workers = options[:workers] || 4
|
|
168
|
+
continuous_mode = options[:continuous] || false
|
|
169
|
+
|
|
170
|
+
puts "Starting Fractor Supervisor..."
|
|
171
|
+
puts "Worker: #{worker}"
|
|
172
|
+
puts "Workers: #{num_workers}"
|
|
173
|
+
puts "Mode: #{continuous_mode ? 'Continuous' : 'Batch'}"
|
|
174
|
+
puts "Work items: #{work_items.size}"
|
|
175
|
+
puts
|
|
176
|
+
|
|
177
|
+
supervisor = Fractor::Supervisor.new(
|
|
178
|
+
worker_pools: [{ worker_class: worker, num_workers: num_workers }],
|
|
179
|
+
continuous_mode: continuous_mode,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Add work items
|
|
183
|
+
work_items.each { |item| supervisor.add_work_item(item) }
|
|
184
|
+
|
|
185
|
+
# Run supervisor
|
|
186
|
+
start_time = Time.now
|
|
187
|
+
supervisor.run
|
|
188
|
+
elapsed = Time.now - start_time
|
|
189
|
+
|
|
190
|
+
results = supervisor.results
|
|
191
|
+
|
|
192
|
+
puts
|
|
193
|
+
puts "Completed in #{elapsed.round(3)}s"
|
|
194
|
+
puts "Results: #{results.results.size} successful"
|
|
195
|
+
puts "Errors: #{results.errors.size} failed"
|
|
196
|
+
|
|
197
|
+
if options[:metrics] && defined?(Fractor::PerformanceMonitor)
|
|
198
|
+
show_metrics(supervisor)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Exit with error code if any failures
|
|
202
|
+
exit(results.errors.empty? ? 0 : 1)
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
warn "Error: #{e.class}: #{e.message}"
|
|
205
|
+
warn e.backtrace.first(5) if options[:verbose]
|
|
206
|
+
exit 1
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
desc "version", "Show Fractor version"
|
|
211
|
+
def version
|
|
212
|
+
puts "Fractor #{Fractor::VERSION}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def setup_logging
|
|
218
|
+
Fractor.enable_logging if options[:debug]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def load_workflow_class(file)
|
|
222
|
+
workflow_code = File.read(file)
|
|
223
|
+
binding = TOPLEVEL_BINDING.dup
|
|
224
|
+
workflow = eval(workflow_code, binding, file)
|
|
225
|
+
|
|
226
|
+
unless workflow.is_a?(Class) && workflow < Fractor::Workflow
|
|
227
|
+
raise ArgumentError, "File does not contain a valid Workflow class"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
workflow
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def load_worker_class(worker_class)
|
|
234
|
+
# Try to load from a file first
|
|
235
|
+
file = File.exist?(worker_class) ? worker_class : "#{worker_class}.rb"
|
|
236
|
+
|
|
237
|
+
if File.exist?(file)
|
|
238
|
+
load file
|
|
239
|
+
# Extract class name from file
|
|
240
|
+
class_name = File.basename(file,
|
|
241
|
+
".rb").split("_").map(&:capitalize).join
|
|
242
|
+
const_get(class_name)
|
|
243
|
+
else
|
|
244
|
+
# Try to resolve as a constant
|
|
245
|
+
worker_class.split("::").inject(Object) do |obj, name|
|
|
246
|
+
obj&.const_get(name)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def parse_input_data
|
|
252
|
+
return nil unless options[:input]
|
|
253
|
+
|
|
254
|
+
input = options[:input]
|
|
255
|
+
|
|
256
|
+
# Check if it's a file path
|
|
257
|
+
if File.exist?(input)
|
|
258
|
+
JSON.parse(File.read(input))
|
|
259
|
+
else
|
|
260
|
+
# Try to parse as JSON
|
|
261
|
+
JSON.parse(input)
|
|
262
|
+
end
|
|
263
|
+
rescue JSON::ParserError
|
|
264
|
+
warn "Error: Invalid JSON input"
|
|
265
|
+
exit 1
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def parse_input_file(file)
|
|
269
|
+
data = if File.exist?(file)
|
|
270
|
+
JSON.parse(File.read(file))
|
|
271
|
+
else
|
|
272
|
+
[{ input: file }] # Treat as simple string input
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
data.map { |item| Fractor::Work.new(item) }
|
|
276
|
+
rescue JSON::ParserError
|
|
277
|
+
warn "Error: Invalid JSON in input file: #{file}"
|
|
278
|
+
exit 1
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def show_metrics(supervisor)
|
|
282
|
+
puts "\nPerformance Metrics:"
|
|
283
|
+
# Basic metrics from supervisor
|
|
284
|
+
puts " Workers: #{supervisor.workers.size}"
|
|
285
|
+
puts " Queue depth: #{supervisor.work_queue.size}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
module Fractor
|
|
7
|
+
# Central configuration management for Fractor.
|
|
8
|
+
# Provides a unified way to configure all Fractor components.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic configuration
|
|
11
|
+
# Fractor.configure do |config|
|
|
12
|
+
# config.logger = Logger.new(STDOUT)
|
|
13
|
+
# config.debug = true
|
|
14
|
+
# config.default_worker_timeout = 30
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Loading from YAML file
|
|
18
|
+
# Fractor.configure_from_file("config/fractor.yml")
|
|
19
|
+
#
|
|
20
|
+
# @example Environment variable support
|
|
21
|
+
# # Set FRACTOR_DEBUG=true to enable debug mode
|
|
22
|
+
# Fractor.config.debug # => true
|
|
23
|
+
class Configuration
|
|
24
|
+
# Default configuration values
|
|
25
|
+
DEFAULTS = {
|
|
26
|
+
debug: false,
|
|
27
|
+
log_level: Logger::INFO,
|
|
28
|
+
default_worker_timeout: 120,
|
|
29
|
+
default_max_retries: 3,
|
|
30
|
+
default_retry_delay: 1,
|
|
31
|
+
enable_performance_monitoring: false,
|
|
32
|
+
enable_error_reporting: false,
|
|
33
|
+
ractor_pool_size: nil, # nil = auto-detect (CPU count)
|
|
34
|
+
workflow_validation_strict: true,
|
|
35
|
+
thread_safe: true,
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# Get the logger instance (creates default if not set).
|
|
39
|
+
#
|
|
40
|
+
# @return [Logger] The logger instance
|
|
41
|
+
def logger
|
|
42
|
+
@logger ||= create_default_logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set the logger instance.
|
|
46
|
+
#
|
|
47
|
+
# @param logger_instance [Logger] The logger to use
|
|
48
|
+
def logger=(logger_instance)
|
|
49
|
+
@logger = logger_instance
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if debug logging is enabled.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean] true if debug is enabled
|
|
55
|
+
def debug_enabled?
|
|
56
|
+
debug && logger&.debug?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Other configuration attributes
|
|
60
|
+
attr_accessor :debug, :log_level, :default_worker_timeout,
|
|
61
|
+
:default_max_retries, :default_retry_delay,
|
|
62
|
+
:enable_performance_monitoring, :enable_error_reporting,
|
|
63
|
+
:ractor_pool_size, :workflow_validation_strict, :thread_safe
|
|
64
|
+
|
|
65
|
+
# Class-level configuration instance
|
|
66
|
+
@instance = nil
|
|
67
|
+
@mutex = Mutex.new
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# Get the global configuration instance.
|
|
71
|
+
#
|
|
72
|
+
# @return [Configuration] The configuration instance
|
|
73
|
+
def instance
|
|
74
|
+
return @instance if @instance
|
|
75
|
+
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@instance ||= new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Configure Fractor with a block.
|
|
82
|
+
#
|
|
83
|
+
# @yield [Configuration] The configuration object
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# Fractor.configure do |config|
|
|
87
|
+
# config.debug = true
|
|
88
|
+
# config.logger = custom_logger
|
|
89
|
+
# end
|
|
90
|
+
def configure
|
|
91
|
+
yield instance if block_given?
|
|
92
|
+
instance
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Load configuration from a YAML file.
|
|
96
|
+
#
|
|
97
|
+
# @param file_path [String] Path to the YAML configuration file
|
|
98
|
+
# @raise [ArgumentError] if file doesn't exist
|
|
99
|
+
# @raise [ConfigurationError] if YAML is invalid
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# # config/fractor.yml
|
|
103
|
+
# debug: true
|
|
104
|
+
# log_level: DEBUG
|
|
105
|
+
# default_worker_timeout: 60
|
|
106
|
+
#
|
|
107
|
+
# Fractor.configure_from_file("config/fractor.yml")
|
|
108
|
+
def configure_from_file(file_path)
|
|
109
|
+
unless File.exist?(file_path)
|
|
110
|
+
raise ArgumentError, "Configuration file not found: #{file_path}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
config_data = YAML.load_file(file_path)
|
|
114
|
+
apply_config(config_data)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Load configuration from a hash.
|
|
118
|
+
#
|
|
119
|
+
# @param config_hash [Hash] Configuration options
|
|
120
|
+
def apply_config(config_hash)
|
|
121
|
+
return if config_hash.nil? || config_hash.empty?
|
|
122
|
+
|
|
123
|
+
config_hash.each do |key, value|
|
|
124
|
+
setter = "#{key}="
|
|
125
|
+
if instance.respond_to?(setter)
|
|
126
|
+
instance.public_send(setter, value)
|
|
127
|
+
else
|
|
128
|
+
warn "Unknown configuration option: #{key}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Load configuration from environment variables.
|
|
134
|
+
# Environment variables should be prefixed with FRACTOR_.
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# # Set environment variable
|
|
138
|
+
# # export FRACTOR_DEBUG=true
|
|
139
|
+
# # export FRACTOR_DEFAULT_WORKER_TIMEOUT=60
|
|
140
|
+
#
|
|
141
|
+
# Fractor.configure_from_env
|
|
142
|
+
def configure_from_env
|
|
143
|
+
env_config = {}
|
|
144
|
+
|
|
145
|
+
ENV.each do |key, value|
|
|
146
|
+
next unless key.start_with?("FRACTOR_")
|
|
147
|
+
|
|
148
|
+
config_key = key.sub(/^FRACTOR_/, "").downcase
|
|
149
|
+
config_key = underscore_to_camelcase(config_key)
|
|
150
|
+
|
|
151
|
+
# Convert string values to appropriate types
|
|
152
|
+
typed_value = parse_env_value(value)
|
|
153
|
+
env_config[config_key] = typed_value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
apply_config(env_config)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Reset configuration to defaults.
|
|
160
|
+
# Useful for testing.
|
|
161
|
+
def reset!
|
|
162
|
+
@mutex.synchronize do
|
|
163
|
+
@instance = new
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Access configuration properties directly on Fractor.
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# Fractor.config.debug
|
|
171
|
+
# Fractor.config.logger
|
|
172
|
+
def config
|
|
173
|
+
instance
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# Convert FRACTOR_DEFAULT_WORKER_TIMEOUT to default_worker_timeout
|
|
179
|
+
def underscore_to_camelcase(str)
|
|
180
|
+
str.gsub(/_(.)/) { Regexp.last_match(1).upcase }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Parse environment variable value to appropriate type
|
|
184
|
+
def parse_env_value(value)
|
|
185
|
+
case value
|
|
186
|
+
when "true"
|
|
187
|
+
true
|
|
188
|
+
when "false"
|
|
189
|
+
false
|
|
190
|
+
when /^\d+$/
|
|
191
|
+
value.to_i
|
|
192
|
+
when /^\d+\.\d+$/
|
|
193
|
+
value.to_f
|
|
194
|
+
else
|
|
195
|
+
value
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Initialize a new configuration with default values.
|
|
201
|
+
def initialize
|
|
202
|
+
apply_defaults
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Get a configuration value by key.
|
|
206
|
+
#
|
|
207
|
+
# @param key [Symbol] The configuration key
|
|
208
|
+
# @return [Object] The configuration value
|
|
209
|
+
def [](key)
|
|
210
|
+
public_send(key) if respond_to?(key)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Set a configuration value by key.
|
|
214
|
+
#
|
|
215
|
+
# @param key [Symbol] The configuration key
|
|
216
|
+
# @param value [Object] The value to set
|
|
217
|
+
def []=(key, value)
|
|
218
|
+
setter = "#{key}="
|
|
219
|
+
public_send(setter, value) if respond_to?(setter)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Export configuration as hash.
|
|
223
|
+
#
|
|
224
|
+
# @return [Hash] Configuration as hash
|
|
225
|
+
def to_h
|
|
226
|
+
{
|
|
227
|
+
debug: @debug,
|
|
228
|
+
log_level: @log_level,
|
|
229
|
+
default_worker_timeout: @default_worker_timeout,
|
|
230
|
+
default_max_retries: @default_max_retries,
|
|
231
|
+
default_retry_delay: @default_retry_delay,
|
|
232
|
+
enable_performance_monitoring: @enable_performance_monitoring,
|
|
233
|
+
enable_error_reporting: @enable_error_reporting,
|
|
234
|
+
ractor_pool_size: @ractor_pool_size,
|
|
235
|
+
workflow_validation_strict: @workflow_validation_strict,
|
|
236
|
+
thread_safe: @thread_safe,
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Validate configuration.
|
|
241
|
+
#
|
|
242
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
243
|
+
# @return [Boolean] true if valid
|
|
244
|
+
def validate!
|
|
245
|
+
validate_timeouts!
|
|
246
|
+
validate_retries!
|
|
247
|
+
validate_pool_size!
|
|
248
|
+
true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def apply_defaults
|
|
254
|
+
@debug = DEFAULTS[:debug]
|
|
255
|
+
@log_level = DEFAULTS[:log_level]
|
|
256
|
+
@logger = nil # Will use default logger if not set
|
|
257
|
+
@default_worker_timeout = DEFAULTS[:default_worker_timeout]
|
|
258
|
+
@default_max_retries = DEFAULTS[:default_max_retries]
|
|
259
|
+
@default_retry_delay = DEFAULTS[:default_retry_delay]
|
|
260
|
+
@enable_performance_monitoring = DEFAULTS[:enable_performance_monitoring]
|
|
261
|
+
@enable_error_reporting = DEFAULTS[:enable_error_reporting]
|
|
262
|
+
@ractor_pool_size = DEFAULTS[:ractor_pool_size]
|
|
263
|
+
@workflow_validation_strict = DEFAULTS[:workflow_validation_strict]
|
|
264
|
+
@thread_safe = DEFAULTS[:thread_safe]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def validate_timeouts!
|
|
268
|
+
if @default_worker_timeout && @default_worker_timeout <= 0
|
|
269
|
+
raise ConfigurationError,
|
|
270
|
+
"default_worker_timeout must be positive, got: #{@default_worker_timeout}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def validate_retries!
|
|
275
|
+
if @default_max_retries&.negative?
|
|
276
|
+
raise ConfigurationError,
|
|
277
|
+
"default_max_retries must be non-negative, got: #{@default_max_retries}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if @default_retry_delay&.negative?
|
|
281
|
+
raise ConfigurationError,
|
|
282
|
+
"default_retry_delay must be non-negative, got: #{@default_retry_delay}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def validate_pool_size!
|
|
287
|
+
return unless @ractor_pool_size
|
|
288
|
+
|
|
289
|
+
if @ractor_pool_size <= 0
|
|
290
|
+
raise ConfigurationError,
|
|
291
|
+
"ractor_pool_size must be positive, got: #{@ractor_pool_size}"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def create_default_logger
|
|
296
|
+
logger = Logger.new($stdout)
|
|
297
|
+
logger.level = @log_level || Logger::INFO
|
|
298
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
299
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
|
|
300
|
+
end
|
|
301
|
+
logger
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Error raised when configuration is invalid.
|
|
306
|
+
class ConfigurationError < StandardError; end
|
|
307
|
+
end
|