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.
- 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,406 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Handles the main event loop for a Supervisor.
|
|
5
|
+
# Responsible for processing Ractor messages and coordinating work distribution.
|
|
6
|
+
#
|
|
7
|
+
# This class extracts the main loop logic from Supervisor to follow
|
|
8
|
+
# the Single Responsibility Principle.
|
|
9
|
+
class MainLoopHandler
|
|
10
|
+
def initialize(supervisor, debug: false)
|
|
11
|
+
@supervisor = supervisor
|
|
12
|
+
@debug = debug
|
|
13
|
+
@shutting_down = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Factory method to create the appropriate MainLoopHandler implementation
|
|
17
|
+
# based on the current Ruby version.
|
|
18
|
+
#
|
|
19
|
+
# @param supervisor [Fractor::Supervisor] The supervisor instance
|
|
20
|
+
# @param debug [Boolean] Whether debug mode is enabled
|
|
21
|
+
# @return [MainLoopHandler] The appropriate subclass instance
|
|
22
|
+
def self.create(supervisor, debug: false)
|
|
23
|
+
ruby_4_0 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
|
|
24
|
+
if ruby_4_0
|
|
25
|
+
MainLoopHandler4.new(supervisor, debug: debug)
|
|
26
|
+
else
|
|
27
|
+
MainLoopHandler3.new(supervisor, debug: debug)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Run the main event loop.
|
|
32
|
+
# This method blocks until all work is processed (batch mode) or until stopped (continuous mode).
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def run_loop
|
|
36
|
+
raise NotImplementedError, "Subclasses must implement #run_loop"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Clean up the ractors map after batch processing.
|
|
40
|
+
# This is critical on Windows Ruby 3.4 where workers may not respond to shutdown
|
|
41
|
+
# if they're stuck in Ractor.receive.
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
def cleanup_ractors_map
|
|
45
|
+
return if ractors_map.empty?
|
|
46
|
+
|
|
47
|
+
puts "Cleaning up ractors map (#{ractors_map.size} entries)..." if @debug
|
|
48
|
+
|
|
49
|
+
# Simply clear the map without trying to interact with ractors
|
|
50
|
+
# The main loop already attempted to shut down workers properly
|
|
51
|
+
# On Windows Ruby 3.4, some workers may be stuck in Ractor.receive
|
|
52
|
+
# and will never acknowledge shutdown - we must not block on them
|
|
53
|
+
ractors_map.clear
|
|
54
|
+
|
|
55
|
+
# Force garbage collection to help clean up orphaned ractors
|
|
56
|
+
# This is a workaround for Ruby 3.4 Windows where orphaned ractors
|
|
57
|
+
# can block creation of new ractors in subsequent tests
|
|
58
|
+
GC.start
|
|
59
|
+
puts "Ractors map cleared and GC forced." if @debug
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Initiate graceful shutdown.
|
|
63
|
+
# Sets the shutting_down flag to allow the main loop to process
|
|
64
|
+
# shutdown acknowledgments before exiting.
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def initiate_shutdown
|
|
68
|
+
@shutting_down = true
|
|
69
|
+
puts "Main loop shutdown initiated" if @debug
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Get the current processed count from results.
|
|
75
|
+
#
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
def get_processed_count
|
|
78
|
+
@supervisor.results.results.size + @supervisor.results.errors.size
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if the main loop should continue running.
|
|
82
|
+
# Continues during shutdown until all workers have acknowledged.
|
|
83
|
+
#
|
|
84
|
+
# @param processed_count [Integer] Current number of processed items
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def should_continue_running?(processed_count)
|
|
87
|
+
return true if @shutting_down && !all_workers_closed?
|
|
88
|
+
|
|
89
|
+
running? && (continuous_mode? || processed_count < total_work_count)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if all workers have closed (acknowledged shutdown).
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def all_workers_closed?
|
|
96
|
+
workers.all?(&:closed?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Log current processing status for debugging.
|
|
100
|
+
#
|
|
101
|
+
# @param processed_count [Integer] Current number of processed items
|
|
102
|
+
# @return [void]
|
|
103
|
+
def log_processing_status(processed_count)
|
|
104
|
+
return unless @debug
|
|
105
|
+
|
|
106
|
+
if continuous_mode?
|
|
107
|
+
puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{work_queue.size}"
|
|
108
|
+
else
|
|
109
|
+
puts "Waiting for Ractor results. Processed: #{processed_count}/#{total_work_count}, Queue size: #{work_queue.size}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Process a message from a ractor.
|
|
114
|
+
#
|
|
115
|
+
# @param ready_ractor_obj [Ractor] The ractor that sent the message
|
|
116
|
+
# @param message [Hash] The message received
|
|
117
|
+
# @return [void]
|
|
118
|
+
def process_message(ready_ractor_obj, message)
|
|
119
|
+
# Find the corresponding WrappedRactor instance
|
|
120
|
+
wrapped_ractor = ractors_map[ready_ractor_obj]
|
|
121
|
+
unless wrapped_ractor
|
|
122
|
+
puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring." if @debug
|
|
123
|
+
ractors_map.delete(ready_ractor_obj)
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Guard against nil messages (indicates closed ractor)
|
|
128
|
+
if message.nil?
|
|
129
|
+
puts "Warning: Received nil message from #{wrapped_ractor.name}. Ractor likely closed." if @debug
|
|
130
|
+
ractors_map.delete(ready_ractor_obj)
|
|
131
|
+
workers.delete(wrapped_ractor)
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if @debug
|
|
136
|
+
|
|
137
|
+
# Route to appropriate message handler
|
|
138
|
+
case message[:type]
|
|
139
|
+
when :initialize
|
|
140
|
+
handle_initialize_message(wrapped_ractor)
|
|
141
|
+
when :shutdown
|
|
142
|
+
handle_shutdown_message(ready_ractor_obj, wrapped_ractor)
|
|
143
|
+
when :result
|
|
144
|
+
handle_result_message(wrapped_ractor, message)
|
|
145
|
+
when :error
|
|
146
|
+
handle_error_message(wrapped_ractor, message)
|
|
147
|
+
else
|
|
148
|
+
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if @debug
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Handle :initialize message from a worker.
|
|
153
|
+
#
|
|
154
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
155
|
+
# @return [void]
|
|
156
|
+
def handle_initialize_message(wrapped_ractor)
|
|
157
|
+
puts "Ractor initialized: #{wrapped_ractor.worker_class}" if @debug
|
|
158
|
+
|
|
159
|
+
if work_distribution_manager.assign_work_to_worker(wrapped_ractor)
|
|
160
|
+
# Work was sent
|
|
161
|
+
elsif continuous_mode?
|
|
162
|
+
work_distribution_manager.mark_worker_idle(wrapped_ractor)
|
|
163
|
+
puts "Worker #{wrapped_ractor.name} marked as idle (continuous mode)" if @debug
|
|
164
|
+
else
|
|
165
|
+
handle_batch_mode_no_work(wrapped_ractor)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Handle :shutdown message from a worker.
|
|
170
|
+
#
|
|
171
|
+
# @param ready_ractor_obj [Ractor] The ractor object
|
|
172
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
173
|
+
# @return [void]
|
|
174
|
+
def handle_shutdown_message(ready_ractor_obj, wrapped_ractor)
|
|
175
|
+
puts "Ractor #{wrapped_ractor.name} acknowledged shutdown" if @debug
|
|
176
|
+
ractors_map.delete(ready_ractor_obj)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Handle :result message from a worker.
|
|
180
|
+
#
|
|
181
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
182
|
+
# @param message [Hash] The message containing the result
|
|
183
|
+
# @return [void]
|
|
184
|
+
def handle_result_message(wrapped_ractor, message)
|
|
185
|
+
work_result = message[:result]
|
|
186
|
+
puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if @debug
|
|
187
|
+
|
|
188
|
+
# Record performance metrics
|
|
189
|
+
record_performance_metrics(work_result, success: true)
|
|
190
|
+
|
|
191
|
+
# Trace work item completed
|
|
192
|
+
@supervisor.send(:trace_work, :completed, work_result.work,
|
|
193
|
+
worker_name: wrapped_ractor.name,
|
|
194
|
+
worker_class: wrapped_ractor.worker_class)
|
|
195
|
+
|
|
196
|
+
# Record result to error reporter
|
|
197
|
+
error_reporter.record(work_result,
|
|
198
|
+
job_name: wrapped_ractor.worker_class.name)
|
|
199
|
+
|
|
200
|
+
results.add_result(work_result)
|
|
201
|
+
|
|
202
|
+
if @debug
|
|
203
|
+
puts "Result processed. Total processed: #{results.results.size + results.errors.size}"
|
|
204
|
+
puts "Aggregated Results: #{results.inspect}" unless continuous_mode?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Send next piece of work
|
|
208
|
+
assign_next_work_or_shutdown(wrapped_ractor)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Handle :error message from a worker.
|
|
212
|
+
#
|
|
213
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
214
|
+
# @param message [Hash] The message containing the error
|
|
215
|
+
# @return [void]
|
|
216
|
+
def handle_error_message(wrapped_ractor, message)
|
|
217
|
+
error_result = message[:result]
|
|
218
|
+
|
|
219
|
+
# Record performance metrics
|
|
220
|
+
record_performance_metrics(error_result, success: false)
|
|
221
|
+
|
|
222
|
+
# Trace work item failed
|
|
223
|
+
@supervisor.send(:trace_work, :failed, error_result.work,
|
|
224
|
+
worker_name: wrapped_ractor.name,
|
|
225
|
+
worker_class: wrapped_ractor.worker_class)
|
|
226
|
+
|
|
227
|
+
# Record error to error reporter
|
|
228
|
+
error_reporter.record(error_result,
|
|
229
|
+
job_name: wrapped_ractor.worker_class.name)
|
|
230
|
+
|
|
231
|
+
# Invoke error callbacks
|
|
232
|
+
error_callbacks.each do |callback|
|
|
233
|
+
callback.call(error_result, wrapped_ractor.name,
|
|
234
|
+
wrapped_ractor.worker_class)
|
|
235
|
+
rescue StandardError => e
|
|
236
|
+
puts "Error in error callback: #{e.message}" if @debug
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Enhanced error message with context
|
|
240
|
+
error_context = @supervisor.send(:format_error_context, wrapped_ractor,
|
|
241
|
+
error_result)
|
|
242
|
+
puts error_context if @debug
|
|
243
|
+
|
|
244
|
+
results.add_result(error_result)
|
|
245
|
+
|
|
246
|
+
if @debug
|
|
247
|
+
puts "Error handled. Total processed: #{results.results.size + results.errors.size}"
|
|
248
|
+
puts "Aggregated Results (including errors): #{results.inspect}" unless continuous_mode?
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Send next piece of work even after an error
|
|
252
|
+
assign_next_work_or_shutdown(wrapped_ractor)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Record performance metrics for a completed job.
|
|
256
|
+
#
|
|
257
|
+
# @param work_result [WorkResult] The result object
|
|
258
|
+
# @param success [Boolean] Whether the job succeeded
|
|
259
|
+
# @return [void]
|
|
260
|
+
def record_performance_metrics(work_result, success:)
|
|
261
|
+
return unless performance_monitor && work_result.work
|
|
262
|
+
|
|
263
|
+
start_time = work_distribution_manager.get_work_start_time(work_result.work.object_id)
|
|
264
|
+
return unless start_time
|
|
265
|
+
|
|
266
|
+
latency = Time.now - start_time
|
|
267
|
+
performance_monitor.record_job(latency, success: success)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Handle batch mode when no work is available.
|
|
271
|
+
#
|
|
272
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
273
|
+
# @return [void]
|
|
274
|
+
def handle_batch_mode_no_work(wrapped_ractor)
|
|
275
|
+
current_processed = results.results.size + results.errors.size
|
|
276
|
+
if current_processed >= total_work_count
|
|
277
|
+
puts "All work processed, shutting down worker #{wrapped_ractor.name} (batch mode)" if @debug
|
|
278
|
+
wrapped_ractor.send(:shutdown)
|
|
279
|
+
else
|
|
280
|
+
# Work still pending but queue empty - shouldn't happen in normal flow
|
|
281
|
+
# Keep worker alive and add to idle list
|
|
282
|
+
work_distribution_manager.mark_worker_idle(wrapped_ractor)
|
|
283
|
+
puts "Worker #{wrapped_ractor.name} marked as idle (queue empty but work pending: #{current_processed}/#{total_work_count})" if @debug
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Assign next work to worker or shut down if all work is done.
|
|
288
|
+
#
|
|
289
|
+
# @param wrapped_ractor [WrappedRactor] The worker ractor
|
|
290
|
+
# @return [void]
|
|
291
|
+
def assign_next_work_or_shutdown(wrapped_ractor)
|
|
292
|
+
if work_distribution_manager.assign_work_to_worker(wrapped_ractor)
|
|
293
|
+
# Work was sent
|
|
294
|
+
elsif continuous_mode?
|
|
295
|
+
work_distribution_manager.mark_worker_idle(wrapped_ractor)
|
|
296
|
+
puts "Worker #{wrapped_ractor.name} marked as idle after completing work (continuous mode)" if @debug
|
|
297
|
+
else
|
|
298
|
+
handle_batch_mode_no_work(wrapped_ractor)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Helper methods to access supervisor state
|
|
303
|
+
|
|
304
|
+
def running?
|
|
305
|
+
@supervisor.instance_variable_get(:@running)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def continuous_mode?
|
|
309
|
+
@supervisor.instance_variable_get(:@continuous_mode)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def total_work_count
|
|
313
|
+
@supervisor.instance_variable_get(:@total_work_count)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def work_queue
|
|
317
|
+
@supervisor.work_queue
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def workers
|
|
321
|
+
@supervisor.workers
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def ractors_map
|
|
325
|
+
@supervisor.instance_variable_get(:@ractors_map)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def wakeup_ractor
|
|
329
|
+
@supervisor.instance_variable_get(:@wakeup_ractor)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def wakeup_port
|
|
333
|
+
@supervisor.instance_variable_get(:@wakeup_port)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def work_distribution_manager
|
|
337
|
+
@supervisor.instance_variable_get(:@work_distribution_manager)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def results
|
|
341
|
+
@supervisor.results
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def error_reporter
|
|
345
|
+
@supervisor.error_reporter
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def performance_monitor
|
|
349
|
+
@supervisor.instance_variable_get(:@performance_monitor)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def work_callbacks
|
|
353
|
+
@supervisor.instance_variable_get(:@work_callbacks)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def error_callbacks
|
|
357
|
+
@supervisor.instance_variable_get(:@error_callbacks)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Check if running on Windows with Ruby 3.4
|
|
361
|
+
# Returns true for Windows Ruby 3.4.x where Ractor issues occur
|
|
362
|
+
#
|
|
363
|
+
# @return [Boolean]
|
|
364
|
+
def windows_ruby_34?
|
|
365
|
+
return false unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
|
|
366
|
+
|
|
367
|
+
ruby_version = Gem::Version.new(RUBY_VERSION)
|
|
368
|
+
ruby_version >= Gem::Version.new("3.4.0") && ruby_version < Gem::Version.new("3.5.0")
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Handle a stuck ractor by identifying and removing it from the active pool
|
|
372
|
+
# This is called when Ractor.select times out on Windows Ruby 3.4
|
|
373
|
+
#
|
|
374
|
+
# @param active [Array] List of active ractors/ports
|
|
375
|
+
# @return [void]
|
|
376
|
+
def handle_stuck_ractor(active)
|
|
377
|
+
puts "[WARNING] Ractor.select timeout - detecting stuck ractor..." if @debug
|
|
378
|
+
|
|
379
|
+
# Try to identify which ractor is stuck by checking their state
|
|
380
|
+
active.each do |ractor_or_port|
|
|
381
|
+
# Skip ports (Ruby 4.0) - they should be checked differently
|
|
382
|
+
next if ractor_or_port.is_a?(Ractor::Port)
|
|
383
|
+
|
|
384
|
+
wrapped_ractor = ractors_map[ractor_or_port]
|
|
385
|
+
next unless wrapped_ractor
|
|
386
|
+
|
|
387
|
+
# Check if ractor appears stuck (terminated or blocked)
|
|
388
|
+
begin
|
|
389
|
+
inspect_result = Timeout.timeout(0.1) { ractor_or_port.inspect }
|
|
390
|
+
rescue Timeout::Error
|
|
391
|
+
inspect_result = "#<Ractor:blocked>"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
if inspect_result.include?("terminated") || inspect_result.include?("invalid")
|
|
395
|
+
puts "[WARNING] Removing stuck/terminated ractor: #{wrapped_ractor.name}" if @debug
|
|
396
|
+
ractors_map.delete(ractor_or_port)
|
|
397
|
+
workers.delete(wrapped_ractor)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Force garbage collection to help clean up stuck ractors
|
|
402
|
+
GC.start
|
|
403
|
+
puts "[WARNING] Stuck ractor handled, GC forced" if @debug
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "main_loop_handler"
|
|
4
|
+
|
|
5
|
+
module Fractor
|
|
6
|
+
# Ruby 3.x specific implementation of MainLoopHandler.
|
|
7
|
+
# Uses Ractor.yield for worker communication.
|
|
8
|
+
class MainLoopHandler3 < MainLoopHandler
|
|
9
|
+
# Run the main event loop for Ruby 3.x.
|
|
10
|
+
def run_loop
|
|
11
|
+
loop do
|
|
12
|
+
processed_count = get_processed_count
|
|
13
|
+
|
|
14
|
+
# Check loop termination condition
|
|
15
|
+
break unless should_continue_running?(processed_count)
|
|
16
|
+
|
|
17
|
+
log_processing_status(processed_count)
|
|
18
|
+
|
|
19
|
+
active_ractors = get_active_ractors
|
|
20
|
+
|
|
21
|
+
# Check for new work from callbacks if in continuous mode
|
|
22
|
+
process_work_callbacks if continuous_mode? && !work_callbacks.empty?
|
|
23
|
+
|
|
24
|
+
# Handle edge cases - break if edge case handler indicates we should
|
|
25
|
+
next if handle_edge_cases(active_ractors, processed_count)
|
|
26
|
+
|
|
27
|
+
# Wait for next message from any active ractor
|
|
28
|
+
ready_ractor_obj, message = select_from_ractors(active_ractors)
|
|
29
|
+
next unless ready_ractor_obj && message
|
|
30
|
+
|
|
31
|
+
# Process the received message
|
|
32
|
+
process_message(ready_ractor_obj, message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts "Main loop finished." if @debug
|
|
36
|
+
|
|
37
|
+
# Clean up ractors map after batch mode completion
|
|
38
|
+
cleanup_ractors_map unless continuous_mode?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Get list of active ractors for Ractor.select (Ruby 3.x).
|
|
44
|
+
# Excludes wakeup ractor unless in continuous mode with callbacks.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Ractor>]
|
|
47
|
+
def get_active_ractors
|
|
48
|
+
ractors_map.keys.reject do |ractor|
|
|
49
|
+
ractor == wakeup_ractor && !(continuous_mode? && !work_callbacks.empty?)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check for new work from callbacks in continuous mode.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def process_work_callbacks
|
|
57
|
+
work_callbacks.each do |callback|
|
|
58
|
+
new_work = callback.call
|
|
59
|
+
if new_work && !new_work.empty?
|
|
60
|
+
@supervisor.add_work_items(new_work)
|
|
61
|
+
puts "Work source provided #{new_work.size} new items" if @debug
|
|
62
|
+
|
|
63
|
+
# Distribute work to idle workers
|
|
64
|
+
distributed = work_distribution_manager.distribute_to_idle_workers
|
|
65
|
+
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handle edge cases like no active workers or empty queue.
|
|
71
|
+
#
|
|
72
|
+
# @param active_ractors [Array<Ractor>] List of active ractors
|
|
73
|
+
# @param processed_count [Integer] Current number of processed items
|
|
74
|
+
# @return [Boolean] true if should break from loop
|
|
75
|
+
def handle_edge_cases(active_ractors, processed_count)
|
|
76
|
+
# In continuous mode, if no active ractors and shutting down, exit loop
|
|
77
|
+
if active_ractors.empty? && @shutting_down
|
|
78
|
+
puts "No active ractors during shutdown, exiting main loop" if @debug
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Break if no active workers and queue is empty, but work remains (indicates potential issue)
|
|
83
|
+
if active_ractors.empty? && work_queue.empty? && !continuous_mode? && processed_count < total_work_count
|
|
84
|
+
puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if @debug
|
|
85
|
+
return true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# In continuous mode, just wait if no active ractors but keep running
|
|
89
|
+
if active_ractors.empty?
|
|
90
|
+
return true unless continuous_mode?
|
|
91
|
+
|
|
92
|
+
sleep(0.1) # Small delay to avoid CPU spinning
|
|
93
|
+
return false # Continue to next iteration
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Wait for a message from any active ractor (Ruby 3.x).
|
|
100
|
+
# Uses a timeout on Windows Ruby 3.4 to detect stuck ractors.
|
|
101
|
+
#
|
|
102
|
+
# @param active_ractors [Array<Ractor>] List of active ractors to select from
|
|
103
|
+
# @return [Array] ready_ractor_obj and message, or nil if should continue
|
|
104
|
+
def select_from_ractors(active_ractors)
|
|
105
|
+
# On Windows Ruby 3.4, use timeout to detect stuck ractors
|
|
106
|
+
ready_ractor_obj, message = if windows_ruby_34?
|
|
107
|
+
begin
|
|
108
|
+
Timeout.timeout(30) do
|
|
109
|
+
Ractor.select(*active_ractors)
|
|
110
|
+
end
|
|
111
|
+
rescue Timeout::Error
|
|
112
|
+
# Timeout indicates a ractor is stuck - identify and remove it
|
|
113
|
+
handle_stuck_ractor(active_ractors)
|
|
114
|
+
return nil, nil
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
Ractor.select(*active_ractors)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if this is the wakeup ractor
|
|
121
|
+
if ready_ractor_obj == wakeup_ractor
|
|
122
|
+
puts "Wakeup signal received: #{message[:message]}" if @debug
|
|
123
|
+
# Remove wakeup ractor from map if shutting down
|
|
124
|
+
if message[:message] == :shutdown
|
|
125
|
+
ractors_map.delete(wakeup_ractor)
|
|
126
|
+
@supervisor.instance_variable_set(:@wakeup_ractor, nil)
|
|
127
|
+
end
|
|
128
|
+
# Return nil to indicate we should continue to next iteration
|
|
129
|
+
return nil, nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
[ready_ractor_obj, message]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|