fractor 0.1.0 → 0.1.2
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.yml +2 -0
- data/.rubocop_todo.yml +98 -0
- data/README.adoc +281 -41
- data/examples/hierarchical_hasher/README.adoc +75 -0
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +150 -0
- data/examples/multi_work_type/README.adoc +45 -0
- data/examples/multi_work_type/multi_work_type.rb +319 -0
- data/examples/pipeline_processing/README.adoc +44 -0
- data/examples/pipeline_processing/pipeline_processing.rb +216 -0
- data/examples/producer_subscriber/README.adoc +92 -0
- data/examples/producer_subscriber/producer_subscriber.rb +256 -0
- data/examples/scatter_gather/README.adoc +43 -0
- data/examples/scatter_gather/scatter_gather.rb +327 -0
- data/examples/simple/sample.rb +101 -0
- data/examples/specialized_workers/README.adoc +45 -0
- data/examples/specialized_workers/specialized_workers.rb +395 -0
- data/lib/fractor/result_aggregator.rb +10 -1
- data/lib/fractor/supervisor.rb +141 -70
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/worker.rb +5 -0
- data/lib/fractor/wrapped_ractor.rb +1 -1
- data/lib/fractor.rb +7 -9
- metadata +16 -5
- data/examples/hierarchical_hasher.rb +0 -158
- data/examples/producer_subscriber.rb +0 -300
- data/sample.rb +0 -64
data/lib/fractor/supervisor.rb
CHANGED
@@ -1,56 +1,89 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'thread' # Required for Queue
|
4
|
-
|
5
3
|
module Fractor
|
6
4
|
# Supervises multiple WrappedRactors, distributes work, and aggregates results.
|
7
5
|
class Supervisor
|
8
|
-
attr_reader :work_queue, :workers, :results, :
|
6
|
+
attr_reader :work_queue, :workers, :results, :worker_pools
|
9
7
|
|
10
8
|
# Initializes the Supervisor.
|
11
|
-
# -
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
9
|
+
# - worker_pools: An array of worker pool configurations, each containing:
|
10
|
+
# - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
|
11
|
+
# - num_workers: The number of Ractors to spawn for this worker class.
|
12
|
+
# - continuous_mode: Whether to run in continuous mode without expecting a fixed work count.
|
13
|
+
def initialize(worker_pools: [], continuous_mode: false)
|
14
|
+
@worker_pools = worker_pools.map do |pool_config|
|
15
|
+
worker_class = pool_config[:worker_class]
|
16
|
+
num_workers = pool_config[:num_workers] || 2
|
17
|
+
|
18
|
+
raise ArgumentError, "#{worker_class} must inherit from Fractor::Worker" unless worker_class < Fractor::Worker
|
19
|
+
|
20
|
+
{
|
21
|
+
worker_class: worker_class,
|
22
|
+
num_workers: num_workers,
|
23
|
+
workers: [] # Will hold the WrappedRactor instances
|
24
|
+
}
|
20
25
|
end
|
21
26
|
|
22
|
-
@worker_class = worker_class
|
23
|
-
@work_class = work_class
|
24
27
|
@work_queue = Queue.new
|
25
28
|
@results = ResultAggregator.new
|
26
|
-
@
|
27
|
-
@workers = []
|
29
|
+
@workers = [] # Flattened array of all workers across all pools
|
28
30
|
@total_work_count = 0 # Track total items initially added
|
29
31
|
@ractors_map = {} # Map Ractor object to WrappedRactor instance
|
32
|
+
@continuous_mode = continuous_mode
|
33
|
+
@running = false
|
34
|
+
@work_callbacks = []
|
35
|
+
end
|
36
|
+
|
37
|
+
# Adds a single work item to the queue.
|
38
|
+
# The item must be an instance of Fractor::Work or a subclass.
|
39
|
+
def add_work_item(work)
|
40
|
+
raise ArgumentError, "#{work.class} must be an instance of Fractor::Work" unless work.is_a?(Fractor::Work)
|
41
|
+
|
42
|
+
@work_queue << work
|
43
|
+
@total_work_count += 1
|
44
|
+
return unless ENV["FRACTOR_DEBUG"]
|
45
|
+
|
46
|
+
puts "Work item added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Alias for better naming
|
50
|
+
alias add_work_item add_work_item
|
51
|
+
|
52
|
+
# Adds multiple work items to the queue.
|
53
|
+
# Each item must be an instance of Fractor::Work or a subclass.
|
54
|
+
def add_work_items(works)
|
55
|
+
works.each do |work|
|
56
|
+
add_work_item(work)
|
57
|
+
end
|
30
58
|
end
|
31
59
|
|
32
|
-
#
|
33
|
-
#
|
34
|
-
def
|
35
|
-
|
36
|
-
@total_work_count += items.size
|
37
|
-
puts "Work added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
|
60
|
+
# Register a callback to provide new work items
|
61
|
+
# The callback should return nil or empty array when no new work is available
|
62
|
+
def register_work_source(&callback)
|
63
|
+
@work_callbacks << callback
|
38
64
|
end
|
39
65
|
|
40
|
-
# Starts the worker Ractors.
|
66
|
+
# Starts the worker Ractors for all worker pools.
|
41
67
|
def start_workers
|
42
|
-
@
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
68
|
+
@worker_pools.each do |pool|
|
69
|
+
worker_class = pool[:worker_class]
|
70
|
+
num_workers = pool[:num_workers]
|
71
|
+
|
72
|
+
pool[:workers] = (1..num_workers).map do |i|
|
73
|
+
wrapped_ractor = WrappedRactor.new("worker #{worker_class}:#{i}", worker_class)
|
74
|
+
wrapped_ractor.start # Start the underlying Ractor
|
75
|
+
# Map the actual Ractor object to the WrappedRactor instance
|
76
|
+
@ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
|
77
|
+
wrapped_ractor
|
78
|
+
end.compact
|
49
79
|
end
|
50
|
-
|
51
|
-
|
80
|
+
|
81
|
+
# Flatten all workers for easier access
|
82
|
+
@workers = @worker_pools.flat_map { |pool| pool[:workers] }
|
52
83
|
@ractors_map.compact! # Ensure map doesn't contain nil keys/values
|
53
|
-
|
84
|
+
return unless ENV["FRACTOR_DEBUG"]
|
85
|
+
|
86
|
+
puts "Workers started: #{@workers.size} active across #{@worker_pools.size} pools."
|
54
87
|
end
|
55
88
|
|
56
89
|
# Sets up a signal handler for graceful shutdown (Ctrl+C).
|
@@ -61,12 +94,10 @@ module Fractor
|
|
61
94
|
puts "\nCtrl+C received. Initiating immediate shutdown..."
|
62
95
|
puts "Attempting to close worker Ractors..."
|
63
96
|
workers_ref.each do |w|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
puts "Error closing Ractor #{w.name}: #{e.message}"
|
69
|
-
end
|
97
|
+
w.close # Use the close method of WrappedRactor
|
98
|
+
puts "Closed Ractor: #{w.name}"
|
99
|
+
rescue StandardError => e
|
100
|
+
puts "Error closing Ractor #{w.name}: #{e.message}"
|
70
101
|
end
|
71
102
|
puts "Exiting now."
|
72
103
|
exit(1) # Exit immediately
|
@@ -78,24 +109,46 @@ module Fractor
|
|
78
109
|
setup_signal_handler
|
79
110
|
start_workers
|
80
111
|
|
112
|
+
@running = true
|
81
113
|
processed_count = 0
|
82
|
-
|
83
|
-
|
114
|
+
|
115
|
+
# Main loop: Process events until conditions are met for termination
|
116
|
+
while @running && (@continuous_mode || processed_count < @total_work_count)
|
84
117
|
processed_count = @results.results.size + @results.errors.size
|
85
|
-
|
118
|
+
|
119
|
+
if ENV["FRACTOR_DEBUG"]
|
120
|
+
if @continuous_mode
|
121
|
+
puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{@work_queue.size}"
|
122
|
+
else
|
123
|
+
puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
|
124
|
+
end
|
125
|
+
end
|
86
126
|
|
87
127
|
# Get active Ractor objects from the map keys
|
88
|
-
# Use keys from ractors_map for the active ractors
|
89
128
|
active_ractors = @ractors_map.keys
|
90
129
|
|
130
|
+
# Check for new work from callbacks if in continuous mode and queue is empty
|
131
|
+
if @continuous_mode && @work_queue.empty? && !@work_callbacks.empty?
|
132
|
+
@work_callbacks.each do |callback|
|
133
|
+
new_work = callback.call
|
134
|
+
add_work_items(new_work) if new_work && !new_work.empty?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
91
138
|
# Break if no active workers and queue is empty, but work remains (indicates potential issue)
|
92
|
-
if active_ractors.empty? && @work_queue.empty? && processed_count < @total_work_count
|
93
|
-
|
94
|
-
|
139
|
+
if active_ractors.empty? && @work_queue.empty? && !@continuous_mode && processed_count < @total_work_count
|
140
|
+
puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if ENV["FRACTOR_DEBUG"]
|
141
|
+
break
|
95
142
|
end
|
96
143
|
|
97
|
-
#
|
98
|
-
|
144
|
+
# In continuous mode, just wait if no active ractors but keep running
|
145
|
+
if active_ractors.empty?
|
146
|
+
break unless @continuous_mode
|
147
|
+
|
148
|
+
sleep(0.1) # Small delay to avoid CPU spinning
|
149
|
+
next
|
150
|
+
|
151
|
+
end
|
99
152
|
|
100
153
|
# Ractor.select blocks until a message is available from any active Ractor
|
101
154
|
ready_ractor_obj, message = Ractor.select(*active_ractors)
|
@@ -103,47 +156,61 @@ module Fractor
|
|
103
156
|
# Find the corresponding WrappedRactor instance
|
104
157
|
wrapped_ractor = @ractors_map[ready_ractor_obj]
|
105
158
|
unless wrapped_ractor
|
106
|
-
puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring."
|
159
|
+
puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring." if ENV["FRACTOR_DEBUG"]
|
107
160
|
next
|
108
161
|
end
|
109
162
|
|
110
|
-
puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}"
|
163
|
+
puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if ENV["FRACTOR_DEBUG"]
|
111
164
|
|
112
165
|
# Process the received message
|
113
166
|
case message[:type]
|
114
167
|
when :initialize
|
115
|
-
puts "Ractor initialized: #{message[:processor]}"
|
168
|
+
puts "Ractor initialized: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
116
169
|
# Send work immediately upon initialization if available
|
117
170
|
send_next_work_if_available(wrapped_ractor)
|
118
171
|
when :result
|
119
172
|
# The message[:result] should be a WorkResult object
|
120
173
|
work_result = message[:result]
|
121
|
-
puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}"
|
174
|
+
puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
|
122
175
|
@results.add_result(work_result)
|
123
|
-
|
124
|
-
|
176
|
+
if ENV["FRACTOR_DEBUG"]
|
177
|
+
puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}"
|
178
|
+
puts "Aggregated Results: #{@results.inspect}" unless @continuous_mode
|
179
|
+
end
|
125
180
|
# Send next piece of work
|
126
181
|
send_next_work_if_available(wrapped_ractor)
|
127
182
|
when :error
|
128
183
|
# The message[:result] should be a WorkResult object containing the error
|
129
184
|
error_result = message[:result]
|
130
|
-
puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}"
|
185
|
+
puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}" if ENV["FRACTOR_DEBUG"]
|
131
186
|
@results.add_result(error_result) # Add error to aggregator
|
132
|
-
|
133
|
-
|
187
|
+
if ENV["FRACTOR_DEBUG"]
|
188
|
+
puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}"
|
189
|
+
puts "Aggregated Results (including errors): #{@results.inspect}" unless @continuous_mode
|
190
|
+
end
|
134
191
|
# Send next piece of work even after an error
|
135
192
|
send_next_work_if_available(wrapped_ractor)
|
136
193
|
else
|
137
|
-
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}"
|
194
|
+
puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
|
138
195
|
end
|
139
196
|
# Update processed count for the loop condition
|
140
197
|
processed_count = @results.results.size + @results.errors.size
|
141
198
|
end
|
142
199
|
|
143
|
-
puts "Main loop finished."
|
200
|
+
puts "Main loop finished." if ENV["FRACTOR_DEBUG"]
|
201
|
+
return if @continuous_mode
|
202
|
+
|
203
|
+
return unless ENV["FRACTOR_DEBUG"]
|
204
|
+
|
144
205
|
puts "Final Aggregated Results: #{@results.inspect}"
|
145
206
|
end
|
146
207
|
|
208
|
+
# Stop the supervisor (for continuous mode)
|
209
|
+
def stop
|
210
|
+
@running = false
|
211
|
+
puts "Stopping supervisor..."
|
212
|
+
end
|
213
|
+
|
147
214
|
private
|
148
215
|
|
149
216
|
# Helper method to send the next available work item to a specific Ractor.
|
@@ -151,21 +218,25 @@ module Fractor
|
|
151
218
|
# Ensure the wrapped_ractor instance is valid and its underlying ractor is not closed
|
152
219
|
if wrapped_ractor && !wrapped_ractor.closed?
|
153
220
|
if !@work_queue.empty?
|
154
|
-
|
155
|
-
|
156
|
-
work_item
|
157
|
-
puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}"
|
221
|
+
work_item = @work_queue.pop # Now directly a Work object
|
222
|
+
|
223
|
+
puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
|
158
224
|
wrapped_ractor.send(work_item) # Send the Work object
|
159
|
-
puts "Work sent to #{wrapped_ractor.name}."
|
225
|
+
puts "Work sent to #{wrapped_ractor.name}." if ENV["FRACTOR_DEBUG"]
|
160
226
|
else
|
161
|
-
puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}."
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
227
|
+
puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}." if ENV["FRACTOR_DEBUG"]
|
228
|
+
# In continuous mode, don't close workers as more work may come
|
229
|
+
unless @continuous_mode
|
230
|
+
# Consider closing the Ractor if the queue is empty and no more work is expected.
|
231
|
+
# wrapped_ractor.close
|
232
|
+
# @ractors_map.delete(wrapped_ractor.ractor)
|
233
|
+
# if ENV["FRACTOR_DEBUG"]
|
234
|
+
# puts "Closed idle Ractor: #{wrapped_ractor.name}"
|
235
|
+
# end
|
236
|
+
end
|
166
237
|
end
|
167
238
|
else
|
168
|
-
puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name ||
|
239
|
+
puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || "unknown"}." if ENV["FRACTOR_DEBUG"]
|
169
240
|
# Remove from map if found but closed
|
170
241
|
@ractors_map.delete(wrapped_ractor.ractor) if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
|
171
242
|
end
|
data/lib/fractor/version.rb
CHANGED
data/lib/fractor/worker.rb
CHANGED
@@ -4,6 +4,11 @@ module Fractor
|
|
4
4
|
# Base class for defining work processors.
|
5
5
|
# Subclasses must implement the `process` method.
|
6
6
|
class Worker
|
7
|
+
def initialize(name: nil, **options)
|
8
|
+
@name = name
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
7
12
|
def process(work)
|
8
13
|
raise NotImplementedError, "Subclasses must implement the 'process' method."
|
9
14
|
end
|
@@ -25,7 +25,7 @@ module Fractor
|
|
25
25
|
Ractor.yield({ type: :initialize, processor: name })
|
26
26
|
|
27
27
|
# Instantiate the specific worker inside the Ractor
|
28
|
-
worker = worker_cls.new
|
28
|
+
worker = worker_cls.new(name: name)
|
29
29
|
|
30
30
|
loop do
|
31
31
|
# Ractor.receive will block until a message is received
|
data/lib/fractor.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'thread' # Required for Queue
|
4
|
-
|
5
3
|
# Require all component files
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
4
|
+
require_relative "fractor/version"
|
5
|
+
require_relative "fractor/worker"
|
6
|
+
require_relative "fractor/work"
|
7
|
+
require_relative "fractor/work_result"
|
8
|
+
require_relative "fractor/result_aggregator"
|
9
|
+
require_relative "fractor/wrapped_ractor"
|
10
|
+
require_relative "fractor/supervisor"
|
13
11
|
|
14
12
|
# Fractor: Function-driven Ractors framework
|
15
13
|
module Fractor
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fractor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ronald Tse
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Fractor is a lightweight Ruby framework designed to simplify the process
|
14
14
|
of distributing computational work across multiple Ractors.
|
@@ -20,11 +20,23 @@ extra_rdoc_files: []
|
|
20
20
|
files:
|
21
21
|
- ".rspec"
|
22
22
|
- ".rubocop.yml"
|
23
|
+
- ".rubocop_todo.yml"
|
23
24
|
- CODE_OF_CONDUCT.md
|
24
25
|
- README.adoc
|
25
26
|
- Rakefile
|
26
|
-
- examples/hierarchical_hasher.
|
27
|
-
- examples/
|
27
|
+
- examples/hierarchical_hasher/README.adoc
|
28
|
+
- examples/hierarchical_hasher/hierarchical_hasher.rb
|
29
|
+
- examples/multi_work_type/README.adoc
|
30
|
+
- examples/multi_work_type/multi_work_type.rb
|
31
|
+
- examples/pipeline_processing/README.adoc
|
32
|
+
- examples/pipeline_processing/pipeline_processing.rb
|
33
|
+
- examples/producer_subscriber/README.adoc
|
34
|
+
- examples/producer_subscriber/producer_subscriber.rb
|
35
|
+
- examples/scatter_gather/README.adoc
|
36
|
+
- examples/scatter_gather/scatter_gather.rb
|
37
|
+
- examples/simple/sample.rb
|
38
|
+
- examples/specialized_workers/README.adoc
|
39
|
+
- examples/specialized_workers/specialized_workers.rb
|
28
40
|
- lib/fractor.rb
|
29
41
|
- lib/fractor/result_aggregator.rb
|
30
42
|
- lib/fractor/supervisor.rb
|
@@ -33,7 +45,6 @@ files:
|
|
33
45
|
- lib/fractor/work_result.rb
|
34
46
|
- lib/fractor/worker.rb
|
35
47
|
- lib/fractor/wrapped_ractor.rb
|
36
|
-
- sample.rb
|
37
48
|
- sig/fractor.rbs
|
38
49
|
- tests/sample.rb.bak
|
39
50
|
- tests/sample_working.rb.bak
|
@@ -1,158 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'digest'
|
4
|
-
require_relative '../lib/fractor'
|
5
|
-
|
6
|
-
module HierarchicalHasher
|
7
|
-
class ChunkWork < Fractor::Work
|
8
|
-
work_type :chunk_hash
|
9
|
-
attr_reader :start, :length, :data
|
10
|
-
|
11
|
-
def initialize(start, length, data)
|
12
|
-
super(nil, { start: start, length: length, data: data })
|
13
|
-
@start = start
|
14
|
-
@length = length
|
15
|
-
@data = data
|
16
|
-
@retry_count = 0
|
17
|
-
@max_retries = 3
|
18
|
-
end
|
19
|
-
|
20
|
-
def should_retry?
|
21
|
-
@retry_count < @max_retries
|
22
|
-
end
|
23
|
-
|
24
|
-
def failed
|
25
|
-
@retry_count += 1
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
class ChunkResult < Fractor::WorkResult
|
30
|
-
attr_reader :start, :length, :hash_result
|
31
|
-
|
32
|
-
def initialize(work, hash_result)
|
33
|
-
super(work, hash_result)
|
34
|
-
@start = work.start
|
35
|
-
@length = work.length
|
36
|
-
@hash_result = hash_result
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class HashWorker < Fractor::Worker
|
41
|
-
work_type_accepted :chunk_hash
|
42
|
-
|
43
|
-
def process_work(work)
|
44
|
-
# Simulate some processing time
|
45
|
-
sleep(rand(0.01..0.05))
|
46
|
-
|
47
|
-
# Calculate SHA-3 hash for the chunk
|
48
|
-
hash = Digest::SHA3.hexdigest(work.data)
|
49
|
-
|
50
|
-
# Return the result
|
51
|
-
ChunkResult.new(work, hash)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
class HashResultAssembler < Fractor::ResultAssembler
|
56
|
-
def finalize
|
57
|
-
return nil if @results.empty?
|
58
|
-
|
59
|
-
# Sort results by start position
|
60
|
-
sorted_results = @results.sort_by { |result| result.start }
|
61
|
-
|
62
|
-
# Concatenate all hashes with newlines
|
63
|
-
combined_hash_string = sorted_results.map(&:hash_result).join("\n")
|
64
|
-
|
65
|
-
# Calculate final SHA-3 hash
|
66
|
-
Digest::SHA3.hexdigest(combined_hash_string)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class HashSupervisor < Fractor::Supervisor
|
71
|
-
def initialize(file_path, chunk_size = 1024, worker_count = 4)
|
72
|
-
super()
|
73
|
-
@file_path = file_path
|
74
|
-
@chunk_size = chunk_size
|
75
|
-
@worker_count = worker_count
|
76
|
-
@assembler = HashResultAssembler.new
|
77
|
-
|
78
|
-
# Create a queue for chunk work
|
79
|
-
chunk_queue = Fractor::Queue.new(work_types: [:chunk_hash])
|
80
|
-
add_queue(chunk_queue)
|
81
|
-
|
82
|
-
# Create a worker pool
|
83
|
-
worker_pool = Fractor::Pool.new(size: @worker_count)
|
84
|
-
@worker_count.times do
|
85
|
-
worker_pool.add_worker(HashWorker.new)
|
86
|
-
end
|
87
|
-
add_pool(worker_pool)
|
88
|
-
|
89
|
-
# Load the file and create work chunks
|
90
|
-
load_file_chunks
|
91
|
-
end
|
92
|
-
|
93
|
-
def load_file_chunks
|
94
|
-
File.open(@file_path, 'rb') do |file|
|
95
|
-
start_pos = 0
|
96
|
-
|
97
|
-
while chunk = file.read(@chunk_size)
|
98
|
-
work = ChunkWork.new(start_pos, chunk.length, chunk)
|
99
|
-
@queues.first.push(work)
|
100
|
-
start_pos += chunk.length
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def process_results
|
106
|
-
until @result_queue.empty? && @queues.all?(&:empty?) && @idle_workers == @active_workers.size
|
107
|
-
result_data = next_result
|
108
|
-
next if result_data.nil?
|
109
|
-
|
110
|
-
type, *data = result_data
|
111
|
-
|
112
|
-
case type
|
113
|
-
when :result
|
114
|
-
result = data.first
|
115
|
-
@assembler.add_result(result)
|
116
|
-
when :error
|
117
|
-
work, error = data
|
118
|
-
@assembler.add_failed_work(work, error)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Small sleep to prevent CPU spinning
|
122
|
-
sleep 0.001
|
123
|
-
end
|
124
|
-
|
125
|
-
# Return the final hash
|
126
|
-
@assembler.finalize
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Example usage
|
132
|
-
if __FILE__ == $PROGRAM_NAME
|
133
|
-
if ARGV.empty?
|
134
|
-
puts "Usage: ruby hierarchical_hasher.rb <file_path> [worker_count]"
|
135
|
-
exit 1
|
136
|
-
end
|
137
|
-
|
138
|
-
file_path = ARGV[0]
|
139
|
-
worker_count = (ARGV[1] || 4).to_i
|
140
|
-
|
141
|
-
unless File.exist?(file_path)
|
142
|
-
puts "Error: File '#{file_path}' not found"
|
143
|
-
exit 1
|
144
|
-
end
|
145
|
-
|
146
|
-
puts "Starting hierarchical hasher with #{worker_count} workers..."
|
147
|
-
puts "Processing file: #{file_path}"
|
148
|
-
|
149
|
-
start_time = Time.now
|
150
|
-
supervisor = HierarchicalHasher::HashSupervisor.new(file_path, 1024, worker_count)
|
151
|
-
final_hash = supervisor.start
|
152
|
-
end_time = Time.now
|
153
|
-
|
154
|
-
puts "Final SHA-3 hash: #{final_hash}"
|
155
|
-
puts "Processing completed in #{end_time - start_time} seconds"
|
156
|
-
|
157
|
-
supervisor.shutdown
|
158
|
-
end
|