fractor 0.1.9 → 0.1.10
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 +28 -91
- data/docs/ARCHITECTURE.md +317 -0
- data/docs/PERFORMANCE_TUNING.md +355 -0
- data/docs/TROUBLESHOOTING.md +463 -0
- data/lib/fractor/callback_registry.rb +106 -0
- data/lib/fractor/config_schema.rb +170 -0
- data/lib/fractor/main_loop_handler.rb +4 -8
- data/lib/fractor/main_loop_handler3.rb +10 -12
- data/lib/fractor/main_loop_handler4.rb +48 -20
- data/lib/fractor/result_cache.rb +58 -10
- data/lib/fractor/shutdown_handler.rb +12 -6
- data/lib/fractor/supervisor.rb +100 -13
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/workflow/execution/dependency_resolver.rb +149 -0
- data/lib/fractor/workflow/execution/fallback_job_handler.rb +68 -0
- data/lib/fractor/workflow/execution/job_executor.rb +242 -0
- data/lib/fractor/workflow/execution/result_builder.rb +76 -0
- data/lib/fractor/workflow/execution/workflow_execution_logger.rb +241 -0
- data/lib/fractor/workflow/workflow_executor.rb +97 -476
- data/lib/fractor/wrapped_ractor.rb +2 -4
- data/lib/fractor.rb +11 -0
- metadata +12 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Configuration schema validation module.
|
|
5
|
+
#
|
|
6
|
+
# Provides declarative configuration schemas with validation.
|
|
7
|
+
# Useful for validating configuration options at initialization time.
|
|
8
|
+
#
|
|
9
|
+
# @example Define a schema
|
|
10
|
+
# class MyConfig
|
|
11
|
+
# extend Fractor::ConfigSchema
|
|
12
|
+
#
|
|
13
|
+
# schema :worker_pools do
|
|
14
|
+
# type Array
|
|
15
|
+
# default []
|
|
16
|
+
# description "Array of worker pool configurations"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# schema :continuous_mode do
|
|
20
|
+
# type :boolean
|
|
21
|
+
# default false
|
|
22
|
+
# description "Whether to run in continuous mode"
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Validate configuration
|
|
27
|
+
# MyConfig.validate!(worker_pools: [...], continuous_mode: true)
|
|
28
|
+
module ConfigSchema
|
|
29
|
+
# Schema entry definition
|
|
30
|
+
class SchemaEntry
|
|
31
|
+
attr_reader :name, :type, :default, :optional, :description
|
|
32
|
+
|
|
33
|
+
def initialize(name, **options)
|
|
34
|
+
@name = name
|
|
35
|
+
@type = options[:type]
|
|
36
|
+
@default = options[:default]
|
|
37
|
+
@optional = options.fetch(:optional, true)
|
|
38
|
+
@description = options[:description]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Validate a value against this schema entry
|
|
42
|
+
# @return [Array<String>] Array of error messages (empty if valid)
|
|
43
|
+
def validate(value)
|
|
44
|
+
errors = []
|
|
45
|
+
|
|
46
|
+
# Check nil for optional fields
|
|
47
|
+
if value.nil?
|
|
48
|
+
errors << "#{name} is required" unless @optional
|
|
49
|
+
return errors
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Type validation
|
|
53
|
+
if @type && !type_matches?(value)
|
|
54
|
+
errors << "#{name} must be of type #{type_description}, got #{value.class}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
errors
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def type_matches?(value)
|
|
63
|
+
case @type
|
|
64
|
+
when :boolean
|
|
65
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
66
|
+
when Class
|
|
67
|
+
value.is_a?(@type)
|
|
68
|
+
when Array
|
|
69
|
+
@type.any? { |t| value.is_a?(t) }
|
|
70
|
+
else
|
|
71
|
+
true # No type constraint
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def type_description
|
|
76
|
+
case @type
|
|
77
|
+
when :boolean
|
|
78
|
+
"boolean"
|
|
79
|
+
when Class
|
|
80
|
+
@type.name
|
|
81
|
+
when Array
|
|
82
|
+
@type.map { |t| t.is_a?(Class) ? t.name : t.to_s }.join(" or ")
|
|
83
|
+
else
|
|
84
|
+
"any"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Module-level methods for defining schemas
|
|
90
|
+
def self.extended(base)
|
|
91
|
+
base.instance_variable_set(:@schema_entries, {})
|
|
92
|
+
base.singleton_class.prepend(SchemaClassMethods)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module SchemaClassMethods
|
|
96
|
+
# Define a configuration schema entry
|
|
97
|
+
# @param name [Symbol] Configuration key name
|
|
98
|
+
# @param options [Hash] Schema options (type, default, optional, description)
|
|
99
|
+
def schema(name, **options)
|
|
100
|
+
@schema_entries ||= {}
|
|
101
|
+
@schema_entries[name] = SchemaEntry.new(name, **options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get all schema entries
|
|
105
|
+
# @return [Hash] Schema entries by name
|
|
106
|
+
def schema_entries
|
|
107
|
+
@schema_entries || {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Validate configuration against schema
|
|
111
|
+
# @param config [Hash] Configuration to validate
|
|
112
|
+
# @raise [ArgumentError] If configuration is invalid
|
|
113
|
+
# @return [Hash] Validated and normalized configuration
|
|
114
|
+
def validate!(config = {})
|
|
115
|
+
all_errors = []
|
|
116
|
+
|
|
117
|
+
schema_entries.each do |name, entry|
|
|
118
|
+
value = config.fetch(name, entry.default)
|
|
119
|
+
errors = entry.validate(value)
|
|
120
|
+
all_errors.concat(errors.map { |e| "- #{e}" })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check for unknown keys
|
|
124
|
+
unknown_keys = config.keys - schema_entries.keys.symbolize_keys
|
|
125
|
+
unknown_keys.each do |key|
|
|
126
|
+
all_errors << "- Unknown configuration option: #{key}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unless all_errors.empty?
|
|
130
|
+
raise ArgumentError,
|
|
131
|
+
"Invalid configuration:\n#{all_errors.join("\n")}\n\n" \
|
|
132
|
+
"Valid options:\n#{schema_help}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
config
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Generate schema help text
|
|
139
|
+
# @return [String] Help text describing all schema entries
|
|
140
|
+
def schema_help
|
|
141
|
+
lines = []
|
|
142
|
+
schema_entries.each_value do |entry|
|
|
143
|
+
type_desc = case entry.type
|
|
144
|
+
when :boolean then "boolean"
|
|
145
|
+
when Class then entry.type.name
|
|
146
|
+
when Array then entry.type.map(&:name).join(" | ")
|
|
147
|
+
else "any"
|
|
148
|
+
end
|
|
149
|
+
default_desc = entry.optional ? " (default: #{entry.default.inspect})" : " (required)"
|
|
150
|
+
desc = entry.description ? " - #{entry.description}" : ""
|
|
151
|
+
lines << " #{entry.name}: #{type_desc}#{default_desc}#{desc}"
|
|
152
|
+
end
|
|
153
|
+
lines.join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get schema as a hash (for documentation purposes)
|
|
157
|
+
# @return [Hash] Schema definition
|
|
158
|
+
def schema_definition
|
|
159
|
+
schema_entries.transform_values do |entry|
|
|
160
|
+
{
|
|
161
|
+
type: entry.type,
|
|
162
|
+
default: entry.default,
|
|
163
|
+
optional: entry.optional,
|
|
164
|
+
description: entry.description,
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -20,8 +20,7 @@ module Fractor
|
|
|
20
20
|
# @param debug [Boolean] Whether debug mode is enabled
|
|
21
21
|
# @return [MainLoopHandler] The appropriate subclass instance
|
|
22
22
|
def self.create(supervisor, debug: false)
|
|
23
|
-
|
|
24
|
-
if ruby_4_0
|
|
23
|
+
if Fractor::RUBY_4_0_OR_HIGHER
|
|
25
24
|
MainLoopHandler4.new(supervisor, debug: debug)
|
|
26
25
|
else
|
|
27
26
|
MainLoopHandler3.new(supervisor, debug: debug)
|
|
@@ -350,11 +349,11 @@ module Fractor
|
|
|
350
349
|
end
|
|
351
350
|
|
|
352
351
|
def work_callbacks
|
|
353
|
-
@supervisor.
|
|
352
|
+
@supervisor.callback_registry.work_callbacks
|
|
354
353
|
end
|
|
355
354
|
|
|
356
355
|
def error_callbacks
|
|
357
|
-
@supervisor.
|
|
356
|
+
@supervisor.callback_registry.error_callbacks
|
|
358
357
|
end
|
|
359
358
|
|
|
360
359
|
# Check if running on Windows with Ruby 3.4
|
|
@@ -362,10 +361,7 @@ module Fractor
|
|
|
362
361
|
#
|
|
363
362
|
# @return [Boolean]
|
|
364
363
|
def windows_ruby_34?
|
|
365
|
-
|
|
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")
|
|
364
|
+
Fractor::WINDOWS_RUBY_34
|
|
369
365
|
end
|
|
370
366
|
|
|
371
367
|
# Handle a stuck ractor by identifying and removing it from the active pool
|
|
@@ -19,7 +19,7 @@ module Fractor
|
|
|
19
19
|
active_ractors = get_active_ractors
|
|
20
20
|
|
|
21
21
|
# Check for new work from callbacks if in continuous mode
|
|
22
|
-
process_work_callbacks if continuous_mode? &&
|
|
22
|
+
process_work_callbacks if continuous_mode? && @supervisor.callback_registry.has_work_callbacks?
|
|
23
23
|
|
|
24
24
|
# Handle edge cases - break if edge case handler indicates we should
|
|
25
25
|
next if handle_edge_cases(active_ractors, processed_count)
|
|
@@ -46,7 +46,7 @@ module Fractor
|
|
|
46
46
|
# @return [Array<Ractor>]
|
|
47
47
|
def get_active_ractors
|
|
48
48
|
ractors_map.keys.reject do |ractor|
|
|
49
|
-
ractor == wakeup_ractor && !(continuous_mode? &&
|
|
49
|
+
ractor == wakeup_ractor && !(continuous_mode? && @supervisor.callback_registry.has_work_callbacks?)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -54,16 +54,14 @@ module Fractor
|
|
|
54
54
|
#
|
|
55
55
|
# @return [void]
|
|
56
56
|
def process_work_callbacks
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
66
|
-
end
|
|
57
|
+
new_work = @supervisor.callback_registry.process_work_callbacks
|
|
58
|
+
if new_work && !new_work.empty?
|
|
59
|
+
@supervisor.add_work_items(new_work)
|
|
60
|
+
puts "Work source provided #{new_work.size} new items" if @debug
|
|
61
|
+
|
|
62
|
+
# Distribute work to idle workers
|
|
63
|
+
distributed = work_distribution_manager.distribute_to_idle_workers
|
|
64
|
+
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
67
65
|
end
|
|
68
66
|
end
|
|
69
67
|
|
|
@@ -8,10 +8,11 @@ module Fractor
|
|
|
8
8
|
class MainLoopHandler4 < MainLoopHandler
|
|
9
9
|
# Run the main event loop for Ruby 4.0+.
|
|
10
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
11
|
loop do
|
|
12
|
+
# Build mapping of response ports to workers for message routing
|
|
13
|
+
# REBUILD ON EACH ITERATION to handle terminated workers
|
|
14
|
+
port_to_worker = build_port_to_worker_map
|
|
15
|
+
|
|
15
16
|
processed_count = get_processed_count
|
|
16
17
|
|
|
17
18
|
# Check loop termination condition
|
|
@@ -22,7 +23,7 @@ module Fractor
|
|
|
22
23
|
active_items = get_active_items
|
|
23
24
|
|
|
24
25
|
# Check for new work from callbacks if in continuous mode
|
|
25
|
-
process_work_callbacks if continuous_mode? &&
|
|
26
|
+
process_work_callbacks if continuous_mode? && @supervisor.callback_registry.has_work_callbacks?
|
|
26
27
|
|
|
27
28
|
# Handle edge cases - break if edge case handler indicates we should
|
|
28
29
|
next if handle_edge_cases_with_ports(active_items, port_to_worker,
|
|
@@ -65,6 +66,7 @@ module Fractor
|
|
|
65
66
|
|
|
66
67
|
# Build mapping of response ports to workers.
|
|
67
68
|
# This is needed to route messages from ports back to workers.
|
|
69
|
+
# Skips workers whose ractors have terminated to avoid blocking on closed ports.
|
|
68
70
|
#
|
|
69
71
|
# @return [Hash] Mapping of Ractor::Port => WrappedRactor
|
|
70
72
|
def build_port_to_worker_map
|
|
@@ -72,6 +74,9 @@ module Fractor
|
|
|
72
74
|
ractors_map.each_value do |wrapped_ractor|
|
|
73
75
|
next unless wrapped_ractor.is_a?(WrappedRactor4)
|
|
74
76
|
|
|
77
|
+
# Skip workers whose ractors have terminated
|
|
78
|
+
next if wrapped_ractor.closed?
|
|
79
|
+
|
|
75
80
|
port = wrapped_ractor.response_port
|
|
76
81
|
port_map[port] = wrapped_ractor if port
|
|
77
82
|
end
|
|
@@ -80,6 +85,7 @@ module Fractor
|
|
|
80
85
|
|
|
81
86
|
# Get list of active items for Ractor.select (Ruby 4.0+).
|
|
82
87
|
# Includes both response ports and ractors (excluding wakeup ractor).
|
|
88
|
+
# Skips workers whose ractors have terminated to avoid blocking on closed ports.
|
|
83
89
|
#
|
|
84
90
|
# @return [Array] List of Ractor::Port and Ractor objects
|
|
85
91
|
def get_active_items
|
|
@@ -89,12 +95,17 @@ module Fractor
|
|
|
89
95
|
ractors_map.each_value do |wrapped_ractor|
|
|
90
96
|
next unless wrapped_ractor.is_a?(WrappedRactor4)
|
|
91
97
|
|
|
98
|
+
# Skip workers whose ractors have terminated
|
|
99
|
+
next if wrapped_ractor.closed?
|
|
100
|
+
|
|
92
101
|
port = wrapped_ractor.response_port
|
|
93
102
|
items << port if port
|
|
94
103
|
end
|
|
95
104
|
|
|
96
|
-
# Add wakeup ractor/port if in continuous mode
|
|
97
|
-
|
|
105
|
+
# Add wakeup ractor/port if in continuous mode
|
|
106
|
+
# CRITICAL: Always include wakeup port in continuous mode to ensure
|
|
107
|
+
# the main loop can be unblocked for shutdown, even without callbacks.
|
|
108
|
+
if continuous_mode? && wakeup_ractor && wakeup_port
|
|
98
109
|
items << wakeup_port
|
|
99
110
|
end
|
|
100
111
|
|
|
@@ -106,8 +117,10 @@ module Fractor
|
|
|
106
117
|
#
|
|
107
118
|
# @return [Array<Ractor>] List of active Ractor objects
|
|
108
119
|
def get_active_ractors
|
|
120
|
+
# CRITICAL: Always include wakeup ractor in continuous mode to ensure
|
|
121
|
+
# the main loop can be unblocked for shutdown, even without callbacks.
|
|
109
122
|
ractors_map.keys.reject do |ractor|
|
|
110
|
-
ractor == wakeup_ractor && !
|
|
123
|
+
ractor == wakeup_ractor && !continuous_mode?
|
|
111
124
|
end
|
|
112
125
|
end
|
|
113
126
|
|
|
@@ -115,16 +128,14 @@ module Fractor
|
|
|
115
128
|
#
|
|
116
129
|
# @return [void]
|
|
117
130
|
def process_work_callbacks
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
127
|
-
end
|
|
131
|
+
new_work = @supervisor.callback_registry.process_work_callbacks
|
|
132
|
+
if new_work && !new_work.empty?
|
|
133
|
+
@supervisor.add_work_items(new_work)
|
|
134
|
+
puts "Work source provided #{new_work.size} new items" if @debug
|
|
135
|
+
|
|
136
|
+
# Distribute work to idle workers
|
|
137
|
+
distributed = work_distribution_manager.distribute_to_idle_workers
|
|
138
|
+
puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
|
|
128
139
|
end
|
|
129
140
|
end
|
|
130
141
|
|
|
@@ -211,8 +222,20 @@ processed_count)
|
|
|
211
222
|
|
|
212
223
|
ready_item, message = Ractor.select(*active_items)
|
|
213
224
|
|
|
214
|
-
#
|
|
215
|
-
if
|
|
225
|
+
# Debug: log what we received
|
|
226
|
+
if @debug
|
|
227
|
+
if ready_item.equal?(wakeup_port)
|
|
228
|
+
puts "DEBUG: Received from wakeup_port: #{message.inspect}"
|
|
229
|
+
elsif ready_item.is_a?(Ractor::Port)
|
|
230
|
+
worker = port_to_worker[ready_item]
|
|
231
|
+
puts "DEBUG: Received from port: #{worker&.name || 'unknown'}, message: #{message.inspect}"
|
|
232
|
+
else
|
|
233
|
+
puts "DEBUG: Received from ractor: #{ready_item.inspect}, message: #{message.inspect}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if this is the wakeup port (use equal? for identity comparison)
|
|
238
|
+
if wakeup_port && ready_item.equal?(wakeup_port)
|
|
216
239
|
puts "Wakeup signal received: #{message[:message]}" if @debug
|
|
217
240
|
# Remove wakeup ractor from map if shutting down
|
|
218
241
|
if message && message[:message] == :shutdown
|
|
@@ -226,7 +249,7 @@ processed_count)
|
|
|
226
249
|
[ready_item, message]
|
|
227
250
|
rescue Ractor::ClosedError, Ractor::Error => e
|
|
228
251
|
# Handle closed ports/ractors - remove them from ractors_map
|
|
229
|
-
puts "Ractor::Error in select: #{e.message}. Cleaning up closed ports." if @debug
|
|
252
|
+
puts "Ractor::Error in select: #{e.class} - #{e.message}. Cleaning up closed ports." if @debug
|
|
230
253
|
|
|
231
254
|
# Find and remove workers with closed ports
|
|
232
255
|
closed_ports = active_items.select { |item| item.is_a?(Ractor::Port) }
|
|
@@ -242,6 +265,11 @@ processed_count)
|
|
|
242
265
|
|
|
243
266
|
# Return nil to continue the loop with updated active_items
|
|
244
267
|
[nil, nil]
|
|
268
|
+
rescue StandardError => e
|
|
269
|
+
# Catch any other unexpected errors and re-raise with context
|
|
270
|
+
puts "Unexpected error in select_from_mixed: #{e.class} - #{e.message}" if @debug
|
|
271
|
+
puts e.backtrace.first(5) if @debug
|
|
272
|
+
raise
|
|
245
273
|
end
|
|
246
274
|
|
|
247
275
|
# Process a message from a ractor or port (Ruby 4.0+).
|
data/lib/fractor/result_cache.rb
CHANGED
|
@@ -156,21 +156,69 @@ module Fractor
|
|
|
156
156
|
|
|
157
157
|
# Generate a cache key for a work item.
|
|
158
158
|
#
|
|
159
|
+
# Optimized to avoid JSON.dump for common simple types.
|
|
160
|
+
# For complex nested structures, falls back to JSON serialization.
|
|
161
|
+
#
|
|
159
162
|
# @param work [Fractor::Work] The work item
|
|
160
163
|
# @return [String] The cache key
|
|
161
164
|
def generate_key(work)
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
# For simple types, use faster string interpolation
|
|
166
|
+
# For complex nested structures, fall back to JSON
|
|
167
|
+
input = work.input
|
|
168
|
+
input_str = if simple_input?(input)
|
|
169
|
+
serialize_simple_input(input)
|
|
170
|
+
else
|
|
171
|
+
JSON.dump(input)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Build key components
|
|
175
|
+
parts = [work.class.name, input_str]
|
|
176
|
+
parts << work.timeout.to_s if work.respond_to?(:timeout) && !work.timeout.nil?
|
|
171
177
|
|
|
172
178
|
# Use SHA256 hash for consistent, collision-resistant keys
|
|
173
|
-
Digest::SHA256.hexdigest(
|
|
179
|
+
Digest::SHA256.hexdigest(parts.join("|"))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check if input is a simple type that can be serialized without JSON.
|
|
183
|
+
# @return [Boolean] true if input is a simple, directly serializable type
|
|
184
|
+
def simple_input?(input)
|
|
185
|
+
case input
|
|
186
|
+
when NilClass, TrueClass, FalseClass, String, Numeric, Symbol
|
|
187
|
+
true
|
|
188
|
+
when Array
|
|
189
|
+
input.all? { |item| simple_input?(item) }
|
|
190
|
+
when Hash
|
|
191
|
+
input.keys.all? { |k| k.is_a?(String) || k.is_a?(Symbol) } &&
|
|
192
|
+
input.values.all? { |v| simple_input?(v) }
|
|
193
|
+
else
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Serialize simple input types efficiently without JSON.
|
|
199
|
+
# @return [String] Serialized representation
|
|
200
|
+
def serialize_simple_input(input)
|
|
201
|
+
case input
|
|
202
|
+
when NilClass
|
|
203
|
+
"nil"
|
|
204
|
+
when TrueClass
|
|
205
|
+
"true"
|
|
206
|
+
when FalseClass
|
|
207
|
+
"false"
|
|
208
|
+
when String
|
|
209
|
+
# Escape special characters for consistent hashing
|
|
210
|
+
input.inspect
|
|
211
|
+
when Symbol
|
|
212
|
+
":#{input}"
|
|
213
|
+
when Array
|
|
214
|
+
"[#{input.map { |item| serialize_simple_input(item) }.join(',')}]"
|
|
215
|
+
when Hash
|
|
216
|
+
pairs = input.map { |k, v| "#{k}=>#{serialize_simple_input(v)}" }
|
|
217
|
+
"{#{pairs.sort.join(',')}}"
|
|
218
|
+
else
|
|
219
|
+
# Fallback - shouldn't happen if simple_input? is correct
|
|
220
|
+
input.to_s
|
|
221
|
+
end
|
|
174
222
|
end
|
|
175
223
|
|
|
176
224
|
# Check if a cache entry is expired.
|
|
@@ -8,29 +8,35 @@ module Fractor
|
|
|
8
8
|
# the Single Responsibility Principle.
|
|
9
9
|
class ShutdownHandler
|
|
10
10
|
def initialize(workers, wakeup_ractor, timer_thread, performance_monitor,
|
|
11
|
-
main_loop_thread: nil, debug: false)
|
|
11
|
+
main_loop_thread: nil, debug: false, continuous_mode: false)
|
|
12
12
|
@workers = workers
|
|
13
13
|
@wakeup_ractor = wakeup_ractor
|
|
14
14
|
@timer_thread = timer_thread
|
|
15
15
|
@performance_monitor = performance_monitor
|
|
16
16
|
@main_loop_thread = main_loop_thread
|
|
17
17
|
@debug = debug
|
|
18
|
+
@continuous_mode = continuous_mode
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# Execute a graceful shutdown of all supervisor components.
|
|
21
22
|
# Components are stopped in the correct order to prevent issues:
|
|
22
23
|
# 1. Stop performance monitor (to stop metric collection)
|
|
23
|
-
# 2.
|
|
24
|
-
# 3.
|
|
25
|
-
# 4. Signal
|
|
26
|
-
# 5.
|
|
24
|
+
# 2. Skip stopping timer thread in continuous mode (it will exit when workers close)
|
|
25
|
+
# 3. Stop timer thread in batch mode (to stop periodic wakeups)
|
|
26
|
+
# 4. Signal wakeup ractor (to unblock Ractor.select)
|
|
27
|
+
# 5. Signal all workers (to stop processing)
|
|
28
|
+
# 6. Wait for main loop thread and workers to finish
|
|
27
29
|
#
|
|
28
30
|
# @param wait_for_completion [Boolean] Whether to wait for all workers to close
|
|
29
31
|
# @param timeout [Integer] Maximum seconds to wait for shutdown (default: 10)
|
|
30
32
|
# @return [void]
|
|
31
33
|
def shutdown(wait_for_completion: false, timeout: 10)
|
|
32
34
|
stop_performance_monitor
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
# Only stop timer thread in batch mode. In continuous mode, the timer thread
|
|
37
|
+
# will exit on its own when workers are closed (it checks this condition).
|
|
38
|
+
stop_timer_thread unless @continuous_mode
|
|
39
|
+
|
|
34
40
|
signal_wakeup_ractor
|
|
35
41
|
signal_all_workers
|
|
36
42
|
|