fractor 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -1,113 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fractor
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Base class for wrapped Ractors with shared functionality.
|
|
5
|
+
# Subclasses implement Ruby 3.x and Ruby 4.0+ specific communication patterns.
|
|
6
6
|
class WrappedRactor
|
|
7
|
-
attr_reader :ractor, :name
|
|
7
|
+
attr_reader :ractor, :name, :worker_class
|
|
8
|
+
|
|
9
|
+
# Factory method to create the appropriate WrappedRactor implementation
|
|
10
|
+
# based on the current Ruby version.
|
|
11
|
+
#
|
|
12
|
+
# @param name [String] Name for the ractor
|
|
13
|
+
# @param worker_class [Class] Worker class to instantiate
|
|
14
|
+
# @param kwargs [Hash] Additional keyword arguments for subclass initialization
|
|
15
|
+
# @return [WrappedRactor] The appropriate subclass instance
|
|
16
|
+
def self.create(name, worker_class, **kwargs)
|
|
17
|
+
ruby_4_0 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
|
|
18
|
+
if ruby_4_0
|
|
19
|
+
WrappedRactor4.new(name, worker_class, **kwargs)
|
|
20
|
+
else
|
|
21
|
+
WrappedRactor3.new(name, worker_class, **kwargs)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
8
24
|
|
|
9
|
-
# Initializes the WrappedRactor with a name and the Worker class
|
|
10
|
-
#
|
|
25
|
+
# Initializes the WrappedRactor with a name and the Worker class.
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] Name for the ractor
|
|
28
|
+
# @param worker_class [Class] Worker class to instantiate
|
|
11
29
|
def initialize(name, worker_class)
|
|
12
30
|
puts "Creating Ractor #{name} with worker #{worker_class}" if ENV["FRACTOR_DEBUG"]
|
|
13
31
|
@name = name
|
|
14
|
-
@worker_class = worker_class
|
|
15
|
-
@ractor = nil
|
|
32
|
+
@worker_class = worker_class
|
|
33
|
+
@ractor = nil
|
|
16
34
|
end
|
|
17
35
|
|
|
18
|
-
# Starts the underlying Ractor.
|
|
36
|
+
# Starts the underlying Ractor. Must be implemented by subclasses.
|
|
19
37
|
def start
|
|
20
|
-
|
|
21
|
-
# Pass worker_class to the Ractor block
|
|
22
|
-
@ractor = Ractor.new(@name, @worker_class) do |name, worker_cls|
|
|
23
|
-
puts "Ractor #{name} started with worker class #{worker_cls}" if ENV["FRACTOR_DEBUG"]
|
|
24
|
-
# Yield an initialization message
|
|
25
|
-
Ractor.yield({ type: :initialize, processor: name })
|
|
26
|
-
|
|
27
|
-
# Instantiate the specific worker inside the Ractor
|
|
28
|
-
worker = worker_cls.new(name: name)
|
|
29
|
-
|
|
30
|
-
loop do
|
|
31
|
-
# Ractor.receive will block until a message is received
|
|
32
|
-
puts "Waiting for work in #{name}" if ENV["FRACTOR_DEBUG"]
|
|
33
|
-
work = Ractor.receive
|
|
34
|
-
|
|
35
|
-
# Handle shutdown message
|
|
36
|
-
if work == :shutdown
|
|
37
|
-
puts "Received shutdown message in Ractor #{name}, terminating..." if ENV["FRACTOR_DEBUG"]
|
|
38
|
-
# Yield a shutdown acknowledgment before terminating
|
|
39
|
-
Ractor.yield({ type: :shutdown, processor: name })
|
|
40
|
-
break
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
puts "Received work #{work.inspect} in #{name}" if ENV["FRACTOR_DEBUG"]
|
|
44
|
-
|
|
45
|
-
begin
|
|
46
|
-
# Process the work using the instantiated worker
|
|
47
|
-
result = worker.process(work)
|
|
48
|
-
puts "Sending result #{result.inspect} from Ractor #{name}" if ENV["FRACTOR_DEBUG"]
|
|
49
|
-
# Wrap the result in a WorkResult object if not already wrapped
|
|
50
|
-
work_result = if result.is_a?(Fractor::WorkResult)
|
|
51
|
-
result
|
|
52
|
-
else
|
|
53
|
-
Fractor::WorkResult.new(result: result, work: work)
|
|
54
|
-
end
|
|
55
|
-
# Yield the result back
|
|
56
|
-
Ractor.yield({ type: :result, result: work_result,
|
|
57
|
-
processor: name })
|
|
58
|
-
rescue StandardError => e
|
|
59
|
-
# Handle errors during processing
|
|
60
|
-
puts "Error processing work #{work.inspect} in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}" if ENV["FRACTOR_DEBUG"]
|
|
61
|
-
# Yield an error message back
|
|
62
|
-
# Ensure the original work object is included in the error result
|
|
63
|
-
error_result = Fractor::WorkResult.new(error: e.message, work: work)
|
|
64
|
-
Ractor.yield({ type: :error, result: error_result,
|
|
65
|
-
processor: name })
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
rescue Ractor::ClosedError
|
|
69
|
-
puts "Ractor #{name} closed." if ENV["FRACTOR_DEBUG"]
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
puts "Unexpected error in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}" if ENV["FRACTOR_DEBUG"]
|
|
72
|
-
# Optionally yield a critical error message if needed
|
|
73
|
-
ensure
|
|
74
|
-
puts "Ractor #{name} shutting down." if ENV["FRACTOR_DEBUG"]
|
|
75
|
-
end
|
|
76
|
-
puts "Ractor #{@name} instance created: #{@ractor}" if ENV["FRACTOR_DEBUG"]
|
|
38
|
+
raise NotImplementedError, "Subclasses must implement #start"
|
|
77
39
|
end
|
|
78
40
|
|
|
79
|
-
# Sends work to the Ractor
|
|
41
|
+
# Sends work to the Ractor. Must be implemented by subclasses.
|
|
42
|
+
#
|
|
43
|
+
# @param work [Fractor::Work] The work item to process
|
|
44
|
+
# @return [Boolean] true if sent successfully, false otherwise
|
|
80
45
|
def send(work)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else
|
|
90
|
-
puts "Warning: Attempted to send work to nil Ractor #{@name}" if ENV["FRACTOR_DEBUG"]
|
|
91
|
-
false
|
|
92
|
-
end
|
|
46
|
+
raise NotImplementedError, "Subclasses must implement #send"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Receives a message from the Ractor. Must be implemented by subclasses.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash, nil] The message received or nil
|
|
52
|
+
def receive_message
|
|
53
|
+
raise NotImplementedError, "Subclasses must implement #receive_message"
|
|
93
54
|
end
|
|
94
55
|
|
|
95
56
|
# Closes the Ractor.
|
|
96
|
-
#
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true if closed successfully
|
|
97
59
|
def close
|
|
98
60
|
return true if @ractor.nil?
|
|
99
61
|
|
|
100
62
|
begin
|
|
101
|
-
# Send a nil message to signal we're done
|
|
102
|
-
# if the Ractor is waiting for input
|
|
63
|
+
# Send a nil message to signal we're done
|
|
103
64
|
begin
|
|
104
|
-
|
|
105
|
-
@ractor.send(nil)
|
|
106
|
-
rescue StandardError
|
|
107
|
-
nil
|
|
108
|
-
end
|
|
65
|
+
@ractor.send(nil)
|
|
109
66
|
rescue StandardError
|
|
110
|
-
|
|
67
|
+
nil
|
|
111
68
|
end
|
|
112
69
|
|
|
113
70
|
# Mark as closed in our object
|
|
@@ -133,25 +90,33 @@ module Fractor
|
|
|
133
90
|
end
|
|
134
91
|
|
|
135
92
|
# Checks if the Ractor is closed or unavailable.
|
|
93
|
+
# Uses a timeout to avoid blocking on Windows Ruby 3.4 where
|
|
94
|
+
# Ractor#inspect can block if the ractor is waiting on receive.
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean] true if closed, false otherwise
|
|
136
97
|
def closed?
|
|
137
98
|
return true if @ractor.nil?
|
|
138
99
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
rescue Exception => e
|
|
150
|
-
# If we get an exception, the Ractor is likely terminated
|
|
151
|
-
puts "Ractor #{@name} appears to be terminated: #{e.message}" if ENV["FRACTOR_DEBUG"]
|
|
100
|
+
# Use a timeout to avoid blocking indefinitely on Windows Ruby 3.4
|
|
101
|
+
result = Timeout.timeout(0.1) do
|
|
102
|
+
@ractor.inspect
|
|
103
|
+
rescue Timeout::Error
|
|
104
|
+
# Timeout means ractor is still running (not terminated)
|
|
105
|
+
"#<Ractor:blocked>"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if result.include?("terminated")
|
|
109
|
+
# If terminated, clean up our reference
|
|
152
110
|
@ractor = nil
|
|
153
|
-
true
|
|
111
|
+
return true
|
|
154
112
|
end
|
|
113
|
+
|
|
114
|
+
false
|
|
115
|
+
rescue Exception => e
|
|
116
|
+
# If we get an exception, the Ractor is likely terminated
|
|
117
|
+
puts "Ractor #{@name} appears to be terminated: #{e.message}" if ENV["FRACTOR_DEBUG"]
|
|
118
|
+
@ractor = nil
|
|
119
|
+
true
|
|
155
120
|
end
|
|
156
121
|
end
|
|
157
122
|
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "wrapped_ractor"
|
|
5
|
+
require_relative "logger"
|
|
6
|
+
|
|
7
|
+
module Fractor
|
|
8
|
+
# Ruby 3.x specific implementation of WrappedRactor.
|
|
9
|
+
# Uses Ractor.yield for sending messages back from workers.
|
|
10
|
+
class WrappedRactor3 < WrappedRactor
|
|
11
|
+
# Initializes the WrappedRactor3.
|
|
12
|
+
# The response_port parameter is accepted for API compatibility but not used in Ruby 3.x.
|
|
13
|
+
#
|
|
14
|
+
# @param name [String] Name for the ractor
|
|
15
|
+
# @param worker_class [Class] Worker class to instantiate
|
|
16
|
+
# @param response_port [Object, nil] Unused in Ruby 3.x (for API compatibility with Ruby 4.0)
|
|
17
|
+
def initialize(name, worker_class, response_port: nil)
|
|
18
|
+
super(name, worker_class)
|
|
19
|
+
# response_port is not used in Ruby 3.x
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Starts the underlying Ractor using Ractor.yield pattern.
|
|
23
|
+
def start
|
|
24
|
+
RactorLogger.info("Starting Ractor #{@name} (Ruby 3.x mode)",
|
|
25
|
+
ractor_name: @name)
|
|
26
|
+
|
|
27
|
+
# Capture timeout value before entering ractor (Ractors can't access Fractor.config)
|
|
28
|
+
# Get class-level timeout, or fall back to default of nil (no timeout)
|
|
29
|
+
# Note: We avoid accessing Fractor.config from ractor creation context
|
|
30
|
+
class_level_timeout = @worker_class.worker_timeout
|
|
31
|
+
|
|
32
|
+
# Pass worker_class and timeout to the Ractor block
|
|
33
|
+
@ractor = Ractor.new(@name, @worker_class,
|
|
34
|
+
class_level_timeout) do |name, worker_cls, timeout_val|
|
|
35
|
+
RactorLogger.debug(
|
|
36
|
+
"Ractor started with worker class #{worker_cls} and timeout #{timeout_val.inspect}", ractor_name: name
|
|
37
|
+
)
|
|
38
|
+
# Yield an initialization message
|
|
39
|
+
Ractor.yield({ type: :initialize, processor: name })
|
|
40
|
+
|
|
41
|
+
# Instantiate the specific worker inside the Ractor
|
|
42
|
+
# Pass timeout as an option only if it's not nil, to avoid accessing self.class from ractor
|
|
43
|
+
worker = if timeout_val.nil?
|
|
44
|
+
worker_cls.new(name: name)
|
|
45
|
+
else
|
|
46
|
+
worker_cls.new(name: name, timeout: timeout_val)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
loop do
|
|
50
|
+
# Ractor.receive will block until a message is received
|
|
51
|
+
RactorLogger.debug("Waiting for work", ractor_name: name)
|
|
52
|
+
work = Ractor.receive
|
|
53
|
+
|
|
54
|
+
# Handle shutdown message
|
|
55
|
+
if work == :shutdown
|
|
56
|
+
RactorLogger.debug("Received shutdown message, terminating",
|
|
57
|
+
ractor_name: name)
|
|
58
|
+
# Yield a shutdown acknowledgment before terminating
|
|
59
|
+
Ractor.yield({ type: :shutdown, processor: name })
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
RactorLogger.debug("Received work #{work.inspect}", ractor_name: name)
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
# Get the timeout for this worker (nil means no timeout)
|
|
67
|
+
worker_timeout = worker.timeout
|
|
68
|
+
|
|
69
|
+
# Process the work with timeout if configured
|
|
70
|
+
# Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
|
|
71
|
+
# We measure execution time and raise timeout error afterward if exceeded.
|
|
72
|
+
result = if worker_timeout
|
|
73
|
+
start_time = Time.now
|
|
74
|
+
process_result = worker.process(work)
|
|
75
|
+
elapsed = Time.now - start_time
|
|
76
|
+
if elapsed > worker_timeout
|
|
77
|
+
# Raise a timeout error after the fact
|
|
78
|
+
# Note: This is a post-facto timeout check - the work has already completed
|
|
79
|
+
raise Timeout::Error,
|
|
80
|
+
"execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
process_result
|
|
84
|
+
else
|
|
85
|
+
worker.process(work)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
RactorLogger.debug("Sending result #{result.inspect}",
|
|
89
|
+
ractor_name: name)
|
|
90
|
+
# Wrap the result in a WorkResult object if not already wrapped
|
|
91
|
+
work_result = if result.is_a?(Fractor::WorkResult)
|
|
92
|
+
result
|
|
93
|
+
else
|
|
94
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
95
|
+
end
|
|
96
|
+
# Yield the result back
|
|
97
|
+
Ractor.yield({ type: :result, result: work_result,
|
|
98
|
+
processor: name })
|
|
99
|
+
rescue Timeout::Error => e
|
|
100
|
+
# Handle timeout errors as retriable errors
|
|
101
|
+
RactorLogger.warn(
|
|
102
|
+
"Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
|
|
103
|
+
)
|
|
104
|
+
error_result = Fractor::WorkResult.new(
|
|
105
|
+
error: "Worker timeout: #{e.message}",
|
|
106
|
+
work: work,
|
|
107
|
+
error_category: :timeout,
|
|
108
|
+
)
|
|
109
|
+
Ractor.yield({ type: :error, result: error_result,
|
|
110
|
+
processor: name })
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
# Handle errors during processing
|
|
113
|
+
RactorLogger.error("Error processing work #{work.inspect}",
|
|
114
|
+
ractor_name: name, exception: e)
|
|
115
|
+
# Yield an error message back
|
|
116
|
+
# Ensure the original work object is included in the error result
|
|
117
|
+
error_result = Fractor::WorkResult.new(error: e.message, work: work)
|
|
118
|
+
Ractor.yield({ type: :error, result: error_result,
|
|
119
|
+
processor: name })
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue Ractor::ClosedError
|
|
123
|
+
RactorLogger.debug("Ractor closed", ractor_name: @name)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
RactorLogger.error("Unexpected error", ractor_name: @name, exception: e)
|
|
126
|
+
ensure
|
|
127
|
+
RactorLogger.debug("Ractor shutting down", ractor_name: @name)
|
|
128
|
+
end
|
|
129
|
+
RactorLogger.debug("Ractor instance created: #{@ractor}",
|
|
130
|
+
ractor_name: @name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Sends work to the Ractor.
|
|
134
|
+
#
|
|
135
|
+
# @param work [Fractor::Work] The work item to process
|
|
136
|
+
# @return [Boolean] true if sent successfully, false otherwise
|
|
137
|
+
def send(work)
|
|
138
|
+
if @ractor
|
|
139
|
+
begin
|
|
140
|
+
@ractor.send(work)
|
|
141
|
+
true
|
|
142
|
+
rescue Exception => e
|
|
143
|
+
RactorLogger.warn("Error sending work to Ractor: #{e.message}",
|
|
144
|
+
ractor_name: @name)
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
RactorLogger.warn("Attempted to send work to nil Ractor",
|
|
149
|
+
ractor_name: @name)
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Receives a message from the Ractor using Ractor.take.
|
|
155
|
+
#
|
|
156
|
+
# @return [Hash, nil] The message received or nil
|
|
157
|
+
def receive_message
|
|
158
|
+
@ractor&.take
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "wrapped_ractor"
|
|
5
|
+
require_relative "logger"
|
|
6
|
+
|
|
7
|
+
module Fractor
|
|
8
|
+
# Ruby 4.0+ specific implementation of WrappedRactor.
|
|
9
|
+
# Uses Ractor::Port for communication - main ractor creates response ports
|
|
10
|
+
# and passes them to workers when sending work.
|
|
11
|
+
class WrappedRactor4 < WrappedRactor
|
|
12
|
+
attr_reader :response_port
|
|
13
|
+
|
|
14
|
+
# Initializes the WrappedRactor with a name, worker class, and response port.
|
|
15
|
+
#
|
|
16
|
+
# @param name [String] Name for the ractor
|
|
17
|
+
# @param worker_class [Class] Worker class to instantiate
|
|
18
|
+
# @param response_port [Ractor::Port, nil] The port to receive responses on (created by main ractor)
|
|
19
|
+
def initialize(name, worker_class, response_port: nil)
|
|
20
|
+
super(name, worker_class)
|
|
21
|
+
@response_port = response_port
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Sets the response port for this worker.
|
|
25
|
+
#
|
|
26
|
+
# @param port [Ractor::Port] The port to receive responses on
|
|
27
|
+
def response_port=(port)
|
|
28
|
+
@response_port = port
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Starts the underlying Ractor using the port-based pattern.
|
|
32
|
+
# In Ruby 4.0:
|
|
33
|
+
# - Main ractor creates response ports (one per worker)
|
|
34
|
+
# - Main ractor sends [work, response_port] to workers
|
|
35
|
+
# - Workers receive work and response_port, send results back via response_port
|
|
36
|
+
def start
|
|
37
|
+
RactorLogger.info("Starting Ractor #{@name} (Ruby 4.0 mode)",
|
|
38
|
+
ractor_name: @name)
|
|
39
|
+
|
|
40
|
+
# Capture timeout value before entering ractor (Ractors can't access Fractor.config)
|
|
41
|
+
# Get class-level timeout, or fall back to default of nil (no timeout)
|
|
42
|
+
# Note: We avoid accessing Fractor.config from ractor creation context
|
|
43
|
+
class_level_timeout = @worker_class.worker_timeout
|
|
44
|
+
|
|
45
|
+
# In Ruby 4.0, workers don't create their own ports
|
|
46
|
+
# They receive response_port from main ractor when work is sent
|
|
47
|
+
@ractor = Ractor.new(@name, @worker_class,
|
|
48
|
+
class_level_timeout) do |name, worker_cls, timeout_val|
|
|
49
|
+
RactorLogger.debug(
|
|
50
|
+
"Ractor started with worker class #{worker_cls} and timeout #{timeout_val.inspect}", ractor_name: name
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Instantiate the specific worker inside the Ractor
|
|
54
|
+
# Pass timeout as an option only if it's not nil, to avoid accessing self.class from ractor
|
|
55
|
+
worker = if timeout_val.nil?
|
|
56
|
+
worker_cls.new(name: name)
|
|
57
|
+
else
|
|
58
|
+
worker_cls.new(name: name, timeout: timeout_val)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Main message processing loop
|
|
62
|
+
loop do
|
|
63
|
+
# Receive work from the main ractor (blocks until message available)
|
|
64
|
+
# In Ruby 4.0, main sends [work, response_port]
|
|
65
|
+
received = Ractor.receive
|
|
66
|
+
RactorLogger.debug("Received #{received.inspect}", ractor_name: name)
|
|
67
|
+
|
|
68
|
+
# Handle shutdown message
|
|
69
|
+
if received == :shutdown
|
|
70
|
+
RactorLogger.debug("Received shutdown message, terminating",
|
|
71
|
+
ractor_name: name)
|
|
72
|
+
break
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extract work and response_port
|
|
76
|
+
# Main should send [work, response_port]
|
|
77
|
+
if received.is_a?(Array) && received.size == 2
|
|
78
|
+
work, response_port = received
|
|
79
|
+
else
|
|
80
|
+
# Legacy format for initialization or other messages
|
|
81
|
+
work = received
|
|
82
|
+
response_port = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Handle initialize message (for backwards compatibility during startup)
|
|
86
|
+
if work.is_a?(Hash) && work[:type] == :initialize
|
|
87
|
+
RactorLogger.debug("Worker initialized", ractor_name: name)
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
# Get the timeout for this worker (nil means no timeout)
|
|
93
|
+
worker_timeout = worker.timeout
|
|
94
|
+
|
|
95
|
+
# Process the work with timeout if configured
|
|
96
|
+
# Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
|
|
97
|
+
# We measure execution time and raise timeout error afterward if exceeded.
|
|
98
|
+
result = if worker_timeout
|
|
99
|
+
start_time = Time.now
|
|
100
|
+
process_result = worker.process(work)
|
|
101
|
+
elapsed = Time.now - start_time
|
|
102
|
+
if elapsed > worker_timeout
|
|
103
|
+
# Raise a timeout error after the fact
|
|
104
|
+
# Note: This is a post-facto timeout check - the work has already completed
|
|
105
|
+
raise Timeout::Error,
|
|
106
|
+
"execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
process_result
|
|
110
|
+
else
|
|
111
|
+
worker.process(work)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
RactorLogger.debug("Sending result #{result.inspect}",
|
|
115
|
+
ractor_name: name)
|
|
116
|
+
|
|
117
|
+
# Wrap the result in a WorkResult object if not already wrapped
|
|
118
|
+
work_result = if result.is_a?(Fractor::WorkResult)
|
|
119
|
+
result
|
|
120
|
+
else
|
|
121
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Send the result back through the response port
|
|
125
|
+
if response_port
|
|
126
|
+
response_port << { type: :result, result: work_result,
|
|
127
|
+
processor: name }
|
|
128
|
+
else
|
|
129
|
+
RactorLogger.warn("No response port available, result lost",
|
|
130
|
+
ractor_name: name)
|
|
131
|
+
end
|
|
132
|
+
rescue Timeout::Error => e
|
|
133
|
+
# Handle timeout errors as retriable errors
|
|
134
|
+
RactorLogger.warn(
|
|
135
|
+
"Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
|
|
136
|
+
)
|
|
137
|
+
error_result = Fractor::WorkResult.new(
|
|
138
|
+
error: "Worker timeout: #{e.message}",
|
|
139
|
+
work: work,
|
|
140
|
+
error_category: :timeout,
|
|
141
|
+
)
|
|
142
|
+
if response_port
|
|
143
|
+
response_port << { type: :error, result: error_result,
|
|
144
|
+
processor: name }
|
|
145
|
+
end
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
# Handle errors during processing
|
|
148
|
+
RactorLogger.error("Error processing work #{work.inspect}",
|
|
149
|
+
ractor_name: name, exception: e)
|
|
150
|
+
|
|
151
|
+
# Send an error message back through the response port
|
|
152
|
+
# Ensure the original work object is included in the error result
|
|
153
|
+
error_result = Fractor::WorkResult.new(error: e.message, work: work)
|
|
154
|
+
if response_port
|
|
155
|
+
response_port << { type: :error, result: error_result,
|
|
156
|
+
processor: name }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue Ractor::ClosedError
|
|
161
|
+
RactorLogger.debug("Ractor closed", ractor_name: name)
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
RactorLogger.error("Unexpected error", ractor_name: name, exception: e)
|
|
164
|
+
ensure
|
|
165
|
+
RactorLogger.debug("Ractor shutting down", ractor_name: name)
|
|
166
|
+
end
|
|
167
|
+
RactorLogger.debug("Ractor instance created", ractor_name: name)
|
|
168
|
+
end
|
|
169
|
+
RactorLogger.debug("Ractor #{@ractor} started", ractor_name: @name)
|
|
170
|
+
|
|
171
|
+
# Sends work to the Ractor.
|
|
172
|
+
# In Ruby 4.0, sends [work, response_port] so worker can reply back.
|
|
173
|
+
# Special case: sends just :shutdown for shutdown messages.
|
|
174
|
+
#
|
|
175
|
+
# @param work [Fractor::Work, Symbol] The work item to process, or :shutdown
|
|
176
|
+
# @return [Boolean] true if sent successfully, false otherwise
|
|
177
|
+
def send(work)
|
|
178
|
+
if @ractor
|
|
179
|
+
begin
|
|
180
|
+
# In Ruby 4.0, send [work, response_port] so worker can reply
|
|
181
|
+
# Special case: shutdown is sent as a symbol, not an array
|
|
182
|
+
if work == :shutdown
|
|
183
|
+
@ractor.send(:shutdown)
|
|
184
|
+
else
|
|
185
|
+
@ractor.send([work, @response_port])
|
|
186
|
+
end
|
|
187
|
+
true
|
|
188
|
+
rescue Exception => e
|
|
189
|
+
RactorLogger.warn("Error sending work to Ractor: #{e.message}",
|
|
190
|
+
ractor_name: @name)
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
RactorLogger.warn("Attempted to send work to nil Ractor",
|
|
195
|
+
ractor_name: @name)
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Receives a message from the Ractor.
|
|
201
|
+
# In Ruby 4.0, messages come through response ports in the main loop.
|
|
202
|
+
# This method is kept for backwards compatibility with tests.
|
|
203
|
+
#
|
|
204
|
+
# Note: In Ruby 4.0, this will block if no message is available.
|
|
205
|
+
# The test should either skip this or ensure a message was sent first.
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash, nil] The message received or nil
|
|
208
|
+
def receive_message
|
|
209
|
+
# In Ruby 4.0, we receive through the response_port
|
|
210
|
+
# Try to receive with a small timeout to avoid blocking indefinitely
|
|
211
|
+
return nil unless @response_port
|
|
212
|
+
|
|
213
|
+
# Use a non-blocking receive attempt
|
|
214
|
+
# In Ruby 4.0, if the port is empty, this would block
|
|
215
|
+
# For test compatibility, we return nil if no message is available
|
|
216
|
+
@response_port.receive
|
|
217
|
+
rescue Ractor::ClosedError, Ractor::Error
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Closes the Ractor and its response port.
|
|
222
|
+
# In Ruby 4.0, we need to explicitly close the response port
|
|
223
|
+
# to prevent Ractor.select from hanging.
|
|
224
|
+
#
|
|
225
|
+
# @return [Boolean] true if closed successfully
|
|
226
|
+
def close
|
|
227
|
+
# Close the response port first
|
|
228
|
+
if @response_port
|
|
229
|
+
begin
|
|
230
|
+
# Send nil through the port to signal it's closing
|
|
231
|
+
@response_port.send(nil) if @response_port.respond_to?(:send)
|
|
232
|
+
rescue StandardError
|
|
233
|
+
# Port may already be closed
|
|
234
|
+
end
|
|
235
|
+
@response_port = nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Then close the underlying ractor
|
|
239
|
+
super
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|