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/lib/fractor/supervisor.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "etc"
|
|
4
4
|
require "timeout"
|
|
5
|
+
require_relative "signal_handler"
|
|
6
|
+
require_relative "error_formatter"
|
|
5
7
|
|
|
6
8
|
module Fractor
|
|
7
9
|
# Custom exception for shutdown signal handling
|
|
@@ -9,21 +11,60 @@ module Fractor
|
|
|
9
11
|
|
|
10
12
|
# Supervises multiple WrappedRactors, distributes work, and aggregates results.
|
|
11
13
|
class Supervisor
|
|
12
|
-
attr_reader :work_queue, :workers, :results, :worker_pools
|
|
14
|
+
attr_reader :work_queue, :workers, :results, :worker_pools, :debug,
|
|
15
|
+
:error_reporter, :logger, :performance_monitor
|
|
13
16
|
|
|
14
17
|
# Initializes the Supervisor.
|
|
15
18
|
# - worker_pools: An array of worker pool configurations, each containing:
|
|
16
19
|
# - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
|
|
17
20
|
# - num_workers: The number of Ractors to spawn for this worker class.
|
|
18
21
|
# - continuous_mode: Whether to run in continuous mode without expecting a fixed work count.
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
# - debug: Enable verbose debugging output for all state changes.
|
|
23
|
+
# - logger: Optional logger instance for this Supervisor (defaults to Fractor.logger).
|
|
24
|
+
# Provides isolation when multiple gems use Fractor in the same process.
|
|
25
|
+
# - tracer_enabled: Optional override for ExecutionTracer (nil uses global setting).
|
|
26
|
+
# - tracer_stream: Optional trace stream for this Supervisor (nil uses global setting).
|
|
27
|
+
# - enable_performance_monitoring: Enable performance monitoring (latency, throughput, etc.).
|
|
28
|
+
def initialize(worker_pools: [], continuous_mode: false, debug: false, logger: nil,
|
|
29
|
+
tracer_enabled: nil, tracer_stream: nil, enable_performance_monitoring: false)
|
|
30
|
+
@debug = debug || ENV["FRACTOR_DEBUG"] == "1"
|
|
31
|
+
@logger = logger # Store instance-specific logger for isolation
|
|
32
|
+
@tracer_enabled = tracer_enabled
|
|
33
|
+
@tracer_stream = tracer_stream
|
|
34
|
+
@worker_pools = worker_pools.map.with_index do |pool_config, index|
|
|
21
35
|
worker_class = pool_config[:worker_class]
|
|
22
36
|
num_workers = pool_config[:num_workers] || detect_num_workers
|
|
23
37
|
|
|
38
|
+
# Validate worker_class
|
|
39
|
+
unless worker_class.is_a?(Class)
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"worker_class must be a Class (got #{worker_class.class}), in worker_pools[#{index}]\n\n" \
|
|
42
|
+
"Expected: { worker_class: MyWorker }\n" \
|
|
43
|
+
"Got: { worker_class: #{worker_class.inspect} }\n\n" \
|
|
44
|
+
"Fix: Use the class itself, not a symbol or string.\n" \
|
|
45
|
+
"Example: { worker_class: MyWorker } # Correct\n" \
|
|
46
|
+
" { worker_class: 'MyWorker' } # Wrong - this is a string"
|
|
47
|
+
end
|
|
48
|
+
|
|
24
49
|
unless worker_class < Fractor::Worker
|
|
25
50
|
raise ArgumentError,
|
|
26
|
-
"#{worker_class} must inherit from Fractor::Worker"
|
|
51
|
+
"#{worker_class} must inherit from Fractor::Worker, in worker_pools[#{index}]\n\n" \
|
|
52
|
+
"Your worker class must be defined as:\n" \
|
|
53
|
+
" class #{worker_class} < Fractor::Worker\n" \
|
|
54
|
+
" def process(work)\n" \
|
|
55
|
+
" # ...\n" \
|
|
56
|
+
" end\n" \
|
|
57
|
+
" end\n\n" \
|
|
58
|
+
"Did you forget to inherit from Fractor::Worker?"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate num_workers
|
|
62
|
+
unless num_workers.is_a?(Integer) && num_workers.positive?
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
"num_workers must be a positive integer (got #{num_workers.inspect}), in worker_pools[#{index}]\n\n" \
|
|
65
|
+
"Valid values: Integer >= 1\n" \
|
|
66
|
+
"Examples: { num_workers: 4 } # Use 4 workers\n" \
|
|
67
|
+
" { num_workers: Etc.nprocessors } # Use available CPUs"
|
|
27
68
|
end
|
|
28
69
|
|
|
29
70
|
{
|
|
@@ -43,7 +84,46 @@ module Fractor
|
|
|
43
84
|
@work_callbacks = []
|
|
44
85
|
@wakeup_ractor = nil # Control ractor for unblocking select
|
|
45
86
|
@timer_thread = nil # Timer thread for periodic wakeup
|
|
46
|
-
@
|
|
87
|
+
@error_reporter = ErrorReporter.new # Track errors and statistics
|
|
88
|
+
@error_callbacks = [] # Custom error callbacks
|
|
89
|
+
@performance_monitor = nil # Performance monitor instance
|
|
90
|
+
|
|
91
|
+
# Initialize performance monitor if enabled
|
|
92
|
+
if enable_performance_monitoring
|
|
93
|
+
require_relative "performance_monitor"
|
|
94
|
+
@performance_monitor = PerformanceMonitor.new(self)
|
|
95
|
+
@performance_monitor.start
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Initialize work distribution manager (handles idle workers and work assignment)
|
|
99
|
+
@work_distribution_manager = WorkDistributionManager.new(
|
|
100
|
+
@work_queue,
|
|
101
|
+
@workers,
|
|
102
|
+
@ractors_map,
|
|
103
|
+
debug: @debug,
|
|
104
|
+
continuous_mode: @continuous_mode,
|
|
105
|
+
performance_monitor: @performance_monitor,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Initialize shutdown handler (manages graceful shutdown)
|
|
109
|
+
@shutdown_handler = ShutdownHandler.new(
|
|
110
|
+
@workers,
|
|
111
|
+
@wakeup_ractor,
|
|
112
|
+
@timer_thread,
|
|
113
|
+
@performance_monitor,
|
|
114
|
+
debug: @debug,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Initialize signal handler for graceful shutdown
|
|
118
|
+
@signal_handler = SignalHandler.new(
|
|
119
|
+
continuous_mode: @continuous_mode,
|
|
120
|
+
debug: @debug,
|
|
121
|
+
status_callback: -> { print_status },
|
|
122
|
+
shutdown_callback: ->(mode) { handle_shutdown_callback(mode) },
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Initialize error formatter for error messages
|
|
126
|
+
@error_formatter = ErrorFormatter.new
|
|
47
127
|
end
|
|
48
128
|
|
|
49
129
|
# Adds a single work item to the queue.
|
|
@@ -51,12 +131,32 @@ module Fractor
|
|
|
51
131
|
def add_work_item(work)
|
|
52
132
|
unless work.is_a?(Fractor::Work)
|
|
53
133
|
raise ArgumentError,
|
|
54
|
-
"#{work.class} must be an instance of Fractor::Work"
|
|
134
|
+
"#{work.class} must be an instance of Fractor::Work.\n\n" \
|
|
135
|
+
"Received: #{work.inspect}\n\n" \
|
|
136
|
+
"To create a valid work item:\n" \
|
|
137
|
+
" class MyWork < Fractor::Work\n" \
|
|
138
|
+
" def initialize(data)\n" \
|
|
139
|
+
" super({ value: data })\n" \
|
|
140
|
+
" end\n" \
|
|
141
|
+
" end\n\n" \
|
|
142
|
+
" work = MyWork.new(42)\n" \
|
|
143
|
+
" supervisor.add_work_item(work)"
|
|
55
144
|
end
|
|
56
145
|
|
|
57
146
|
@work_queue << work
|
|
58
147
|
@total_work_count += 1
|
|
59
|
-
|
|
148
|
+
|
|
149
|
+
# Trace work item queued
|
|
150
|
+
trace_work(:queued, work, queue_size: @work_queue.size)
|
|
151
|
+
|
|
152
|
+
# Distribute to idle workers if work_distribution_manager is available
|
|
153
|
+
# This ensures work added from callbacks gets picked up immediately
|
|
154
|
+
if @work_distribution_manager && @running
|
|
155
|
+
distributed = @work_distribution_manager.distribute_to_idle_workers
|
|
156
|
+
puts "Distributed work to #{distributed} idle workers after add_work_item" if @debug && distributed.positive?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return unless @debug
|
|
60
160
|
|
|
61
161
|
puts "Work item added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
|
|
62
162
|
end
|
|
@@ -75,20 +175,51 @@ module Fractor
|
|
|
75
175
|
@work_callbacks << callback
|
|
76
176
|
end
|
|
77
177
|
|
|
178
|
+
# Register a callback to handle errors
|
|
179
|
+
# The callback receives (error_result, worker_name, worker_class)
|
|
180
|
+
# Example: supervisor.on_error { |err, worker, klass| puts "Error in #{klass}: #{err.error}" }
|
|
181
|
+
def on_error(&callback)
|
|
182
|
+
@error_callbacks << callback
|
|
183
|
+
end
|
|
184
|
+
|
|
78
185
|
# Starts the worker Ractors for all worker pools.
|
|
79
186
|
def start_workers
|
|
187
|
+
# Capture debug flag for Ractor isolation (Ruby 4.0)
|
|
188
|
+
# Pass as parameter to avoid isolation error
|
|
189
|
+
debug_mode = @debug
|
|
190
|
+
|
|
191
|
+
# Check if running on Ruby 4.0
|
|
192
|
+
ruby_4_0 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
|
|
193
|
+
|
|
80
194
|
# Create a wakeup Ractor for unblocking Ractor.select
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
puts "Wakeup Ractor
|
|
86
|
-
|
|
87
|
-
Ractor.
|
|
88
|
-
|
|
195
|
+
if ruby_4_0
|
|
196
|
+
# In Ruby 4.0, wakeup uses ports too
|
|
197
|
+
@wakeup_port = Ractor::Port.new
|
|
198
|
+
@wakeup_ractor = Ractor.new(@wakeup_port, debug_mode) do |port, debug|
|
|
199
|
+
puts "Wakeup Ractor started" if debug
|
|
200
|
+
loop do
|
|
201
|
+
msg = Ractor.receive
|
|
202
|
+
puts "Wakeup Ractor received: #{msg.inspect}" if debug
|
|
203
|
+
if %i[wakeup shutdown].include?(msg)
|
|
204
|
+
port << { type: :wakeup, message: msg }
|
|
205
|
+
break if msg == :shutdown
|
|
206
|
+
end
|
|
89
207
|
end
|
|
208
|
+
puts "Wakeup Ractor shutting down" if debug
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
@wakeup_ractor = Ractor.new(debug_mode) do |debug|
|
|
212
|
+
puts "Wakeup Ractor started" if debug
|
|
213
|
+
loop do
|
|
214
|
+
msg = Ractor.receive
|
|
215
|
+
puts "Wakeup Ractor received: #{msg.inspect}" if debug
|
|
216
|
+
if %i[wakeup shutdown].include?(msg)
|
|
217
|
+
Ractor.yield({ type: :wakeup, message: msg })
|
|
218
|
+
break if msg == :shutdown
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
puts "Wakeup Ractor shutting down" if debug
|
|
90
222
|
end
|
|
91
|
-
puts "Wakeup Ractor shutting down" if ENV["FRACTOR_DEBUG"]
|
|
92
223
|
end
|
|
93
224
|
|
|
94
225
|
# Add wakeup ractor to the map with a special marker
|
|
@@ -99,7 +230,17 @@ module Fractor
|
|
|
99
230
|
num_workers = pool[:num_workers]
|
|
100
231
|
|
|
101
232
|
pool[:workers] = (1..num_workers).map do |i|
|
|
102
|
-
|
|
233
|
+
# In Ruby 4.0, create a response port for each worker
|
|
234
|
+
response_port = if ruby_4_0
|
|
235
|
+
Ractor::Port.new
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Use the factory method to create the appropriate implementation
|
|
239
|
+
wrapped_ractor = WrappedRactor.create(
|
|
240
|
+
"worker #{worker_class}:#{i}",
|
|
241
|
+
worker_class,
|
|
242
|
+
response_port: response_port,
|
|
243
|
+
)
|
|
103
244
|
wrapped_ractor.start # Start the underlying Ractor
|
|
104
245
|
# Map the actual Ractor object to the WrappedRactor instance
|
|
105
246
|
@ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
|
|
@@ -110,332 +251,304 @@ module Fractor
|
|
|
110
251
|
# Flatten all workers for easier access
|
|
111
252
|
@workers = @worker_pools.flat_map { |pool| pool[:workers] }
|
|
112
253
|
@ractors_map.compact! # Ensure map doesn't contain nil keys/values
|
|
113
|
-
|
|
254
|
+
|
|
255
|
+
# Update work distribution manager's workers reference
|
|
256
|
+
# This is critical because @workers was reassigned, and WorkDistributionManager
|
|
257
|
+
# needs the updated reference to properly track idle workers
|
|
258
|
+
@work_distribution_manager.update_workers(@workers)
|
|
259
|
+
|
|
260
|
+
# Mark all workers as idle initially so they can receive work
|
|
261
|
+
# This is critical for Ruby 4.0 where workers don't send :initialize messages
|
|
262
|
+
@workers.each do |worker|
|
|
263
|
+
@work_distribution_manager.mark_worker_idle(worker)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
return unless @debug
|
|
114
267
|
|
|
115
268
|
puts "Workers started: #{@workers.size} active across #{@worker_pools.size} pools."
|
|
269
|
+
puts "All workers marked as idle and ready for work."
|
|
116
270
|
end
|
|
117
271
|
|
|
118
272
|
# Sets up signal handlers for graceful shutdown.
|
|
119
|
-
#
|
|
273
|
+
# Uses SignalHandler to manage signal handling logic.
|
|
120
274
|
def setup_signal_handler
|
|
121
|
-
|
|
122
|
-
Signal.trap("INT") { handle_shutdown("SIGINT") }
|
|
123
|
-
Signal.trap("TERM") { handle_shutdown("SIGTERM") }
|
|
124
|
-
|
|
125
|
-
# Platform-specific status monitoring
|
|
126
|
-
setup_status_signal
|
|
275
|
+
@signal_handler.setup
|
|
127
276
|
end
|
|
128
277
|
|
|
129
|
-
#
|
|
130
|
-
def
|
|
131
|
-
if
|
|
132
|
-
puts "\n#{signal_name} received. Initiating graceful shutdown..." if ENV["FRACTOR_DEBUG"]
|
|
278
|
+
# Callback for signal handler shutdown requests.
|
|
279
|
+
def handle_shutdown_callback(mode)
|
|
280
|
+
if mode == :graceful
|
|
133
281
|
stop
|
|
134
282
|
else
|
|
135
|
-
|
|
136
|
-
Thread.current.raise(ShutdownSignal, "Interrupted by
|
|
137
|
-
end
|
|
138
|
-
rescue Exception => e
|
|
139
|
-
puts "Error in signal handler: #{e.class}: #{e.message}" if ENV["FRACTOR_DEBUG"]
|
|
140
|
-
puts e.backtrace.join("\n") if ENV["FRACTOR_DEBUG"]
|
|
141
|
-
exit!(1)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Sets up platform-specific status monitoring signal
|
|
145
|
-
def setup_status_signal
|
|
146
|
-
if Gem.win_platform?
|
|
147
|
-
# Windows: Try SIGBREAK (Ctrl+Break) if available
|
|
148
|
-
begin
|
|
149
|
-
Signal.trap("BREAK") { print_status }
|
|
150
|
-
rescue ArgumentError
|
|
151
|
-
# SIGBREAK not supported on this Ruby version/platform
|
|
152
|
-
# Status monitoring unavailable on Windows
|
|
153
|
-
end
|
|
154
|
-
else
|
|
155
|
-
# Unix/Linux/macOS: Use SIGUSR1
|
|
156
|
-
begin
|
|
157
|
-
Signal.trap("USR1") { print_status }
|
|
158
|
-
rescue ArgumentError
|
|
159
|
-
# SIGUSR1 not supported on this platform
|
|
160
|
-
end
|
|
283
|
+
# Immediate shutdown - raise signal in current thread
|
|
284
|
+
Thread.current.raise(ShutdownSignal, "Interrupted by signal")
|
|
161
285
|
end
|
|
162
286
|
end
|
|
163
287
|
|
|
164
288
|
# Prints current supervisor status
|
|
165
289
|
def print_status
|
|
290
|
+
status = @work_distribution_manager.status_summary
|
|
166
291
|
puts "\n=== Fractor Supervisor Status ==="
|
|
167
292
|
puts "Mode: #{@continuous_mode ? 'Continuous' : 'Batch'}"
|
|
168
293
|
puts "Running: #{@running}"
|
|
169
294
|
puts "Workers: #{@workers.size}"
|
|
170
|
-
puts "Idle workers: #{
|
|
295
|
+
puts "Idle workers: #{status[:idle]}"
|
|
171
296
|
puts "Queue size: #{@work_queue.size}"
|
|
172
297
|
puts "Results: #{@results.results.size}"
|
|
173
298
|
puts "Errors: #{@results.errors.size}"
|
|
174
299
|
puts "================================\n"
|
|
175
300
|
end
|
|
176
301
|
|
|
302
|
+
# Starts the supervisor (alias for run).
|
|
303
|
+
# Provides a consistent API with stop method.
|
|
304
|
+
#
|
|
305
|
+
# @see #run
|
|
306
|
+
def start
|
|
307
|
+
run
|
|
308
|
+
end
|
|
309
|
+
|
|
177
310
|
# Runs the main processing loop.
|
|
178
311
|
def run
|
|
179
312
|
setup_signal_handler
|
|
180
313
|
start_workers
|
|
181
314
|
|
|
182
315
|
@running = true
|
|
183
|
-
processed_count = 0
|
|
184
316
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if @wakeup_ractor && @running
|
|
191
|
-
begin
|
|
192
|
-
@wakeup_ractor.send(:wakeup)
|
|
193
|
-
rescue StandardError => e
|
|
194
|
-
puts "Timer thread error sending wakeup: #{e.message}" if ENV["FRACTOR_DEBUG"]
|
|
195
|
-
break
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
puts "Timer thread shutting down" if ENV["FRACTOR_DEBUG"]
|
|
200
|
-
end
|
|
317
|
+
# Distribute any work that was added before run() was called
|
|
318
|
+
# This is critical for Ruby 4.0 where workers need explicit work distribution
|
|
319
|
+
if @work_distribution_manager
|
|
320
|
+
distributed = @work_distribution_manager.distribute_to_idle_workers
|
|
321
|
+
puts "Distributed initial work to #{distributed} idle workers (work_queue.size: #{@work_queue.size})" if @debug
|
|
201
322
|
end
|
|
202
323
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
while @running && (@continuous_mode || processed_count < @total_work_count)
|
|
206
|
-
processed_count = @results.results.size + @results.errors.size
|
|
207
|
-
|
|
208
|
-
if ENV["FRACTOR_DEBUG"]
|
|
209
|
-
if @continuous_mode
|
|
210
|
-
puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{@work_queue.size}"
|
|
211
|
-
else
|
|
212
|
-
puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Get active Ractor objects from the map keys
|
|
217
|
-
active_ractors = @ractors_map.keys
|
|
218
|
-
|
|
219
|
-
# Check for new work from callbacks if in continuous mode
|
|
220
|
-
if @continuous_mode && !@work_callbacks.empty?
|
|
221
|
-
@work_callbacks.each do |callback|
|
|
222
|
-
new_work = callback.call
|
|
223
|
-
if new_work && !new_work.empty?
|
|
224
|
-
add_work_items(new_work)
|
|
225
|
-
puts "Work source provided #{new_work.size} new items" if ENV["FRACTOR_DEBUG"]
|
|
226
|
-
|
|
227
|
-
# Try to send work to idle workers first
|
|
228
|
-
while !@work_queue.empty? && !@idle_workers.empty?
|
|
229
|
-
worker = @idle_workers.shift
|
|
230
|
-
if send_next_work_if_available(worker)
|
|
231
|
-
puts "Sent work to idle worker #{worker.name}" if ENV["FRACTOR_DEBUG"]
|
|
232
|
-
else
|
|
233
|
-
# Worker couldn't accept work, don't re-add to idle list
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Break if no active workers and queue is empty, but work remains (indicates potential issue)
|
|
241
|
-
if active_ractors.empty? && @work_queue.empty? && !@continuous_mode && processed_count < @total_work_count
|
|
242
|
-
puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if ENV["FRACTOR_DEBUG"]
|
|
243
|
-
break
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# In continuous mode, just wait if no active ractors but keep running
|
|
247
|
-
if active_ractors.empty?
|
|
248
|
-
break unless @continuous_mode
|
|
249
|
-
|
|
250
|
-
sleep(0.1) # Small delay to avoid CPU spinning
|
|
251
|
-
next
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Ractor.select blocks until a message is available from any active Ractor
|
|
255
|
-
# The wakeup ractor ensures we can unblock this call when needed
|
|
256
|
-
ready_ractor_obj, message = Ractor.select(*active_ractors)
|
|
257
|
-
|
|
258
|
-
# Check if this is the wakeup ractor
|
|
259
|
-
if ready_ractor_obj == @wakeup_ractor
|
|
260
|
-
puts "Wakeup signal received: #{message[:message]}" if ENV["FRACTOR_DEBUG"]
|
|
261
|
-
# Remove wakeup ractor from map if shutting down
|
|
262
|
-
if message[:message] == :shutdown
|
|
263
|
-
@ractors_map.delete(@wakeup_ractor)
|
|
264
|
-
@wakeup_ractor = nil
|
|
265
|
-
end
|
|
266
|
-
# Continue loop to check @running flag
|
|
267
|
-
next
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Find the corresponding WrappedRactor instance
|
|
271
|
-
wrapped_ractor = @ractors_map[ready_ractor_obj]
|
|
272
|
-
unless wrapped_ractor
|
|
273
|
-
puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring." if ENV["FRACTOR_DEBUG"]
|
|
274
|
-
next
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if ENV["FRACTOR_DEBUG"]
|
|
278
|
-
|
|
279
|
-
# Process the received message
|
|
280
|
-
case message[:type]
|
|
281
|
-
when :initialize
|
|
282
|
-
puts "Ractor initialized: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
|
283
|
-
# Send work immediately upon initialization if available
|
|
284
|
-
if send_next_work_if_available(wrapped_ractor)
|
|
285
|
-
# Work was sent
|
|
286
|
-
else
|
|
287
|
-
# No work available, mark worker as idle
|
|
288
|
-
@idle_workers << wrapped_ractor unless @idle_workers.include?(wrapped_ractor)
|
|
289
|
-
puts "Worker #{wrapped_ractor.name} marked as idle" if ENV["FRACTOR_DEBUG"]
|
|
290
|
-
end
|
|
291
|
-
when :shutdown
|
|
292
|
-
puts "Ractor #{wrapped_ractor.name} acknowledged shutdown" if ENV["FRACTOR_DEBUG"]
|
|
293
|
-
# Remove from active ractors
|
|
294
|
-
@ractors_map.delete(ready_ractor_obj)
|
|
295
|
-
when :result
|
|
296
|
-
# The message[:result] should be a WorkResult object
|
|
297
|
-
work_result = message[:result]
|
|
298
|
-
puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
|
299
|
-
@results.add_result(work_result)
|
|
300
|
-
if ENV["FRACTOR_DEBUG"]
|
|
301
|
-
puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}"
|
|
302
|
-
puts "Aggregated Results: #{@results.inspect}" unless @continuous_mode
|
|
303
|
-
end
|
|
304
|
-
# Send next piece of work
|
|
305
|
-
if send_next_work_if_available(wrapped_ractor)
|
|
306
|
-
# Work was sent
|
|
307
|
-
else
|
|
308
|
-
# No work available, mark worker as idle
|
|
309
|
-
@idle_workers << wrapped_ractor unless @idle_workers.include?(wrapped_ractor)
|
|
310
|
-
puts "Worker #{wrapped_ractor.name} marked as idle after completing work" if ENV["FRACTOR_DEBUG"]
|
|
311
|
-
end
|
|
312
|
-
when :error
|
|
313
|
-
# The message[:result] should be a WorkResult object containing the error
|
|
314
|
-
error_result = message[:result]
|
|
315
|
-
puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}" if ENV["FRACTOR_DEBUG"]
|
|
316
|
-
@results.add_result(error_result) # Add error to aggregator
|
|
317
|
-
if ENV["FRACTOR_DEBUG"]
|
|
318
|
-
puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}"
|
|
319
|
-
puts "Aggregated Results (including errors): #{@results.inspect}" unless @continuous_mode
|
|
320
|
-
end
|
|
321
|
-
# Send next piece of work even after an error
|
|
322
|
-
if send_next_work_if_available(wrapped_ractor)
|
|
323
|
-
# Work was sent
|
|
324
|
-
else
|
|
325
|
-
# No work available, mark worker as idle
|
|
326
|
-
@idle_workers << wrapped_ractor unless @idle_workers.include?(wrapped_ractor)
|
|
327
|
-
puts "Worker #{wrapped_ractor.name} marked as idle after error" if ENV["FRACTOR_DEBUG"]
|
|
328
|
-
end
|
|
329
|
-
else
|
|
330
|
-
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
|
|
331
|
-
end
|
|
332
|
-
# Update processed count for the loop condition
|
|
333
|
-
processed_count = @results.results.size + @results.errors.size
|
|
334
|
-
end
|
|
324
|
+
# Start timer thread for continuous mode to periodically check work sources
|
|
325
|
+
start_timer_thread if @continuous_mode && !@work_callbacks.empty?
|
|
335
326
|
|
|
336
|
-
|
|
327
|
+
begin
|
|
328
|
+
# Run the main event loop through MainLoopHandler
|
|
329
|
+
@main_loop_handler = MainLoopHandler.create(self, debug: @debug)
|
|
330
|
+
@main_loop_handler.run_loop
|
|
337
331
|
rescue ShutdownSignal => e
|
|
338
|
-
puts "Shutdown signal caught: #{e.message}" if
|
|
339
|
-
puts "Sending shutdown message to all Ractors..." if
|
|
332
|
+
puts "Shutdown signal caught: #{e.message}" if @debug
|
|
333
|
+
puts "Sending shutdown message to all Ractors..." if @debug
|
|
340
334
|
|
|
341
335
|
# Send shutdown message to each worker Ractor
|
|
342
336
|
@workers.each do |w|
|
|
343
337
|
w.send(:shutdown)
|
|
344
|
-
puts "Sent shutdown to Ractor: #{w.name}" if
|
|
338
|
+
puts "Sent shutdown to Ractor: #{w.name}" if @debug
|
|
345
339
|
rescue StandardError => send_error
|
|
346
|
-
puts "Error sending shutdown to Ractor #{w.name}: #{send_error.message}" if
|
|
340
|
+
puts "Error sending shutdown to Ractor #{w.name}: #{send_error.message}" if @debug
|
|
347
341
|
end
|
|
348
342
|
|
|
349
|
-
puts "Exiting due to shutdown signal." if
|
|
343
|
+
puts "Exiting due to shutdown signal." if @debug
|
|
350
344
|
exit!(1) # Force exit immediately
|
|
351
345
|
end
|
|
352
346
|
|
|
353
347
|
return if @continuous_mode
|
|
354
348
|
|
|
355
|
-
return unless
|
|
349
|
+
return unless @debug
|
|
356
350
|
|
|
357
351
|
puts "Final Aggregated Results: #{@results.inspect}"
|
|
358
352
|
end
|
|
359
353
|
|
|
360
354
|
# Stop the supervisor (for continuous mode)
|
|
361
355
|
def stop
|
|
356
|
+
puts "Stopping supervisor..." if @debug
|
|
357
|
+
|
|
358
|
+
# Initiate shutdown in main loop handler first, so it continues
|
|
359
|
+
# processing shutdown acknowledgments even after @running = false
|
|
360
|
+
@main_loop_handler&.initiate_shutdown
|
|
361
|
+
|
|
362
362
|
@running = false
|
|
363
|
-
puts "Stopping supervisor..." if ENV["FRACTOR_DEBUG"]
|
|
364
363
|
|
|
365
|
-
#
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
# Update shutdown handler with current references before shutdown
|
|
365
|
+
@shutdown_handler.instance_variable_set(:@workers, @workers)
|
|
366
|
+
@shutdown_handler.instance_variable_set(:@wakeup_ractor, @wakeup_ractor)
|
|
367
|
+
@shutdown_handler.instance_variable_set(:@timer_thread, @timer_thread)
|
|
368
|
+
@shutdown_handler.instance_variable_set(:@performance_monitor,
|
|
369
|
+
@performance_monitor)
|
|
370
370
|
|
|
371
|
-
#
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
puts "Sent shutdown signal to wakeup ractor" if ENV["FRACTOR_DEBUG"]
|
|
376
|
-
rescue StandardError => e
|
|
377
|
-
puts "Error sending shutdown to wakeup ractor: #{e.message}" if ENV["FRACTOR_DEBUG"]
|
|
378
|
-
end
|
|
379
|
-
end
|
|
371
|
+
# Send shutdown signals but don't wait for workers to close
|
|
372
|
+
# The caller (e.g., ContinuousServer) should wait for the main loop thread
|
|
373
|
+
@shutdown_handler.shutdown
|
|
374
|
+
end
|
|
380
375
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
# Start the timer thread for continuous mode.
|
|
379
|
+
# This thread periodically wakes up the main loop to check for new work.
|
|
380
|
+
#
|
|
381
|
+
# @return [void]
|
|
382
|
+
def start_timer_thread
|
|
383
|
+
@timer_thread = Thread.new do
|
|
384
|
+
while @running
|
|
385
|
+
sleep(0.1) # Check work sources every 100ms
|
|
386
|
+
if @wakeup_ractor && @running
|
|
387
|
+
begin
|
|
388
|
+
@wakeup_ractor.send(:wakeup)
|
|
389
|
+
rescue StandardError => e
|
|
390
|
+
puts "Timer thread error sending wakeup: #{e.message}" if @debug
|
|
391
|
+
break
|
|
392
|
+
end
|
|
393
|
+
end
|
|
387
394
|
end
|
|
388
|
-
puts "
|
|
395
|
+
puts "Timer thread shutting down" if @debug
|
|
389
396
|
end
|
|
390
397
|
end
|
|
391
398
|
|
|
392
|
-
|
|
399
|
+
# Format error context with rich information for debugging.
|
|
400
|
+
# Uses ErrorFormatter to generate formatted error messages.
|
|
401
|
+
#
|
|
402
|
+
# @param wrapped_ractor [WrappedRactor] The worker that encountered the error
|
|
403
|
+
# @param error_result [WorkResult] The error result
|
|
404
|
+
# @return [String] Formatted error message with context
|
|
405
|
+
def format_error_context(wrapped_ractor, error_result)
|
|
406
|
+
@error_formatter.format(wrapped_ractor, error_result)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Trace a work event using instance-specific or global tracer configuration.
|
|
410
|
+
# This allows multiple Supervisors to have independent tracer settings.
|
|
411
|
+
# @param event [Symbol] The event type (:queued, :completed, :failed, etc.)
|
|
412
|
+
# @param work [Work] The work item
|
|
413
|
+
# @param context [Hash] Additional context (worker_name, worker_class, etc.)
|
|
414
|
+
def trace_work(event, work = nil, context = {})
|
|
415
|
+
# Check if instance-specific tracing is configured
|
|
416
|
+
if @tracer_enabled.nil? && @tracer_stream.nil?
|
|
417
|
+
# No instance config - use global ExecutionTracer
|
|
418
|
+
Fractor::ExecutionTracer.trace(event, work, context)
|
|
419
|
+
return
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Instance-specific tracing - do it here
|
|
423
|
+
enabled = @tracer_enabled.nil? ? ExecutionTracer.enabled? : @tracer_enabled
|
|
424
|
+
return unless enabled
|
|
425
|
+
|
|
426
|
+
stream = @tracer_stream || ExecutionTracer.trace_stream
|
|
427
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
428
|
+
thread_id = Thread.current.object_id
|
|
429
|
+
|
|
430
|
+
# Build trace line (simplified version of ExecutionTracer logic)
|
|
431
|
+
parts = [
|
|
432
|
+
"[TRACE]",
|
|
433
|
+
timestamp,
|
|
434
|
+
"[T#{thread_id}]",
|
|
435
|
+
event.to_s.upcase,
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
if work
|
|
439
|
+
work_info = work.instance_of?(::Fractor::Work) ? "Work" : work.class.name
|
|
440
|
+
parts << "#{work_info}:#{work.object_id}"
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
if context[:worker_name]
|
|
444
|
+
parts << "worker=#{context[:worker_name]}"
|
|
445
|
+
end
|
|
446
|
+
if context[:worker_class]
|
|
447
|
+
parts << "class=#{context[:worker_class]}"
|
|
448
|
+
end
|
|
449
|
+
if context[:duration_ms]
|
|
450
|
+
parts << "duration=#{context[:duration_ms]}ms"
|
|
451
|
+
end
|
|
452
|
+
if context[:queue_size]
|
|
453
|
+
parts << "queue_size=#{context[:queue_size]}"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
stream.puts(parts.join(" "))
|
|
457
|
+
end
|
|
393
458
|
|
|
394
459
|
# Detects the number of available processors on the system.
|
|
395
460
|
# Returns the number of processors, or 2 as a fallback if detection fails.
|
|
396
461
|
def detect_num_workers
|
|
397
462
|
num_processors = Etc.nprocessors
|
|
398
|
-
puts "Auto-detected #{num_processors} available processors" if
|
|
463
|
+
puts "Auto-detected #{num_processors} available processors" if @debug
|
|
399
464
|
num_processors
|
|
400
465
|
rescue StandardError => e
|
|
401
|
-
puts "Failed to detect processors: #{e.message}. Using default of 2 workers." if
|
|
466
|
+
puts "Failed to detect processors: #{e.message}. Using default of 2 workers." if @debug
|
|
402
467
|
2
|
|
403
468
|
end
|
|
404
469
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
470
|
+
public
|
|
471
|
+
|
|
472
|
+
# ============================================
|
|
473
|
+
# DEBUGGING METHODS
|
|
474
|
+
# ============================================
|
|
475
|
+
|
|
476
|
+
# Inspect the current state of the work queue
|
|
477
|
+
# Returns a hash with queue information and items
|
|
478
|
+
def inspect_queue
|
|
479
|
+
items = []
|
|
480
|
+
# Queue doesn't have to_a, need to iterate
|
|
481
|
+
temp_queue = Queue.new
|
|
482
|
+
until @work_queue.empty?
|
|
483
|
+
item = @work_queue.pop
|
|
484
|
+
items << item
|
|
485
|
+
temp_queue.push(item)
|
|
486
|
+
end
|
|
487
|
+
# Restore the queue
|
|
488
|
+
until temp_queue.empty?
|
|
489
|
+
@work_queue.push(temp_queue.pop)
|
|
490
|
+
end
|
|
424
491
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
492
|
+
{
|
|
493
|
+
size: @work_queue.size,
|
|
494
|
+
total_added: @total_work_count,
|
|
495
|
+
items: items.map do |work|
|
|
496
|
+
{
|
|
497
|
+
class: work.class.name,
|
|
498
|
+
input: work.input,
|
|
499
|
+
inspect: work.inspect,
|
|
500
|
+
}
|
|
501
|
+
end,
|
|
502
|
+
}
|
|
503
|
+
end
|
|
428
504
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
505
|
+
# Get current worker status
|
|
506
|
+
# Returns a hash with worker statistics
|
|
507
|
+
def workers_status
|
|
508
|
+
status = @work_distribution_manager.status_summary
|
|
509
|
+
idle_count = status[:idle]
|
|
510
|
+
busy_count = status[:busy]
|
|
511
|
+
|
|
512
|
+
{
|
|
513
|
+
total: @workers.size,
|
|
514
|
+
idle: idle_count,
|
|
515
|
+
busy: busy_count,
|
|
516
|
+
pools: @worker_pools.map do |pool|
|
|
517
|
+
{
|
|
518
|
+
worker_class: pool[:worker_class].name,
|
|
519
|
+
num_workers: pool[:num_workers],
|
|
520
|
+
workers: pool[:workers].map do |w|
|
|
521
|
+
{
|
|
522
|
+
name: w.name,
|
|
523
|
+
idle: @work_distribution_manager.idle_workers_list.include?(w),
|
|
524
|
+
}
|
|
525
|
+
end,
|
|
526
|
+
}
|
|
527
|
+
end,
|
|
528
|
+
}
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Enable debug mode for verbose output
|
|
532
|
+
def debug!
|
|
533
|
+
@debug = true
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Disable debug mode
|
|
537
|
+
def debug_off!
|
|
538
|
+
@debug = false
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Check if debug mode is enabled
|
|
542
|
+
def debug?
|
|
543
|
+
@debug
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Get performance metrics snapshot if performance monitoring is enabled
|
|
547
|
+
# Returns nil if performance monitoring is not enabled
|
|
548
|
+
def performance_metrics
|
|
549
|
+
return nil unless @performance_monitor
|
|
550
|
+
|
|
551
|
+
@performance_monitor.snapshot
|
|
439
552
|
end
|
|
440
553
|
end
|
|
441
554
|
end
|