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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Handles logging for Supervisor operations.
|
|
5
|
+
# Extracted from Supervisor to follow Single Responsibility Principle.
|
|
6
|
+
class SupervisorLogger
|
|
7
|
+
attr_reader :logger, :debug_enabled
|
|
8
|
+
|
|
9
|
+
def initialize(logger: :default, debug: false)
|
|
10
|
+
@logger = if logger == :default
|
|
11
|
+
Fractor.logger
|
|
12
|
+
else
|
|
13
|
+
logger
|
|
14
|
+
end
|
|
15
|
+
@debug_enabled = debug
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Log debug message (only when debug mode is enabled)
|
|
19
|
+
def debug(message)
|
|
20
|
+
return unless @debug_enabled
|
|
21
|
+
|
|
22
|
+
if @logger
|
|
23
|
+
@logger.debug("[Fractor] #{message}")
|
|
24
|
+
else
|
|
25
|
+
puts "[DEBUG] #{message}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Log info message
|
|
30
|
+
def info(message)
|
|
31
|
+
if @logger
|
|
32
|
+
@logger.info("[Fractor] #{message}")
|
|
33
|
+
else
|
|
34
|
+
puts "[INFO] #{message}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Log warning message
|
|
39
|
+
def warn(message)
|
|
40
|
+
if @logger
|
|
41
|
+
@logger.warn("[Fractor] #{message}")
|
|
42
|
+
else
|
|
43
|
+
Kernel.warn "[WARN] #{message}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Log error message
|
|
48
|
+
def error(message)
|
|
49
|
+
if @logger
|
|
50
|
+
@logger.error("[Fractor] #{message}")
|
|
51
|
+
else
|
|
52
|
+
Kernel.warn "[ERROR] #{message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Log work item status
|
|
57
|
+
def log_work_added(work, total_count, queue_size)
|
|
58
|
+
debug "Work item added: #{work.inspect}"
|
|
59
|
+
debug "Initial work count: #{total_count}, Queue size: #{queue_size}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Log worker status
|
|
63
|
+
def log_worker_status(total:, idle:, busy:)
|
|
64
|
+
debug "Workers: #{total} total, #{idle} idle, #{busy} busy"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Log processing status
|
|
68
|
+
def log_processing_status(processed:, total:, queue_size:)
|
|
69
|
+
debug "Processing: #{processed}/#{total}, Queue size: #{queue_size}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Log result received
|
|
73
|
+
def log_result_received(result)
|
|
74
|
+
debug "Result received: #{result.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Log error received
|
|
78
|
+
def log_error_received(error_result)
|
|
79
|
+
error "Error in worker: #{error_result.error}"
|
|
80
|
+
error "Work item: #{error_result.work.inspect}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Enable or disable debug mode
|
|
84
|
+
def debug=(enabled)
|
|
85
|
+
@debug_enabled = enabled
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/fractor/version.rb
CHANGED
data/lib/fractor/work.rb
CHANGED
|
@@ -13,5 +13,17 @@ module Fractor
|
|
|
13
13
|
def to_s
|
|
14
14
|
"Work: #{@input}"
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
# Provide detailed inspection of work item for debugging
|
|
18
|
+
# @return [String] Detailed inspection string
|
|
19
|
+
def inspect
|
|
20
|
+
details = [
|
|
21
|
+
"#<#{self.class.name}",
|
|
22
|
+
"0x#{(object_id << 1).to_s(16)}",
|
|
23
|
+
"@input=#{@input.inspect}",
|
|
24
|
+
"@type=#{input.class.name}",
|
|
25
|
+
]
|
|
26
|
+
details.join(" ")
|
|
27
|
+
end
|
|
16
28
|
end
|
|
17
29
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Manages work distribution to workers in a Supervisor.
|
|
5
|
+
# Responsible for tracking idle workers and assigning work from the queue.
|
|
6
|
+
#
|
|
7
|
+
# This class extracts work distribution logic from Supervisor to follow
|
|
8
|
+
# the Single Responsibility Principle.
|
|
9
|
+
class WorkDistributionManager
|
|
10
|
+
attr_reader :idle_workers
|
|
11
|
+
|
|
12
|
+
def initialize(work_queue, workers, ractors_map, debug: false,
|
|
13
|
+
continuous_mode: false, performance_monitor: nil)
|
|
14
|
+
@work_queue = work_queue
|
|
15
|
+
@workers = workers
|
|
16
|
+
@ractors_map = ractors_map
|
|
17
|
+
@debug = debug
|
|
18
|
+
@continuous_mode = continuous_mode
|
|
19
|
+
@performance_monitor = performance_monitor
|
|
20
|
+
@idle_workers = []
|
|
21
|
+
@work_start_times = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Update the workers reference after workers are created
|
|
25
|
+
# This is needed when @workers is reassigned in Supervisor.start_workers
|
|
26
|
+
#
|
|
27
|
+
# @param workers [Array<WrappedRactor>] The new workers array
|
|
28
|
+
def update_workers(workers)
|
|
29
|
+
@workers = workers
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Assign work to a specific worker if work is available.
|
|
33
|
+
#
|
|
34
|
+
# @param wrapped_ractor [WrappedRactor] The worker to assign work to
|
|
35
|
+
# @return [Boolean] true if work was sent, false otherwise
|
|
36
|
+
def assign_work_to_worker(wrapped_ractor)
|
|
37
|
+
# Ensure the wrapped_ractor instance is valid and its underlying ractor is not closed
|
|
38
|
+
if wrapped_ractor && !wrapped_ractor.closed?
|
|
39
|
+
if @work_queue.empty?
|
|
40
|
+
puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}." if @debug
|
|
41
|
+
false
|
|
42
|
+
else
|
|
43
|
+
work_item = @work_queue.pop # Now directly a Work object
|
|
44
|
+
|
|
45
|
+
# Track start time for performance monitoring
|
|
46
|
+
if @performance_monitor
|
|
47
|
+
@work_start_times[work_item.object_id] = Time.now
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}." if @debug
|
|
51
|
+
wrapped_ractor.send(work_item) # Send the Work object
|
|
52
|
+
puts "Work sent to #{wrapped_ractor.name}." if @debug
|
|
53
|
+
|
|
54
|
+
# Remove from idle workers list since it's now busy
|
|
55
|
+
@idle_workers.delete(wrapped_ractor)
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || 'unknown'}." if @debug
|
|
60
|
+
# Remove from map if found but closed
|
|
61
|
+
if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
|
|
62
|
+
@ractors_map.delete(wrapped_ractor.ractor)
|
|
63
|
+
end
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Mark a worker as idle (available for work).
|
|
69
|
+
#
|
|
70
|
+
# @param wrapped_ractor [WrappedRactor] The worker to mark as idle
|
|
71
|
+
def mark_worker_idle(wrapped_ractor)
|
|
72
|
+
@idle_workers << wrapped_ractor unless @idle_workers.include?(wrapped_ractor)
|
|
73
|
+
puts "Worker #{wrapped_ractor.name} marked as idle." if @debug
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Mark a worker as busy (not available for work).
|
|
77
|
+
#
|
|
78
|
+
# @param wrapped_ractor [WrappedRactor] The worker to mark as busy
|
|
79
|
+
def mark_worker_busy(wrapped_ractor)
|
|
80
|
+
@idle_workers.delete(wrapped_ractor)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Distribute available work to idle workers.
|
|
84
|
+
# Useful when new work is added to the queue.
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer] Number of workers that received work
|
|
87
|
+
def distribute_to_idle_workers
|
|
88
|
+
distributed = 0
|
|
89
|
+
while !@work_queue.empty? && !@idle_workers.empty?
|
|
90
|
+
worker = @idle_workers.shift
|
|
91
|
+
if assign_work_to_worker(worker)
|
|
92
|
+
puts "Sent work to idle worker #{worker.name}" if @debug
|
|
93
|
+
distributed += 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
distributed
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the list of idle (available) workers.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<WrappedRactor>] List of idle workers
|
|
102
|
+
def idle_workers_list
|
|
103
|
+
@idle_workers.dup
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get the list of busy (processing) workers.
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<WrappedRactor>] List of busy workers
|
|
109
|
+
def busy_workers_list
|
|
110
|
+
@workers.reject { |w| @idle_workers.include?(w) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get the count of idle workers.
|
|
114
|
+
#
|
|
115
|
+
# @return [Integer] Number of idle workers
|
|
116
|
+
def idle_count
|
|
117
|
+
@idle_workers.size
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get the count of busy workers.
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer] Number of busy workers
|
|
123
|
+
def busy_count
|
|
124
|
+
@workers.size - @idle_workers.size
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the work start time for a specific work item.
|
|
128
|
+
#
|
|
129
|
+
# @param work_object_id [Integer] The object_id of the work item
|
|
130
|
+
# @return [Time, nil] The start time, or nil if not found
|
|
131
|
+
def get_work_start_time(work_object_id)
|
|
132
|
+
@work_start_times.delete(work_object_id)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Clear all tracked work start times.
|
|
136
|
+
# Useful for cleanup or testing.
|
|
137
|
+
def clear_work_start_times
|
|
138
|
+
@work_start_times.clear
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get current worker status summary.
|
|
142
|
+
#
|
|
143
|
+
# @return [Hash] Worker status summary with :idle and :busy counts
|
|
144
|
+
def status_summary
|
|
145
|
+
{
|
|
146
|
+
idle: @idle_workers.size,
|
|
147
|
+
busy: @workers.size - @idle_workers.size,
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/fractor/work_queue.rb
CHANGED
|
@@ -16,6 +16,13 @@ module Fractor
|
|
|
16
16
|
# @param work_item [Fractor::Work] The work item to add
|
|
17
17
|
# @return [void]
|
|
18
18
|
def <<(work_item)
|
|
19
|
+
enqueue(work_item)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a work item to the queue (standardized API)
|
|
23
|
+
# @param work_item [Fractor::Work] The work item to add
|
|
24
|
+
# @return [void]
|
|
25
|
+
def enqueue(work_item)
|
|
19
26
|
unless work_item.is_a?(Fractor::Work)
|
|
20
27
|
raise ArgumentError,
|
|
21
28
|
"#{work_item.class} must be an instance of Fractor::Work"
|
|
@@ -28,6 +35,13 @@ module Fractor
|
|
|
28
35
|
# @param max_items [Integer] Maximum number of items to retrieve
|
|
29
36
|
# @return [Array<Fractor::Work>] Array of work items (may be empty)
|
|
30
37
|
def pop_batch(max_items = 10)
|
|
38
|
+
dequeue_batch(max_items)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Retrieve multiple work items from the queue in a single operation (standardized API)
|
|
42
|
+
# @param max_items [Integer] Maximum number of items to retrieve
|
|
43
|
+
# @return [Array<Fractor::Work>] Array of work items (may be empty)
|
|
44
|
+
def dequeue_batch(max_items = 10)
|
|
31
45
|
items = []
|
|
32
46
|
max_items.times do
|
|
33
47
|
break if @queue.empty?
|
|
@@ -42,6 +56,12 @@ module Fractor
|
|
|
42
56
|
items
|
|
43
57
|
end
|
|
44
58
|
|
|
59
|
+
# Retrieve a single work item from the queue (blocking if empty)
|
|
60
|
+
# @return [Fractor::Work, nil] A work item or nil if queue is closed
|
|
61
|
+
def dequeue
|
|
62
|
+
@queue.pop
|
|
63
|
+
end
|
|
64
|
+
|
|
45
65
|
# Check if the queue is empty
|
|
46
66
|
# @return [Boolean] true if the queue is empty
|
|
47
67
|
def empty?
|
data/lib/fractor/work_result.rb
CHANGED
|
@@ -2,34 +2,206 @@
|
|
|
2
2
|
|
|
3
3
|
module Fractor
|
|
4
4
|
# Represents the result of processing a Work item.
|
|
5
|
-
# Can hold either a successful result or an error.
|
|
5
|
+
# Can hold either a successful result or an error with rich metadata.
|
|
6
6
|
class WorkResult
|
|
7
|
-
|
|
7
|
+
# Error severity levels
|
|
8
|
+
SEVERITY_CRITICAL = :critical # System-breaking errors
|
|
9
|
+
SEVERITY_ERROR = :error # Standard errors
|
|
10
|
+
SEVERITY_WARNING = :warning # Non-fatal issues
|
|
11
|
+
SEVERITY_INFO = :info # Informational
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
# Error categories
|
|
14
|
+
CATEGORY_VALIDATION = :validation # Input validation errors
|
|
15
|
+
CATEGORY_TIMEOUT = :timeout # Timeout errors
|
|
16
|
+
CATEGORY_NETWORK = :network # Network-related errors
|
|
17
|
+
CATEGORY_RESOURCE = :resource # Resource exhaustion
|
|
18
|
+
CATEGORY_BUSINESS = :business # Business logic errors
|
|
19
|
+
CATEGORY_SYSTEM = :system # System errors
|
|
20
|
+
CATEGORY_UNKNOWN = :unknown # Unknown/uncategorized
|
|
21
|
+
|
|
22
|
+
attr_reader :result, :error, :work, :error_code, :error_context,
|
|
23
|
+
:error_category, :error_severity, :suggestion, :stack_trace
|
|
24
|
+
|
|
25
|
+
def initialize(
|
|
26
|
+
result: nil,
|
|
27
|
+
error: nil,
|
|
28
|
+
work: nil,
|
|
29
|
+
error_code: nil,
|
|
30
|
+
error_context: nil,
|
|
31
|
+
error_category: nil,
|
|
32
|
+
error_severity: nil,
|
|
33
|
+
suggestion: nil,
|
|
34
|
+
stack_trace: nil
|
|
35
|
+
)
|
|
10
36
|
@result = result
|
|
11
37
|
@error = error
|
|
12
38
|
@work = work
|
|
39
|
+
@error_code = error_code
|
|
40
|
+
@error_context = error_context || {}
|
|
41
|
+
@error_category = error_category || infer_category(error)
|
|
42
|
+
@error_severity = error_severity || infer_severity(error)
|
|
43
|
+
@suggestion = suggestion || infer_suggestion(error)
|
|
44
|
+
@stack_trace = stack_trace || capture_stack_trace(error)
|
|
13
45
|
end
|
|
14
46
|
|
|
15
47
|
def success?
|
|
16
48
|
!@error
|
|
17
49
|
end
|
|
18
50
|
|
|
51
|
+
def failure?
|
|
52
|
+
!success?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if error is critical
|
|
56
|
+
def critical?
|
|
57
|
+
@error_severity == SEVERITY_CRITICAL
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if error is retriable based on category
|
|
61
|
+
def retriable?
|
|
62
|
+
return false if success?
|
|
63
|
+
|
|
64
|
+
retriable_categories = [
|
|
65
|
+
CATEGORY_TIMEOUT,
|
|
66
|
+
CATEGORY_NETWORK,
|
|
67
|
+
CATEGORY_RESOURCE,
|
|
68
|
+
]
|
|
69
|
+
retriable_categories.include?(@error_category)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get full error information as hash
|
|
73
|
+
def error_info
|
|
74
|
+
return nil if success?
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
error: @error,
|
|
78
|
+
error_class: @error&.class&.name,
|
|
79
|
+
error_message: @error&.message,
|
|
80
|
+
error_code: @error_code,
|
|
81
|
+
error_category: @error_category,
|
|
82
|
+
error_severity: @error_severity,
|
|
83
|
+
error_context: @error_context,
|
|
84
|
+
suggestion: @suggestion,
|
|
85
|
+
stack_trace: @stack_trace,
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
19
89
|
def to_s
|
|
20
90
|
if success?
|
|
21
91
|
"Result: #{@result}"
|
|
22
92
|
else
|
|
23
|
-
"Error: #{@error},
|
|
93
|
+
"Error: #{@error}, Code: #{@error_code}, Category: #{@error_category}, Severity: #{@error_severity}"
|
|
24
94
|
end
|
|
25
95
|
end
|
|
26
96
|
|
|
27
97
|
def inspect
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
98
|
+
if success?
|
|
99
|
+
{
|
|
100
|
+
result: @result,
|
|
101
|
+
work: @work&.to_s,
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
{
|
|
105
|
+
error: @error,
|
|
106
|
+
error_code: @error_code,
|
|
107
|
+
error_category: @error_category,
|
|
108
|
+
error_severity: @error_severity,
|
|
109
|
+
error_context: @error_context,
|
|
110
|
+
work: @work&.to_s,
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Infer error category from error type
|
|
118
|
+
def infer_category(error)
|
|
119
|
+
return nil unless error
|
|
120
|
+
|
|
121
|
+
case error
|
|
122
|
+
when ArgumentError, TypeError
|
|
123
|
+
CATEGORY_VALIDATION
|
|
124
|
+
when Timeout::Error
|
|
125
|
+
CATEGORY_TIMEOUT
|
|
126
|
+
when String
|
|
127
|
+
# Categorize string error messages by content
|
|
128
|
+
error_msg = error.downcase
|
|
129
|
+
if error_msg.include?("timeout") || error_msg.include?("timed out")
|
|
130
|
+
CATEGORY_TIMEOUT
|
|
131
|
+
elsif error_msg.include?("connection") || error_msg.include?("network") ||
|
|
132
|
+
error_msg.include?("socket") || error_msg.include?("refused")
|
|
133
|
+
CATEGORY_NETWORK
|
|
134
|
+
elsif error_msg.include?("memory") || error_msg.include?("space")
|
|
135
|
+
CATEGORY_RESOURCE
|
|
136
|
+
else
|
|
137
|
+
CATEGORY_UNKNOWN
|
|
138
|
+
end
|
|
139
|
+
when defined?(SocketError) ? SocketError : nil, Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
140
|
+
CATEGORY_NETWORK
|
|
141
|
+
when Errno::ENOMEM, Errno::ENOSPC
|
|
142
|
+
CATEGORY_RESOURCE
|
|
143
|
+
when SystemCallError, SystemStackError
|
|
144
|
+
CATEGORY_SYSTEM
|
|
145
|
+
else
|
|
146
|
+
CATEGORY_UNKNOWN
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Infer error severity from error type
|
|
151
|
+
def infer_severity(error)
|
|
152
|
+
return nil unless error
|
|
153
|
+
|
|
154
|
+
case error
|
|
155
|
+
when SystemStackError, Errno::ENOMEM
|
|
156
|
+
SEVERITY_CRITICAL
|
|
157
|
+
when StandardError
|
|
158
|
+
SEVERITY_ERROR
|
|
159
|
+
else
|
|
160
|
+
SEVERITY_WARNING
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Infer suggestion from error type
|
|
165
|
+
def infer_suggestion(error)
|
|
166
|
+
return nil unless error
|
|
167
|
+
|
|
168
|
+
error_msg = error.to_s.downcase
|
|
169
|
+
|
|
170
|
+
case error_msg
|
|
171
|
+
when /negative number/i, /must be positive/i
|
|
172
|
+
"Ensure input values are positive. Consider using absolute value or validating input range."
|
|
173
|
+
when /timeout/i
|
|
174
|
+
"Consider increasing timeout duration or breaking work into smaller chunks."
|
|
175
|
+
when /memory/i, /out of memory/i
|
|
176
|
+
"Try processing smaller batches or increasing available memory."
|
|
177
|
+
when /connection/i, /network/i, /refused/i
|
|
178
|
+
"Verify network connectivity and service availability. Check firewall settings."
|
|
179
|
+
when /undefined method/i, /no method/i
|
|
180
|
+
"Ensure the Worker class implements all required methods for the Work type."
|
|
181
|
+
when /nil/i, /null/i
|
|
182
|
+
"Check if work items are being initialized with valid input data."
|
|
183
|
+
when /argument/i, /type/i
|
|
184
|
+
"Verify input data types match expected format. Check Work item initialization."
|
|
185
|
+
when /file/i, /not found/i
|
|
186
|
+
"Ensure file paths are correct and files exist before processing."
|
|
187
|
+
when /permission/i, /authorized/i
|
|
188
|
+
"Check file permissions and ensure proper access rights for the operation."
|
|
189
|
+
else
|
|
190
|
+
"Check the error message and ensure all requirements are met. Enable debug logging for more details."
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Capture stack trace from error if available
|
|
195
|
+
def capture_stack_trace(error)
|
|
196
|
+
return nil unless error
|
|
197
|
+
|
|
198
|
+
# If error is a string, return nil
|
|
199
|
+
return nil unless error.is_a?(Exception)
|
|
200
|
+
|
|
201
|
+
# Get backtrace from exception
|
|
202
|
+
error.backtrace
|
|
203
|
+
rescue StandardError
|
|
204
|
+
nil
|
|
33
205
|
end
|
|
34
206
|
end
|
|
35
207
|
end
|
data/lib/fractor/worker.rb
CHANGED
|
@@ -1,17 +1,90 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
3
5
|
module Fractor
|
|
4
6
|
# Base class for defining work processors.
|
|
5
7
|
# Subclasses must implement the `process` method.
|
|
6
8
|
class Worker
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :input_type_class, :output_type_class, :worker_timeout
|
|
11
|
+
|
|
12
|
+
# Declare the input type for this worker.
|
|
13
|
+
# Used by the workflow system to validate data flow.
|
|
14
|
+
#
|
|
15
|
+
# @param klass [Class] A Lutaml::Model::Serializable subclass
|
|
16
|
+
def input_type(klass)
|
|
17
|
+
validate_type_class!(klass, "input_type")
|
|
18
|
+
@input_type_class = klass
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Declare the output type for this worker.
|
|
22
|
+
# Used by the workflow system to validate data flow.
|
|
23
|
+
#
|
|
24
|
+
# @param klass [Class] A Lutaml::Model::Serializable subclass
|
|
25
|
+
def output_type(klass)
|
|
26
|
+
validate_type_class!(klass, "output_type")
|
|
27
|
+
@output_type_class = klass
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set a timeout for this worker's process method.
|
|
31
|
+
# If the process method takes longer than this, a Timeout::Error will be raised.
|
|
32
|
+
#
|
|
33
|
+
# @param seconds [Numeric] Timeout in seconds
|
|
34
|
+
def timeout(seconds)
|
|
35
|
+
@worker_timeout = seconds
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the effective timeout for this worker.
|
|
39
|
+
# Returns the worker-specific timeout, or the global default if not set.
|
|
40
|
+
# Note: This method accesses Fractor.config and should be called from
|
|
41
|
+
# the main ractor context, not from within worker ractors.
|
|
42
|
+
#
|
|
43
|
+
# @return [Numeric, nil] Timeout in seconds, or nil if not configured
|
|
44
|
+
def effective_timeout
|
|
45
|
+
@worker_timeout || begin
|
|
46
|
+
# Access config safely - this must be called from main ractor
|
|
47
|
+
Fractor.config.default_worker_timeout
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_type_class!(klass, method_name)
|
|
54
|
+
# Allow any class for now, stricter validation can be added later
|
|
55
|
+
# In production, you'd want to check for Lutaml::Model::Serializable
|
|
56
|
+
return if klass.is_a?(Class)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "#{method_name} must be a Class"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
7
62
|
def initialize(name: nil, **options)
|
|
8
63
|
@name = name
|
|
9
64
|
@options = options
|
|
65
|
+
# If timeout is not provided or is nil, fall back to class-level timeout
|
|
66
|
+
# Note: This must only be called from the main ractor, not from within worker ractors.
|
|
67
|
+
# In the ractor context, timeout should always be passed explicitly.
|
|
68
|
+
if !@options.key?(:timeout) || @options[:timeout].nil?
|
|
69
|
+
@options[:timeout] = self.class.worker_timeout
|
|
70
|
+
end
|
|
10
71
|
end
|
|
11
72
|
|
|
12
73
|
def process(work)
|
|
13
74
|
raise NotImplementedError,
|
|
14
75
|
"Subclasses must implement the 'process' method."
|
|
15
76
|
end
|
|
77
|
+
|
|
78
|
+
# Get the timeout for this worker instance.
|
|
79
|
+
# Uses the class-level timeout if not overridden.
|
|
80
|
+
# Note: This method is safe to call from within ractors as it only
|
|
81
|
+
# accesses instance variables that were set at initialization time.
|
|
82
|
+
#
|
|
83
|
+
# @return [Numeric, nil] Timeout in seconds, or nil if not configured
|
|
84
|
+
def timeout
|
|
85
|
+
# The timeout is always set at initialization time via options,
|
|
86
|
+
# so we can safely access it from within the ractor
|
|
87
|
+
@options[:timeout]
|
|
88
|
+
end
|
|
16
89
|
end
|
|
17
90
|
end
|