fractor 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.adoc +755 -0
- data/Rakefile +12 -0
- data/examples/hierarchical_hasher.rb +158 -0
- data/examples/producer_subscriber.rb +300 -0
- data/lib/fractor/result_aggregator.rb +34 -0
- data/lib/fractor/supervisor.rb +174 -0
- data/lib/fractor/version.rb +6 -0
- data/lib/fractor/work.rb +17 -0
- data/lib/fractor/work_result.rb +35 -0
- data/lib/fractor/worker.rb +11 -0
- data/lib/fractor/wrapped_ractor.rb +140 -0
- data/lib/fractor.rb +17 -0
- data/sample.rb +64 -0
- data/sig/fractor.rbs +4 -0
- data/tests/sample.rb.bak +309 -0
- data/tests/sample_working.rb.bak +209 -0
- metadata +66 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fractor
|
4
|
+
# Represents the result of processing a Work item.
|
5
|
+
# Can hold either a successful result or an error.
|
6
|
+
class WorkResult
|
7
|
+
attr_reader :result, :error, :work
|
8
|
+
|
9
|
+
def initialize(result: nil, error: nil, work: nil)
|
10
|
+
@result = result
|
11
|
+
@error = error
|
12
|
+
@work = work
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
!@error
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
if success?
|
21
|
+
"Result: #{@result}"
|
22
|
+
else
|
23
|
+
"Error: #{@error}, Work: #{@work}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
{
|
29
|
+
result: @result,
|
30
|
+
error: @error,
|
31
|
+
work: @work&.to_s # Use safe navigation for work
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fractor
|
4
|
+
# Base class for defining work processors.
|
5
|
+
# Subclasses must implement the `process` method.
|
6
|
+
class Worker
|
7
|
+
def process(work)
|
8
|
+
raise NotImplementedError, "Subclasses must implement the 'process' method."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fractor
|
4
|
+
# Wraps a Ruby Ractor to manage a worker instance.
|
5
|
+
# Handles communication and error propagation.
|
6
|
+
class WrappedRactor
|
7
|
+
attr_reader :ractor, :name
|
8
|
+
|
9
|
+
# Initializes the WrappedRactor with a name and the Worker class to instantiate.
|
10
|
+
# The worker_class parameter allows flexibility in specifying the worker type.
|
11
|
+
def initialize(name, worker_class)
|
12
|
+
puts "Creating Ractor #{name} with worker #{worker_class}"
|
13
|
+
@name = name
|
14
|
+
@worker_class = worker_class # Store the worker class
|
15
|
+
@ractor = nil # Initialize ractor as nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Starts the underlying Ractor.
|
19
|
+
def start
|
20
|
+
puts "Starting Ractor #{@name}"
|
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}"
|
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
|
29
|
+
|
30
|
+
loop do
|
31
|
+
# Ractor.receive will block until a message is received
|
32
|
+
puts "Waiting for work in #{name}"
|
33
|
+
work = Ractor.receive
|
34
|
+
puts "Received work #{work.inspect} in #{name}"
|
35
|
+
|
36
|
+
begin
|
37
|
+
# Process the work using the instantiated worker
|
38
|
+
result = worker.process(work)
|
39
|
+
puts "Sending result #{result.inspect} from Ractor #{name}"
|
40
|
+
# Yield the result back
|
41
|
+
Ractor.yield({ type: :result, result: result, processor: name })
|
42
|
+
rescue StandardError => e
|
43
|
+
# Handle errors during processing
|
44
|
+
puts "Error processing work #{work.inspect} in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}"
|
45
|
+
# Yield an error message back
|
46
|
+
# Ensure the original work object is included in the error result
|
47
|
+
error_result = Fractor::WorkResult.new(error: e.message, work: work)
|
48
|
+
Ractor.yield({ type: :error, result: error_result, processor: name })
|
49
|
+
end
|
50
|
+
end
|
51
|
+
rescue Ractor::ClosedError
|
52
|
+
puts "Ractor #{name} closed."
|
53
|
+
rescue StandardError => e
|
54
|
+
puts "Unexpected error in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}"
|
55
|
+
# Optionally yield a critical error message if needed
|
56
|
+
ensure
|
57
|
+
puts "Ractor #{name} shutting down."
|
58
|
+
end
|
59
|
+
puts "Ractor #{@name} instance created: #{@ractor}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sends work to the Ractor if it's active.
|
63
|
+
def send(work)
|
64
|
+
if @ractor
|
65
|
+
begin
|
66
|
+
@ractor.send(work)
|
67
|
+
true
|
68
|
+
rescue Exception => e
|
69
|
+
puts "Warning: Error sending work to Ractor #{@name}: #{e.message}"
|
70
|
+
false
|
71
|
+
end
|
72
|
+
else
|
73
|
+
puts "Warning: Attempted to send work to nil Ractor #{@name}"
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Closes the Ractor.
|
79
|
+
# Ruby 3.0+ has different ways to terminate Ractors, we try the available methods
|
80
|
+
def close
|
81
|
+
return true if @ractor.nil?
|
82
|
+
|
83
|
+
begin
|
84
|
+
# Send a nil message to signal we're done - this might be processed
|
85
|
+
# if the Ractor is waiting for input
|
86
|
+
begin
|
87
|
+
begin
|
88
|
+
@ractor.send(nil)
|
89
|
+
rescue StandardError
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
rescue StandardError
|
93
|
+
# Ignore errors when sending nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Mark as closed in our object
|
97
|
+
old_ractor = @ractor
|
98
|
+
@ractor = nil
|
99
|
+
|
100
|
+
# If available in this Ruby version, we'll try kill
|
101
|
+
if old_ractor.respond_to?(:kill)
|
102
|
+
begin
|
103
|
+
old_ractor.kill
|
104
|
+
rescue StandardError
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
true
|
110
|
+
rescue Exception => e
|
111
|
+
puts "Warning: Error closing Ractor #{@name}: #{e.message}"
|
112
|
+
# Consider it closed even if there was an error
|
113
|
+
@ractor = nil
|
114
|
+
true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Checks if the Ractor is closed or unavailable.
|
119
|
+
def closed?
|
120
|
+
return true if @ractor.nil?
|
121
|
+
|
122
|
+
begin
|
123
|
+
# Check if the Ractor is terminated using Ractor#inspect
|
124
|
+
# This is safer than calling methods on the Ractor
|
125
|
+
r_status = @ractor.inspect
|
126
|
+
if r_status.include?("terminated")
|
127
|
+
# If terminated, clean up our reference
|
128
|
+
@ractor = nil
|
129
|
+
return true
|
130
|
+
end
|
131
|
+
false
|
132
|
+
rescue Exception => e
|
133
|
+
# If we get an exception, the Ractor is likely terminated
|
134
|
+
puts "Ractor #{@name} appears to be terminated: #{e.message}"
|
135
|
+
@ractor = nil
|
136
|
+
true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/fractor.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread' # Required for Queue
|
4
|
+
|
5
|
+
# Require all component files
|
6
|
+
require_relative 'fractor/version'
|
7
|
+
require_relative 'fractor/worker'
|
8
|
+
require_relative 'fractor/work'
|
9
|
+
require_relative 'fractor/work_result'
|
10
|
+
require_relative 'fractor/result_aggregator'
|
11
|
+
require_relative 'fractor/wrapped_ractor'
|
12
|
+
require_relative 'fractor/supervisor'
|
13
|
+
|
14
|
+
# Fractor: Function-driven Ractors framework
|
15
|
+
module Fractor
|
16
|
+
# The module is defined in the individual files
|
17
|
+
end
|
data/sample.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'fractor'
|
4
|
+
|
5
|
+
# Client-specific worker implementation inheriting from Fractor::Worker
|
6
|
+
class MyWorker < Fractor::Worker
|
7
|
+
# This method is called by the Ractor to process the work
|
8
|
+
# It should return a Fractor::WorkResult object
|
9
|
+
# If there is an error, it should raise an exception
|
10
|
+
# The Ractor will catch the exception and send it back to the main thread
|
11
|
+
def process(work)
|
12
|
+
puts "Working on '#{work.inspect}'"
|
13
|
+
|
14
|
+
if work.input == 5
|
15
|
+
# Return a Fractor::WorkResult for errors
|
16
|
+
return Fractor::WorkResult.new(error: "Error processing work #{work.input}", work: work)
|
17
|
+
end
|
18
|
+
|
19
|
+
calculated = work.input * 2
|
20
|
+
# Return a Fractor::WorkResult for success
|
21
|
+
Fractor::WorkResult.new(result: calculated, work: work)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Client-specific work item implementation inheriting from Fractor::Work
|
26
|
+
class MyWork < Fractor::Work
|
27
|
+
def initialize(input)
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"MyWork: #{@input}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# --- Main Execution ---
|
37
|
+
# This section demonstrates how to use the Fractor framework with custom
|
38
|
+
# MyWorker and MyWork classes.
|
39
|
+
if __FILE__ == $0
|
40
|
+
# Create supervisor, passing the client-specific worker and work classes
|
41
|
+
supervisor = Fractor::Supervisor.new(
|
42
|
+
worker_class: MyWorker,
|
43
|
+
work_class: MyWork,
|
44
|
+
num_workers: 2 # Specify the number of worker Ractors
|
45
|
+
)
|
46
|
+
|
47
|
+
# Add work items (raw data) - the Supervisor will wrap these in MyWork objects
|
48
|
+
work_items = (1..10).to_a
|
49
|
+
supervisor.add_work(work_items)
|
50
|
+
|
51
|
+
# Run the supervisor to start processing work
|
52
|
+
supervisor.run
|
53
|
+
|
54
|
+
puts "Processing complete."
|
55
|
+
puts "Final Aggregated Results:"
|
56
|
+
# Access the results aggregator from the supervisor
|
57
|
+
puts supervisor.results.inspect
|
58
|
+
|
59
|
+
# Print failed items directly from the Fractor::ResultAggregator's errors array
|
60
|
+
failed_items = supervisor.results.errors # Access the errors array
|
61
|
+
puts "\nFailed Work Items (#{failed_items.size}):"
|
62
|
+
# Display each Fractor::WorkResult object in the errors array
|
63
|
+
puts failed_items.map(&:to_s).join("\n") # Use to_s on the WorkResult objects
|
64
|
+
end
|
data/sig/fractor.rbs
ADDED
data/tests/sample.rb.bak
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
class Worker
|
5
|
+
def process(work)
|
6
|
+
raise NotImplementedError, "This #{self.class} cannot respond to:"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class MyWorker < Worker
|
11
|
+
# This method is called by the Ractor to process the work
|
12
|
+
# It should return a WorkResult object
|
13
|
+
# If there is an error, it should raise an exception
|
14
|
+
# The Ractor will catch the exception and send it back to the main thread
|
15
|
+
def process(work)
|
16
|
+
puts "Working on '#{work.inspect}'"
|
17
|
+
|
18
|
+
if work.input == 5
|
19
|
+
return WorkResult.new(error: "Error processing work #{work.input}", work: work)
|
20
|
+
end
|
21
|
+
|
22
|
+
calculated = work.input * 2
|
23
|
+
WorkResult.new(result: calculated, work: work)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Work
|
28
|
+
attr_reader :input
|
29
|
+
def initialize(input)
|
30
|
+
@input = input
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"Work: #{@input}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class MyWork < Work
|
39
|
+
def initialize(input)
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
"MyWork: #{@input}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class WorkResult
|
49
|
+
attr_reader :result, :error, :work
|
50
|
+
def initialize(result: nil, error: nil, work: nil)
|
51
|
+
@result = result
|
52
|
+
@error = error
|
53
|
+
@work = work
|
54
|
+
end
|
55
|
+
|
56
|
+
def success?
|
57
|
+
!@error
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
if success?
|
62
|
+
"Result: #{@result}"
|
63
|
+
else
|
64
|
+
"Error: #{@error}, Work: #{@work}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
class ResultAggregator
|
71
|
+
attr_reader :results, :errors
|
72
|
+
|
73
|
+
# This class is used to aggregate the results and errors from the Ractors
|
74
|
+
# It will store the results and errors in separate arrays
|
75
|
+
# It will also provide a method to print the results and errors
|
76
|
+
def initialize
|
77
|
+
@results = []
|
78
|
+
@errors = []
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_result(result)
|
82
|
+
if result.success?
|
83
|
+
puts "Work completed successfully: #{result}"
|
84
|
+
@results << result
|
85
|
+
else
|
86
|
+
puts "Error processing work: #{result}"
|
87
|
+
@errors << result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
"Results: #{@results.each(&:to_s).join(", ")}, Errors: #{@errors.each(&:to_s).join(", ")}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
{
|
97
|
+
results: @results.map(&:to_s),
|
98
|
+
errors: @errors.map(&:to_s)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class MyRactor
|
104
|
+
def initialize(name)
|
105
|
+
puts "Creating Ractor #{name}"
|
106
|
+
@name = name
|
107
|
+
end
|
108
|
+
|
109
|
+
def start
|
110
|
+
puts "Starting Ractor #{@name}"
|
111
|
+
@ractor = Ractor.new(@name) do |name|
|
112
|
+
puts "Ractor #{name} started"
|
113
|
+
Ractor.yield({ type: :initialize, processor: name })
|
114
|
+
worker = MyWorker.new
|
115
|
+
|
116
|
+
loop do
|
117
|
+
puts "Waiting for work in #{name}"
|
118
|
+
work = Ractor.receive
|
119
|
+
puts "Received work #{work} in #{name}"
|
120
|
+
begin
|
121
|
+
result = worker.process(work)
|
122
|
+
puts "Sending result #{result} from Ractor #{name}"
|
123
|
+
Ractor.yield({ type: :result, result: result })
|
124
|
+
rescue StandardError => e
|
125
|
+
puts "Error processing work #{work} in Ractor #{name}: #{e.message}"
|
126
|
+
Ractor.yield({ type: :error, error: e.message, processor: name, work: work })
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def ractor
|
133
|
+
@ractor
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Supervisor
|
138
|
+
# Removed failed_queue from attr_reader
|
139
|
+
attr_reader :work_queue, :workers, :results
|
140
|
+
|
141
|
+
def initialize(num_workers = 2)
|
142
|
+
@work_queue = Queue.new
|
143
|
+
@results = ResultAggregator.new
|
144
|
+
# @failed_queue = Queue.new # Removed failed_queue
|
145
|
+
@num_workers = num_workers
|
146
|
+
@workers = []
|
147
|
+
@total_work_count = 0 # Track total items initially added
|
148
|
+
# @shutdown_requested = false # Removed shutdown flag
|
149
|
+
end
|
150
|
+
|
151
|
+
def add_work(items)
|
152
|
+
items.each { |item| @work_queue << item }
|
153
|
+
@total_work_count += items.size # Increment initial work count
|
154
|
+
puts "Work added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def start_workers
|
158
|
+
@workers = (1..@num_workers).map do |i|
|
159
|
+
MyRactor.new("worker #{i}")
|
160
|
+
end
|
161
|
+
@workers.each(&:start)
|
162
|
+
puts "Workers started"
|
163
|
+
end
|
164
|
+
|
165
|
+
def setup_signal_handler
|
166
|
+
# No need for Ractor.current here anymore
|
167
|
+
# Need access to @workers within the trap block
|
168
|
+
workers_ref = @workers
|
169
|
+
Signal.trap("INT") do
|
170
|
+
puts "\nCtrl+C received. Initiating immediate shutdown..."
|
171
|
+
# Attempt to close worker Ractors before exiting
|
172
|
+
puts "Attempting to close worker Ractors..."
|
173
|
+
workers_ref.each do |w|
|
174
|
+
begin
|
175
|
+
# Check if ractor exists and is not closed
|
176
|
+
if w && w.respond_to?(:ractor) && w.ractor && !w.ractor.closed?
|
177
|
+
w.ractor.close
|
178
|
+
puts "Closed Ractor: #{w.ractor}"
|
179
|
+
end
|
180
|
+
rescue => e # Catch potential errors during close
|
181
|
+
puts "Error closing Ractor #{w.ractor rescue 'unknown'}: #{e.message}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
puts "Exiting now."
|
185
|
+
exit(1) # Exit immediately
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def run
|
190
|
+
setup_signal_handler # Sets up the immediate exit trap
|
191
|
+
start_workers
|
192
|
+
|
193
|
+
# Removed the initial work distribution loop.
|
194
|
+
# The main loop will handle sending work upon receiving :initialize message.
|
195
|
+
|
196
|
+
# Main loop: Process events until the number of results equals the initial work count.
|
197
|
+
# The signal trap handles immediate exit.
|
198
|
+
while (@results.results.size + @results.errors.size) < @total_work_count
|
199
|
+
processed_count = @results.results.size + @results.errors.size
|
200
|
+
puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
|
201
|
+
|
202
|
+
# Only select from worker ractors now
|
203
|
+
ready_ractors = @workers.map(&:ractor).compact
|
204
|
+
# Safety break if all workers somehow finished/closed unexpectedly AND no work left
|
205
|
+
# This condition might need refinement depending on exact desired behavior if workers die.
|
206
|
+
break if ready_ractors.empty? && @work_queue.empty? && processed_count < @total_work_count
|
207
|
+
|
208
|
+
# Ractor.select will block until a worker sends a message
|
209
|
+
# If ready_ractors is empty but loop continues, select would raise error. Added break above.
|
210
|
+
next if ready_ractors.empty? # Skip iteration if no workers available but loop condition met (e.g., waiting for final results)
|
211
|
+
|
212
|
+
ractor, completed_work = Ractor.select(*ready_ractors)
|
213
|
+
|
214
|
+
puts "Selected Ractor returned: #{ractor}, completed work: #{completed_work}"
|
215
|
+
|
216
|
+
# Process the received message
|
217
|
+
case completed_work[:type]
|
218
|
+
when :initialize
|
219
|
+
puts "Initializing Ractor: #{completed_work[:processor]}"
|
220
|
+
# Send work if available
|
221
|
+
if !@work_queue.empty?
|
222
|
+
queued_work = @work_queue.pop # Pop before sending
|
223
|
+
puts "Sending initial work #{queued_work} to initialized Ractor: #{ractor}"
|
224
|
+
ractor.send(MyWork.new(queued_work))
|
225
|
+
puts "Initial work sent to #{completed_work[:processor]}."
|
226
|
+
else
|
227
|
+
puts "Work queue empty when Ractor #{completed_work[:processor]} initialized."
|
228
|
+
end
|
229
|
+
when :result
|
230
|
+
puts "Completed work: #{completed_work[:result]} in Ractor: #{completed_work[:processor]}"
|
231
|
+
@results.add_result(completed_work[:result])
|
232
|
+
# No need to decrement a counter here, loop condition checks total results
|
233
|
+
puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
|
234
|
+
puts "Results: #{@results.inspect}"
|
235
|
+
# Call helper to send next work
|
236
|
+
send_next_work_if_available(ractor)
|
237
|
+
when :error
|
238
|
+
error_result = WorkResult.new(error: completed_work[:error], work: completed_work[:work])
|
239
|
+
puts "Error processing work #{error_result.work} in Ractor: #{completed_work[:processor]}: #{error_result.error}"
|
240
|
+
# Removed adding to failed_queue
|
241
|
+
@results.add_result(error_result) # This adds it to the errors array in the aggregator
|
242
|
+
# No need to decrement a counter here, loop condition checks total results
|
243
|
+
puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
|
244
|
+
# Removed Failed Queue size log
|
245
|
+
puts "Results (including errors): #{@results.inspect}"
|
246
|
+
# Call helper to send next work
|
247
|
+
send_next_work_if_available(ractor)
|
248
|
+
else
|
249
|
+
# Log unknown message types from workers
|
250
|
+
puts "Unknown message type received: #{completed_work[:type]} from #{ractor}"
|
251
|
+
end
|
252
|
+
# Loop continues based on the while condition at the top
|
253
|
+
end
|
254
|
+
|
255
|
+
# Removed DEBUG LOG for failed_queue
|
256
|
+
|
257
|
+
# This part might not be reached if exit(1) is called in the trap
|
258
|
+
puts "Main loop finished."
|
259
|
+
puts "Final Results: #{@results.inspect}"
|
260
|
+
# Removed Failed Work Queue size log
|
261
|
+
# Optionally print failed items
|
262
|
+
# until @failed_queue.empty?
|
263
|
+
# puts "Failed: #{@failed_queue.pop.inspect}"
|
264
|
+
# end
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
# Helper method to send next work item if available
|
270
|
+
def send_next_work_if_available(ractor)
|
271
|
+
# Ensure the ractor is valid before attempting to send
|
272
|
+
# Ractor.select should only return active ractors, so closed? check is removed.
|
273
|
+
unless ractor.nil?
|
274
|
+
if !@work_queue.empty?
|
275
|
+
queued_work = @work_queue.pop # Pop before sending
|
276
|
+
puts "Sending next work #{queued_work} to Ractor: #{ractor}"
|
277
|
+
ractor.send(MyWork.new(queued_work))
|
278
|
+
puts "Work sent."
|
279
|
+
else
|
280
|
+
puts "Work queue empty. Not sending new work to Ractor #{ractor}."
|
281
|
+
end
|
282
|
+
else
|
283
|
+
puts "Attempted to send work to an invalid or closed Ractor."
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# --- Main Execution ---
|
289
|
+
if __FILE__ == $0
|
290
|
+
supervisor = Supervisor.new(2) # Create supervisor with 2 workers
|
291
|
+
|
292
|
+
# Add work items
|
293
|
+
work_items = (1..10).to_a
|
294
|
+
supervisor.add_work(work_items)
|
295
|
+
|
296
|
+
# Run the supervisor
|
297
|
+
supervisor.run
|
298
|
+
|
299
|
+
puts "Processing complete."
|
300
|
+
puts "Final Aggregated Results:"
|
301
|
+
puts supervisor.results.inspect
|
302
|
+
|
303
|
+
# Print failed items directly from the ResultAggregator's errors array
|
304
|
+
failed_items = supervisor.results.errors # Access the errors array
|
305
|
+
puts "\nFailed Work Items (#{failed_items.size}):"
|
306
|
+
# Inspect each item individually for better readability if they are objects
|
307
|
+
# The items are already WorkResult objects
|
308
|
+
puts failed_items.map(&:inspect).inspect
|
309
|
+
end
|