fractor 0.1.4 → 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-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- 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/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- 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/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- 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/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- 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 +183 -0
- 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 +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- 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 +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- 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 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
data/lib/fractor/supervisor.rb
CHANGED
|
@@ -1,28 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "etc"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "signal_handler"
|
|
6
|
+
require_relative "error_formatter"
|
|
4
7
|
|
|
5
8
|
module Fractor
|
|
9
|
+
# Custom exception for shutdown signal handling
|
|
10
|
+
class ShutdownSignal < StandardError; end
|
|
11
|
+
|
|
6
12
|
# Supervises multiple WrappedRactors, distributes work, and aggregates results.
|
|
7
13
|
class Supervisor
|
|
8
|
-
attr_reader :work_queue, :workers, :results, :worker_pools
|
|
14
|
+
attr_reader :work_queue, :workers, :results, :worker_pools, :debug,
|
|
15
|
+
:error_reporter, :logger, :performance_monitor
|
|
9
16
|
|
|
10
17
|
# Initializes the Supervisor.
|
|
11
18
|
# - worker_pools: An array of worker pool configurations, each containing:
|
|
12
19
|
# - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
|
|
13
20
|
# - num_workers: The number of Ractors to spawn for this worker class.
|
|
14
21
|
# - continuous_mode: Whether to run in continuous mode without expecting a fixed work count.
|
|
15
|
-
|
|
16
|
-
|
|
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|
|
|
17
35
|
worker_class = pool_config[:worker_class]
|
|
18
36
|
num_workers = pool_config[:num_workers] || detect_num_workers
|
|
19
37
|
|
|
20
|
-
|
|
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
|
+
|
|
49
|
+
unless worker_class < Fractor::Worker
|
|
50
|
+
raise ArgumentError,
|
|
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"
|
|
68
|
+
end
|
|
21
69
|
|
|
22
70
|
{
|
|
23
71
|
worker_class: worker_class,
|
|
24
72
|
num_workers: num_workers,
|
|
25
|
-
workers: [] # Will hold the WrappedRactor instances
|
|
73
|
+
workers: [], # Will hold the WrappedRactor instances
|
|
26
74
|
}
|
|
27
75
|
end
|
|
28
76
|
|
|
@@ -34,16 +82,81 @@ module Fractor
|
|
|
34
82
|
@continuous_mode = continuous_mode
|
|
35
83
|
@running = false
|
|
36
84
|
@work_callbacks = []
|
|
85
|
+
@wakeup_ractor = nil # Control ractor for unblocking select
|
|
86
|
+
@timer_thread = nil # Timer thread for periodic wakeup
|
|
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
|
|
37
127
|
end
|
|
38
128
|
|
|
39
129
|
# Adds a single work item to the queue.
|
|
40
130
|
# The item must be an instance of Fractor::Work or a subclass.
|
|
41
131
|
def add_work_item(work)
|
|
42
|
-
|
|
132
|
+
unless work.is_a?(Fractor::Work)
|
|
133
|
+
raise ArgumentError,
|
|
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)"
|
|
144
|
+
end
|
|
43
145
|
|
|
44
146
|
@work_queue << work
|
|
45
147
|
@total_work_count += 1
|
|
46
|
-
|
|
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
|
|
47
160
|
|
|
48
161
|
puts "Work item added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
|
|
49
162
|
end
|
|
@@ -62,14 +175,72 @@ module Fractor
|
|
|
62
175
|
@work_callbacks << callback
|
|
63
176
|
end
|
|
64
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
|
+
|
|
65
185
|
# Starts the worker Ractors for all worker pools.
|
|
66
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
|
+
|
|
194
|
+
# Create a wakeup Ractor for unblocking Ractor.select
|
|
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
|
|
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
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Add wakeup ractor to the map with a special marker
|
|
226
|
+
@ractors_map[@wakeup_ractor] = :wakeup
|
|
227
|
+
|
|
67
228
|
@worker_pools.each do |pool|
|
|
68
229
|
worker_class = pool[:worker_class]
|
|
69
230
|
num_workers = pool[:num_workers]
|
|
70
231
|
|
|
71
232
|
pool[:workers] = (1..num_workers).map do |i|
|
|
72
|
-
|
|
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
|
+
)
|
|
73
244
|
wrapped_ractor.start # Start the underlying Ractor
|
|
74
245
|
# Map the actual Ractor object to the WrappedRactor instance
|
|
75
246
|
@ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
|
|
@@ -80,40 +251,60 @@ module Fractor
|
|
|
80
251
|
# Flatten all workers for easier access
|
|
81
252
|
@workers = @worker_pools.flat_map { |pool| pool[:workers] }
|
|
82
253
|
@ractors_map.compact! # Ensure map doesn't contain nil keys/values
|
|
83
|
-
|
|
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
|
|
84
267
|
|
|
85
268
|
puts "Workers started: #{@workers.size} active across #{@worker_pools.size} pools."
|
|
269
|
+
puts "All workers marked as idle and ready for work."
|
|
86
270
|
end
|
|
87
271
|
|
|
88
|
-
# Sets up
|
|
272
|
+
# Sets up signal handlers for graceful shutdown.
|
|
273
|
+
# Uses SignalHandler to manage signal handling logic.
|
|
89
274
|
def setup_signal_handler
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# Trap INT signal (Ctrl+C)
|
|
94
|
-
Signal.trap("INT") do
|
|
95
|
-
puts "\nCtrl+C received. Initiating immediate shutdown..." if ENV["FRACTOR_DEBUG"]
|
|
96
|
-
|
|
97
|
-
# Set running to false to break the main loop
|
|
98
|
-
@running = false
|
|
275
|
+
@signal_handler.setup
|
|
276
|
+
end
|
|
99
277
|
|
|
100
|
-
|
|
278
|
+
# Callback for signal handler shutdown requests.
|
|
279
|
+
def handle_shutdown_callback(mode)
|
|
280
|
+
if mode == :graceful
|
|
281
|
+
stop
|
|
282
|
+
else
|
|
283
|
+
# Immediate shutdown - raise signal in current thread
|
|
284
|
+
Thread.current.raise(ShutdownSignal, "Interrupted by signal")
|
|
285
|
+
end
|
|
286
|
+
end
|
|
101
287
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
288
|
+
# Prints current supervisor status
|
|
289
|
+
def print_status
|
|
290
|
+
status = @work_distribution_manager.status_summary
|
|
291
|
+
puts "\n=== Fractor Supervisor Status ==="
|
|
292
|
+
puts "Mode: #{@continuous_mode ? 'Continuous' : 'Batch'}"
|
|
293
|
+
puts "Running: #{@running}"
|
|
294
|
+
puts "Workers: #{@workers.size}"
|
|
295
|
+
puts "Idle workers: #{status[:idle]}"
|
|
296
|
+
puts "Queue size: #{@work_queue.size}"
|
|
297
|
+
puts "Results: #{@results.results.size}"
|
|
298
|
+
puts "Errors: #{@results.errors.size}"
|
|
299
|
+
puts "================================\n"
|
|
300
|
+
end
|
|
109
301
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
302
|
+
# Starts the supervisor (alias for run).
|
|
303
|
+
# Provides a consistent API with stop method.
|
|
304
|
+
#
|
|
305
|
+
# @see #run
|
|
306
|
+
def start
|
|
307
|
+
run
|
|
117
308
|
end
|
|
118
309
|
|
|
119
310
|
# Runs the main processing loop.
|
|
@@ -122,147 +313,242 @@ module Fractor
|
|
|
122
313
|
start_workers
|
|
123
314
|
|
|
124
315
|
@running = true
|
|
125
|
-
processed_count = 0
|
|
126
|
-
|
|
127
|
-
# Main loop: Process events until conditions are met for termination
|
|
128
|
-
while @running && (@continuous_mode || processed_count < @total_work_count)
|
|
129
|
-
processed_count = @results.results.size + @results.errors.size
|
|
130
|
-
|
|
131
|
-
if ENV["FRACTOR_DEBUG"]
|
|
132
|
-
if @continuous_mode
|
|
133
|
-
puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{@work_queue.size}"
|
|
134
|
-
else
|
|
135
|
-
puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Get active Ractor objects from the map keys
|
|
140
|
-
active_ractors = @ractors_map.keys
|
|
141
|
-
|
|
142
|
-
# Check for new work from callbacks if in continuous mode and queue is empty
|
|
143
|
-
if @continuous_mode && @work_queue.empty? && !@work_callbacks.empty?
|
|
144
|
-
@work_callbacks.each do |callback|
|
|
145
|
-
new_work = callback.call
|
|
146
|
-
add_work_items(new_work) if new_work && !new_work.empty?
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Break if no active workers and queue is empty, but work remains (indicates potential issue)
|
|
151
|
-
if active_ractors.empty? && @work_queue.empty? && !@continuous_mode && processed_count < @total_work_count
|
|
152
|
-
puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if ENV["FRACTOR_DEBUG"]
|
|
153
|
-
break
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# In continuous mode, just wait if no active ractors but keep running
|
|
157
|
-
if active_ractors.empty?
|
|
158
|
-
break unless @continuous_mode
|
|
159
316
|
|
|
160
|
-
|
|
161
|
-
|
|
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 || true
|
|
322
|
+
end
|
|
162
323
|
|
|
163
|
-
|
|
324
|
+
# Start timer thread for continuous mode to periodically check work sources
|
|
325
|
+
start_timer_thread if @continuous_mode && !@work_callbacks.empty?
|
|
164
326
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
331
|
+
rescue ShutdownSignal => e
|
|
332
|
+
puts "Shutdown signal caught: #{e.message}" if @debug
|
|
333
|
+
puts "Sending shutdown message to all Ractors..." if @debug
|
|
167
334
|
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
puts "
|
|
172
|
-
|
|
335
|
+
# Send shutdown message to each worker Ractor
|
|
336
|
+
@workers.each do |w|
|
|
337
|
+
w.send(:shutdown)
|
|
338
|
+
puts "Sent shutdown to Ractor: #{w.name}" if @debug
|
|
339
|
+
rescue StandardError => send_error
|
|
340
|
+
puts "Error sending shutdown to Ractor #{w.name}: #{send_error.message}" if @debug
|
|
173
341
|
end
|
|
174
342
|
|
|
175
|
-
puts "
|
|
176
|
-
|
|
177
|
-
# Process the received message
|
|
178
|
-
case message[:type]
|
|
179
|
-
when :initialize
|
|
180
|
-
puts "Ractor initialized: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
|
181
|
-
# Send work immediately upon initialization if available
|
|
182
|
-
send_next_work_if_available(wrapped_ractor)
|
|
183
|
-
when :result
|
|
184
|
-
# The message[:result] should be a WorkResult object
|
|
185
|
-
work_result = message[:result]
|
|
186
|
-
puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
|
187
|
-
@results.add_result(work_result)
|
|
188
|
-
if ENV["FRACTOR_DEBUG"]
|
|
189
|
-
puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}"
|
|
190
|
-
puts "Aggregated Results: #{@results.inspect}" unless @continuous_mode
|
|
191
|
-
end
|
|
192
|
-
# Send next piece of work
|
|
193
|
-
send_next_work_if_available(wrapped_ractor)
|
|
194
|
-
when :error
|
|
195
|
-
# The message[:result] should be a WorkResult object containing the error
|
|
196
|
-
error_result = message[:result]
|
|
197
|
-
puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}" if ENV["FRACTOR_DEBUG"]
|
|
198
|
-
@results.add_result(error_result) # Add error to aggregator
|
|
199
|
-
if ENV["FRACTOR_DEBUG"]
|
|
200
|
-
puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}"
|
|
201
|
-
puts "Aggregated Results (including errors): #{@results.inspect}" unless @continuous_mode
|
|
202
|
-
end
|
|
203
|
-
# Send next piece of work even after an error
|
|
204
|
-
send_next_work_if_available(wrapped_ractor)
|
|
205
|
-
else
|
|
206
|
-
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
|
|
207
|
-
end
|
|
208
|
-
# Update processed count for the loop condition
|
|
209
|
-
processed_count = @results.results.size + @results.errors.size
|
|
343
|
+
puts "Exiting due to shutdown signal." if @debug
|
|
344
|
+
exit!(1) # Force exit immediately
|
|
210
345
|
end
|
|
211
346
|
|
|
212
|
-
puts "Main loop finished." if ENV["FRACTOR_DEBUG"]
|
|
213
347
|
return if @continuous_mode
|
|
214
348
|
|
|
215
|
-
return unless
|
|
349
|
+
return unless @debug
|
|
216
350
|
|
|
217
351
|
puts "Final Aggregated Results: #{@results.inspect}"
|
|
218
352
|
end
|
|
219
353
|
|
|
220
354
|
# Stop the supervisor (for continuous mode)
|
|
221
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
|
+
|
|
222
362
|
@running = false
|
|
223
|
-
|
|
363
|
+
|
|
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
|
+
|
|
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
|
|
224
374
|
end
|
|
225
375
|
|
|
226
376
|
private
|
|
227
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
|
|
394
|
+
end
|
|
395
|
+
puts "Timer thread shutting down" if @debug
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
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
|
|
458
|
+
|
|
228
459
|
# Detects the number of available processors on the system.
|
|
229
460
|
# Returns the number of processors, or 2 as a fallback if detection fails.
|
|
230
461
|
def detect_num_workers
|
|
231
462
|
num_processors = Etc.nprocessors
|
|
232
|
-
puts "Auto-detected #{num_processors} available processors" if
|
|
463
|
+
puts "Auto-detected #{num_processors} available processors" if @debug
|
|
233
464
|
num_processors
|
|
234
465
|
rescue StandardError => e
|
|
235
|
-
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
|
|
236
467
|
2
|
|
237
468
|
end
|
|
238
469
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
else
|
|
262
|
-
puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || "unknown"}." if ENV["FRACTOR_DEBUG"]
|
|
263
|
-
# Remove from map if found but closed
|
|
264
|
-
@ractors_map.delete(wrapped_ractor.ractor) if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
|
|
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)
|
|
265
490
|
end
|
|
491
|
+
|
|
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
|
|
504
|
+
|
|
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
|
|
266
552
|
end
|
|
267
553
|
end
|
|
268
554
|
end
|