fractor 0.1.6 → 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.
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
data/exe/fractor ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add lib to load path
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+
7
+ require "fractor"
8
+
9
+ # Start the CLI
10
+ Fractor::Cli.start(ARGV)
@@ -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