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,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "main_loop_handler"
|
|
4
|
+
|
|
5
|
+
module Fractor
|
|
6
|
+
# Ruby 4.0+ specific implementation of MainLoopHandler.
|
|
7
|
+
# Uses Ractor::Port for worker communication.
|
|
8
|
+
class MainLoopHandler4 < MainLoopHandler
|
|
9
|
+
# Run the main event loop for Ruby 4.0+.
|
|
10
|
+
def run_loop
|
|
11
|
+
# Build mapping of response ports to workers for message routing
|
|
12
|
+
port_to_worker = build_port_to_worker_map
|
|
13
|
+
|
|
14
|
+
loop do
|
|
15
|
+
processed_count = get_processed_count
|
|
16
|
+
|
|
17
|
+
# Check loop termination condition
|
|
18
|
+
break unless should_continue_running?(processed_count)
|
|
19
|
+
|
|
20
|
+
log_processing_status(processed_count)
|
|
21
|
+
|
|
22
|
+
active_items = get_active_items
|
|
23
|
+
|
|
24
|
+
# Check for new work from callbacks if in continuous mode
|
|
25
|
+
process_work_callbacks if continuous_mode? && !work_callbacks.empty?
|
|
26
|
+
|
|
27
|
+
# Handle edge cases - break if edge case handler indicates we should
|
|
28
|
+
next if handle_edge_cases_with_ports(active_items, port_to_worker,
|
|
29
|
+
processed_count)
|
|
30
|
+
|
|
31
|
+
# Wait for next message from any active ractor or port
|
|
32
|
+
ready_item, message = select_from_mixed(active_items, port_to_worker)
|
|
33
|
+
next unless ready_item && message
|
|
34
|
+
|
|
35
|
+
# Process the received message
|
|
36
|
+
process_message_40(ready_item, message, port_to_worker)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "Main loop finished." if @debug
|
|
40
|
+
|
|
41
|
+
# Clean up ractors map after batch mode completion
|
|
42
|
+
cleanup_ractors_map unless continuous_mode?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Clean up the ractors map after batch processing.
|
|
46
|
+
# In Ruby 4.0, we simply clear the map to allow garbage collection.
|
|
47
|
+
# The main loop already attempted to shut down workers properly.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def cleanup_ractors_map
|
|
51
|
+
return if ractors_map.empty?
|
|
52
|
+
|
|
53
|
+
puts "Cleaning up ractors map (#{ractors_map.size} entries)..." if @debug
|
|
54
|
+
|
|
55
|
+
# Simply clear the map without trying to interact with ractors
|
|
56
|
+
# The main loop already attempted to shut down workers properly
|
|
57
|
+
ractors_map.clear
|
|
58
|
+
|
|
59
|
+
# Force garbage collection to help clean up orphaned ractors
|
|
60
|
+
GC.start
|
|
61
|
+
puts "Ractors map cleared and GC forced." if @debug
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Build mapping of response ports to workers.
|
|
67
|
+
# This is needed to route messages from ports back to workers.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] Mapping of Ractor::Port => WrappedRactor
|
|
70
|
+
def build_port_to_worker_map
|
|
71
|
+
port_map = {}
|
|
72
|
+
ractors_map.each_value do |wrapped_ractor|
|
|
73
|
+
next unless wrapped_ractor.is_a?(WrappedRactor4)
|
|
74
|
+
|
|
75
|
+
port = wrapped_ractor.response_port
|
|
76
|
+
port_map[port] = wrapped_ractor if port
|
|
77
|
+
end
|
|
78
|
+
port_map
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get list of active items for Ractor.select (Ruby 4.0+).
|
|
82
|
+
# Includes both response ports and ractors (excluding wakeup ractor).
|
|
83
|
+
#
|
|
84
|
+
# @return [Array] List of Ractor::Port and Ractor objects
|
|
85
|
+
def get_active_items
|
|
86
|
+
items = []
|
|
87
|
+
|
|
88
|
+
# Add response ports from all workers
|
|
89
|
+
ractors_map.each_value do |wrapped_ractor|
|
|
90
|
+
next unless wrapped_ractor.is_a?(WrappedRactor4)
|
|
91
|
+
|
|
92
|
+
port = wrapped_ractor.response_port
|
|
93
|
+
items << port if port
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Add wakeup ractor/port if in continuous mode with callbacks
|
|
97
|
+
if continuous_mode? && !work_callbacks.empty? && wakeup_ractor && wakeup_port
|
|
98
|
+
items << wakeup_port
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
items
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get list of active ractors for compatibility with Ruby 3.x tests.
|
|
105
|
+
# In Ruby 4.0, returns the actual ractor objects (not ports).
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<Ractor>] List of active Ractor objects
|
|
108
|
+
def get_active_ractors
|
|
109
|
+
ractors_map.keys.reject do |ractor|
|
|
110
|
+
ractor == wakeup_ractor && !(continuous_mode? && !work_callbacks.empty?)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check for new work from callbacks in continuous mode.
|
|
115
|
+
#
|
|
116
|
+
# @return [void]
|
|
117
|
+
def process_work_callbacks
|
|
118
|
+
work_callbacks.each do |callback|
|
|
119
|
+
new_work = callback.call
|
|
120
|
+
if new_work && !new_work.empty?
|
|
121
|
+
@supervisor.add_work_items(new_work)
|
|
122
|
+
puts "Work source provided #{new_work.size} new items" if @debug
|
|
123
|
+
|
|
124
|
+
# Distribute work to idle workers
|
|
125
|
+
distributed = work_distribution_manager.distribute_to_idle_workers
|
|
126
|
+
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Handle edge cases like no active workers or empty queue.
|
|
132
|
+
# Overload for compatibility with tests (2-argument version).
|
|
133
|
+
#
|
|
134
|
+
# @param active_ractors [Array<Ractor>] List of active ractors
|
|
135
|
+
# @param processed_count [Integer] Current number of processed items
|
|
136
|
+
# @return [Boolean] true if should break from loop
|
|
137
|
+
def handle_edge_cases(active_ractors, processed_count)
|
|
138
|
+
# For Ruby 4.0 compatibility with tests:
|
|
139
|
+
# Use the active_ractors array directly since tests pass it in
|
|
140
|
+
# In normal operation, this would be derived from ractors_map
|
|
141
|
+
|
|
142
|
+
# In continuous mode, if no active ractors and shutting down, exit loop
|
|
143
|
+
if active_ractors.empty? && @shutting_down
|
|
144
|
+
puts "No active ractors during shutdown, exiting main loop" if @debug
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Break if no active ractors and queue is empty, but work remains
|
|
149
|
+
if active_ractors.empty? && work_queue.empty? && !continuous_mode? && processed_count < total_work_count
|
|
150
|
+
puts "Warning: No active ractors and queue is empty, but not all work is processed. Exiting loop." if @debug
|
|
151
|
+
return true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# In continuous mode, just wait if no active ractors but keep running
|
|
155
|
+
if active_ractors.empty?
|
|
156
|
+
return true unless continuous_mode?
|
|
157
|
+
|
|
158
|
+
sleep(0.1) # Small delay to avoid CPU spinning
|
|
159
|
+
return false # Continue to next iteration
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
false # There are active ractors, continue the loop
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Handle edge cases like no active workers or empty queue (3-argument version).
|
|
166
|
+
#
|
|
167
|
+
# @param _active_items [Array] List of active ports/ractors
|
|
168
|
+
# @param port_to_worker [Hash] Mapping of ports to workers
|
|
169
|
+
# @param processed_count [Integer] Current number of processed items
|
|
170
|
+
# @return [Boolean] true if should break from loop
|
|
171
|
+
def handle_edge_cases_with_ports(_active_items, port_to_worker,
|
|
172
|
+
processed_count)
|
|
173
|
+
# Count active workers (those with response ports)
|
|
174
|
+
active_worker_count = port_to_worker.size
|
|
175
|
+
|
|
176
|
+
# In continuous mode, if no active workers and shutting down, exit loop
|
|
177
|
+
if active_worker_count.zero? && @shutting_down
|
|
178
|
+
puts "No active workers during shutdown, exiting main loop" if @debug
|
|
179
|
+
return true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Break if no active workers and queue is empty, but work remains
|
|
183
|
+
if active_worker_count.zero? && work_queue.empty? && !continuous_mode? && processed_count < total_work_count
|
|
184
|
+
puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if @debug
|
|
185
|
+
return true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# In continuous mode, just wait if no active workers but keep running
|
|
189
|
+
if active_worker_count.zero?
|
|
190
|
+
return true unless continuous_mode?
|
|
191
|
+
|
|
192
|
+
sleep(0.1) # Small delay to avoid CPU spinning
|
|
193
|
+
return false # Continue to next iteration
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Wait for a message from any active ractor or port (Ruby 4.0+).
|
|
200
|
+
# In Ruby 4.0, we select from a mix of response ports and ractors.
|
|
201
|
+
#
|
|
202
|
+
# @param active_items [Array] List of active ports/ractors
|
|
203
|
+
# @param port_to_worker [Hash] Mapping of ports to workers
|
|
204
|
+
# @return [Array] ready_item and message, or nil if should continue
|
|
205
|
+
def select_from_mixed(active_items, port_to_worker)
|
|
206
|
+
# In Ruby 4.0, we use Ractor.select on ports (and potentially ractors)
|
|
207
|
+
# The response ports receive :result and :error messages
|
|
208
|
+
# The wakeup ractor (if present) receives wakeup signals
|
|
209
|
+
|
|
210
|
+
return nil, nil if active_items.empty?
|
|
211
|
+
|
|
212
|
+
ready_item, message = Ractor.select(*active_items)
|
|
213
|
+
|
|
214
|
+
# Check if this is the wakeup port
|
|
215
|
+
if ready_item == wakeup_port
|
|
216
|
+
puts "Wakeup signal received: #{message[:message]}" if @debug
|
|
217
|
+
# Remove wakeup ractor from map if shutting down
|
|
218
|
+
if message && message[:message] == :shutdown
|
|
219
|
+
ractors_map.delete(wakeup_ractor)
|
|
220
|
+
@supervisor.instance_variable_set(:@wakeup_ractor, nil)
|
|
221
|
+
end
|
|
222
|
+
# Return nil to indicate we should continue to next iteration
|
|
223
|
+
return nil, nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
[ready_item, message]
|
|
227
|
+
rescue Ractor::ClosedError, Ractor::Error => e
|
|
228
|
+
# Handle closed ports/ractors - remove them from ractors_map
|
|
229
|
+
puts "Ractor::Error in select: #{e.message}. Cleaning up closed ports." if @debug
|
|
230
|
+
|
|
231
|
+
# Find and remove workers with closed ports
|
|
232
|
+
closed_ports = active_items.select { |item| item.is_a?(Ractor::Port) }
|
|
233
|
+
closed_ports.each do |port|
|
|
234
|
+
wrapped_ractor = port_to_worker[port]
|
|
235
|
+
if wrapped_ractor
|
|
236
|
+
puts "Removing worker with closed port: #{wrapped_ractor.name}" if @debug
|
|
237
|
+
ractors_map.delete(wrapped_ractor.ractor)
|
|
238
|
+
workers.delete(wrapped_ractor)
|
|
239
|
+
port_to_worker.delete(port)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Return nil to continue the loop with updated active_items
|
|
244
|
+
[nil, nil]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Process a message from a ractor or port (Ruby 4.0+).
|
|
248
|
+
# Most messages come through response ports in Ruby 4.0.
|
|
249
|
+
#
|
|
250
|
+
# @param ready_item [Ractor::Port, Ractor] The port or ractor that sent the message
|
|
251
|
+
# @param message [Hash] The message received
|
|
252
|
+
# @param port_to_worker [Hash] Mapping of ports to workers
|
|
253
|
+
# @return [void]
|
|
254
|
+
def process_message_40(ready_item, message, port_to_worker)
|
|
255
|
+
# Find the corresponding WrappedRactor instance
|
|
256
|
+
if ready_item.is_a?(Ractor::Port)
|
|
257
|
+
# Message from a response port - look up worker
|
|
258
|
+
wrapped_ractor = port_to_worker[ready_item]
|
|
259
|
+
unless wrapped_ractor
|
|
260
|
+
puts "Warning: Received message from unknown port: #{ready_item}. Ignoring." if @debug
|
|
261
|
+
return
|
|
262
|
+
end
|
|
263
|
+
else
|
|
264
|
+
# Message from a ractor (e.g., initialize, shutdown acknowledgment)
|
|
265
|
+
wrapped_ractor = ractors_map[ready_item]
|
|
266
|
+
unless wrapped_ractor
|
|
267
|
+
puts "Warning: Received message from unknown Ractor: #{ready_item}. Ignoring." if @debug
|
|
268
|
+
ractors_map.delete(ready_item)
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Guard against nil messages (indicates closed port/ractor)
|
|
274
|
+
if message.nil?
|
|
275
|
+
puts "Warning: Received nil message from #{wrapped_ractor.name}. Port/Ractor likely closed." if @debug
|
|
276
|
+
ractors_map.delete(wrapped_ractor.ractor)
|
|
277
|
+
workers.delete(wrapped_ractor)
|
|
278
|
+
port_to_worker.delete(ready_item) if ready_item.is_a?(Ractor::Port)
|
|
279
|
+
return
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
puts "Selected from: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if @debug
|
|
283
|
+
|
|
284
|
+
# Route to appropriate message handler
|
|
285
|
+
case message[:type]
|
|
286
|
+
when :initialize
|
|
287
|
+
handle_initialize_message(wrapped_ractor)
|
|
288
|
+
when :shutdown
|
|
289
|
+
handle_shutdown_message(wrapped_ractor.ractor, wrapped_ractor)
|
|
290
|
+
when :result
|
|
291
|
+
handle_result_message(wrapped_ractor, message)
|
|
292
|
+
when :error
|
|
293
|
+
handle_error_message(wrapped_ractor, message)
|
|
294
|
+
else
|
|
295
|
+
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if @debug
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Internal metrics collector for performance monitoring.
|
|
5
|
+
# Thread-safe collection of performance metrics.
|
|
6
|
+
class PerformanceMetricsCollector
|
|
7
|
+
attr_reader :jobs_processed, :jobs_succeeded, :jobs_failed, :total_latency
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
reset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Reset all metrics to initial state
|
|
14
|
+
def reset
|
|
15
|
+
@jobs_processed = 0
|
|
16
|
+
@jobs_succeeded = 0
|
|
17
|
+
@jobs_failed = 0
|
|
18
|
+
@latencies = []
|
|
19
|
+
@total_latency = 0.0
|
|
20
|
+
@queue_depths = []
|
|
21
|
+
@memory_samples = []
|
|
22
|
+
@utilization_samples = []
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Record a job completion with its latency
|
|
27
|
+
#
|
|
28
|
+
# @param latency [Float] Job latency in seconds
|
|
29
|
+
# @param success [Boolean] Whether job succeeded
|
|
30
|
+
# @return [void]
|
|
31
|
+
def record_job(latency, success: true)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@jobs_processed += 1
|
|
34
|
+
@jobs_succeeded += 1 if success
|
|
35
|
+
@jobs_failed += 1 unless success
|
|
36
|
+
@latencies << latency
|
|
37
|
+
@total_latency += latency
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Sample the current queue depth
|
|
42
|
+
#
|
|
43
|
+
# @param depth [Integer] Current queue depth
|
|
44
|
+
# @return [void]
|
|
45
|
+
def sample_queue_depth(depth)
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
@queue_depths << depth
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sample current memory usage
|
|
52
|
+
#
|
|
53
|
+
# @param mb [Float] Memory usage in MB
|
|
54
|
+
# @return [void]
|
|
55
|
+
def sample_memory(mb)
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@memory_samples << mb
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Sample worker utilization ratio
|
|
62
|
+
#
|
|
63
|
+
# @param ratio [Float] Worker utilization (0.0 to 1.0)
|
|
64
|
+
# @return [void]
|
|
65
|
+
def sample_worker_utilization(ratio)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@utilization_samples << ratio
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Calculate average latency
|
|
72
|
+
#
|
|
73
|
+
# @return [Float] Average latency in seconds
|
|
74
|
+
def average_latency
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
average_latency_unsynchronized
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Calculate latency percentile
|
|
81
|
+
#
|
|
82
|
+
# @param p [Integer] Percentile (0-100)
|
|
83
|
+
# @return [Float] Latency at percentile in seconds
|
|
84
|
+
def percentile(p)
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
return 0.0 if @latencies.empty?
|
|
87
|
+
|
|
88
|
+
sorted = @latencies.sort
|
|
89
|
+
index = ((p / 100.0) * sorted.size).ceil - 1
|
|
90
|
+
sorted[[index, 0].max]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Calculate average queue depth
|
|
95
|
+
#
|
|
96
|
+
# @return [Float] Average queue depth
|
|
97
|
+
def average_queue_depth
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
return 0.0 if @queue_depths.empty?
|
|
100
|
+
|
|
101
|
+
@queue_depths.sum / @queue_depths.size.to_f
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get maximum queue depth observed
|
|
106
|
+
#
|
|
107
|
+
# @return [Integer] Maximum queue depth
|
|
108
|
+
def max_queue_depth
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
return 0 if @queue_depths.empty?
|
|
111
|
+
|
|
112
|
+
@queue_depths.max
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Calculate enqueue rate (jobs per second)
|
|
117
|
+
#
|
|
118
|
+
# @param duration [Float] Time period in seconds
|
|
119
|
+
# @return [Float] Enqueue rate
|
|
120
|
+
def enqueue_rate(duration)
|
|
121
|
+
return 0.0 if duration <= 0
|
|
122
|
+
|
|
123
|
+
@jobs_processed / duration.to_f
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Calculate dequeue rate (jobs per second)
|
|
127
|
+
#
|
|
128
|
+
# @param duration [Float] Time period in seconds
|
|
129
|
+
# @return [Float] Dequeue rate
|
|
130
|
+
def dequeue_rate(duration)
|
|
131
|
+
return 0.0 if duration <= 0
|
|
132
|
+
|
|
133
|
+
@jobs_processed / duration.to_f
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Calculate average wait time using Little's Law
|
|
137
|
+
#
|
|
138
|
+
# @return [Float] Average wait time in seconds
|
|
139
|
+
def average_wait_time
|
|
140
|
+
# Wait time approximation based on queue depth and throughput
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
return 0.0 if @queue_depths.empty? || @latencies.empty?
|
|
143
|
+
|
|
144
|
+
avg_depth = @queue_depths.sum / @queue_depths.size.to_f
|
|
145
|
+
avg_lat = @total_latency / @latencies.size
|
|
146
|
+
return 0.0 if avg_lat.zero?
|
|
147
|
+
|
|
148
|
+
# Little's Law: Wait Time ≈ Queue Length / Throughput
|
|
149
|
+
avg_depth * avg_lat
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Calculate wait time at a given percentile
|
|
154
|
+
#
|
|
155
|
+
# @param p [Integer] Percentile (0-100)
|
|
156
|
+
# @return [Float] Wait time at percentile in seconds
|
|
157
|
+
def wait_time_percentile(p)
|
|
158
|
+
# Simplified wait time percentile based on queue depth percentile
|
|
159
|
+
@mutex.synchronize do
|
|
160
|
+
return 0.0 if @queue_depths.empty?
|
|
161
|
+
|
|
162
|
+
sorted = @queue_depths.sort
|
|
163
|
+
index = ((p / 100.0) * sorted.size).ceil - 1
|
|
164
|
+
depth_percentile = sorted[[index, 0].max]
|
|
165
|
+
|
|
166
|
+
avg_lat = @total_latency / @latencies.size
|
|
167
|
+
return 0.0 if avg_lat.zero?
|
|
168
|
+
|
|
169
|
+
depth_percentile * avg_lat
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def average_latency_unsynchronized
|
|
176
|
+
return 0.0 if @latencies.empty?
|
|
177
|
+
|
|
178
|
+
@total_latency / @latencies.size
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "performance_metrics_collector"
|
|
4
|
+
require_relative "performance_report_generator"
|
|
5
|
+
|
|
6
|
+
module Fractor
|
|
7
|
+
# Monitors and tracks performance metrics for Fractor supervisors and workers.
|
|
8
|
+
#
|
|
9
|
+
# Collects metrics including:
|
|
10
|
+
# - Jobs processed count
|
|
11
|
+
# - Latency statistics (average, p50, p95, p99)
|
|
12
|
+
# - Throughput (jobs/second)
|
|
13
|
+
# - Worker utilization
|
|
14
|
+
# - Queue depth over time
|
|
15
|
+
# - Memory usage
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# supervisor = Fractor::Supervisor.new(...)
|
|
19
|
+
# monitor = Fractor::PerformanceMonitor.new(supervisor)
|
|
20
|
+
# monitor.start
|
|
21
|
+
#
|
|
22
|
+
# # ... run workload ...
|
|
23
|
+
#
|
|
24
|
+
# monitor.stop
|
|
25
|
+
# puts monitor.report
|
|
26
|
+
#
|
|
27
|
+
# @example With custom sampling interval
|
|
28
|
+
# monitor = Fractor::PerformanceMonitor.new(
|
|
29
|
+
# supervisor,
|
|
30
|
+
# sample_interval: 0.5 # Sample every 500ms
|
|
31
|
+
# )
|
|
32
|
+
class PerformanceMonitor
|
|
33
|
+
attr_reader :supervisor, :metrics, :start_time, :end_time
|
|
34
|
+
|
|
35
|
+
# Create a new performance monitor
|
|
36
|
+
#
|
|
37
|
+
# @param supervisor [Supervisor] The supervisor to monitor
|
|
38
|
+
# @param sample_interval [Float] How often to sample metrics (seconds)
|
|
39
|
+
def initialize(supervisor, sample_interval: 1.0)
|
|
40
|
+
@supervisor = supervisor
|
|
41
|
+
@sample_interval = sample_interval
|
|
42
|
+
@metrics = PerformanceMetricsCollector.new
|
|
43
|
+
@start_time = nil
|
|
44
|
+
@end_time = nil
|
|
45
|
+
@monitoring = false
|
|
46
|
+
@monitor_thread = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Start monitoring
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def start
|
|
53
|
+
return if @monitoring
|
|
54
|
+
|
|
55
|
+
@monitoring = true
|
|
56
|
+
@start_time = Time.now
|
|
57
|
+
@metrics.reset
|
|
58
|
+
|
|
59
|
+
# Start background monitoring thread
|
|
60
|
+
@monitor_thread = Thread.new { monitor_loop }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Stop monitoring
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
def stop
|
|
67
|
+
return unless @monitoring
|
|
68
|
+
|
|
69
|
+
@monitoring = false
|
|
70
|
+
@end_time = Time.now
|
|
71
|
+
@monitor_thread&.join
|
|
72
|
+
@monitor_thread = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if currently monitoring
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def monitoring?
|
|
79
|
+
@monitoring
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get current metrics snapshot
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash] Current metrics
|
|
85
|
+
def snapshot
|
|
86
|
+
{
|
|
87
|
+
jobs_processed: @metrics.jobs_processed,
|
|
88
|
+
jobs_succeeded: @metrics.jobs_succeeded,
|
|
89
|
+
jobs_failed: @metrics.jobs_failed,
|
|
90
|
+
average_latency: @metrics.average_latency,
|
|
91
|
+
p50_latency: @metrics.percentile(50),
|
|
92
|
+
p95_latency: @metrics.percentile(95),
|
|
93
|
+
p99_latency: @metrics.percentile(99),
|
|
94
|
+
throughput: calculate_throughput,
|
|
95
|
+
queue_depth: current_queue_depth,
|
|
96
|
+
queue_depth_avg: @metrics.average_queue_depth,
|
|
97
|
+
queue_depth_max: @metrics.max_queue_depth,
|
|
98
|
+
enqueue_rate: @metrics.enqueue_rate(uptime),
|
|
99
|
+
dequeue_rate: @metrics.dequeue_rate(uptime),
|
|
100
|
+
average_wait_time: @metrics.average_wait_time,
|
|
101
|
+
p50_wait_time: @metrics.wait_time_percentile(50),
|
|
102
|
+
p95_wait_time: @metrics.wait_time_percentile(95),
|
|
103
|
+
p99_wait_time: @metrics.wait_time_percentile(99),
|
|
104
|
+
worker_count: worker_count,
|
|
105
|
+
active_workers: active_worker_count,
|
|
106
|
+
worker_utilization: worker_utilization,
|
|
107
|
+
memory_mb: current_memory_mb,
|
|
108
|
+
uptime: uptime,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Generate a human-readable report
|
|
113
|
+
#
|
|
114
|
+
# @return [String] Formatted report
|
|
115
|
+
def report
|
|
116
|
+
PerformanceReportGenerator.generate_report(snapshot)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Export metrics in JSON format
|
|
120
|
+
#
|
|
121
|
+
# @return [String] JSON representation
|
|
122
|
+
def to_json(*_args)
|
|
123
|
+
PerformanceReportGenerator.to_json(snapshot)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Export metrics in Prometheus format
|
|
127
|
+
#
|
|
128
|
+
# @return [String] Prometheus metrics
|
|
129
|
+
def to_prometheus
|
|
130
|
+
stats = snapshot
|
|
131
|
+
PerformanceReportGenerator.to_prometheus(stats, @metrics.total_latency)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Record a job completion
|
|
135
|
+
#
|
|
136
|
+
# @param latency [Float] Job latency in seconds
|
|
137
|
+
# @param success [Boolean] Whether job succeeded
|
|
138
|
+
# @return [void]
|
|
139
|
+
def record_job(latency, success: true)
|
|
140
|
+
@metrics.record_job(latency, success: success)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def monitor_loop
|
|
146
|
+
while @monitoring
|
|
147
|
+
sample_metrics
|
|
148
|
+
sleep(@sample_interval)
|
|
149
|
+
end
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
warn "Performance monitor error: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def sample_metrics
|
|
155
|
+
@metrics.sample_queue_depth(current_queue_depth)
|
|
156
|
+
@metrics.sample_memory(current_memory_mb)
|
|
157
|
+
@metrics.sample_worker_utilization(worker_utilization)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def calculate_throughput
|
|
161
|
+
duration = uptime
|
|
162
|
+
return 0.0 if duration <= 0
|
|
163
|
+
|
|
164
|
+
@metrics.jobs_processed / duration.to_f
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def uptime
|
|
168
|
+
end_time = @end_time || Time.now
|
|
169
|
+
return 0 unless @start_time
|
|
170
|
+
|
|
171
|
+
end_time - @start_time
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def current_queue_depth
|
|
175
|
+
@supervisor.work_queue.size
|
|
176
|
+
rescue StandardError
|
|
177
|
+
0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def worker_count
|
|
181
|
+
@supervisor.worker_pools.sum { |pool| pool[:num_workers] || 1 }
|
|
182
|
+
rescue StandardError
|
|
183
|
+
0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def active_worker_count
|
|
187
|
+
# This would need worker state tracking
|
|
188
|
+
# For now, estimate based on queue depth
|
|
189
|
+
depth = current_queue_depth
|
|
190
|
+
total = worker_count
|
|
191
|
+
return total if depth.positive?
|
|
192
|
+
|
|
193
|
+
0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def worker_utilization
|
|
197
|
+
total = worker_count
|
|
198
|
+
return 0.0 if total.zero?
|
|
199
|
+
|
|
200
|
+
active = active_worker_count
|
|
201
|
+
active.to_f / total
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def current_memory_mb
|
|
205
|
+
# Get current process memory usage in MB
|
|
206
|
+
if RUBY_PLATFORM.match?(/darwin|linux/)
|
|
207
|
+
`ps -o rss= -p #{Process.pid}`.to_i / 1024.0
|
|
208
|
+
else
|
|
209
|
+
0.0 # Unsupported platform
|
|
210
|
+
end
|
|
211
|
+
rescue StandardError
|
|
212
|
+
0.0
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|