fractor 0.1.0 → 0.1.1

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.
@@ -1,56 +1,91 @@
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, :worker_class, :work_class
6
+ attr_reader :work_queue, :workers, :results, :worker_pools
9
7
 
10
8
  # Initializes the Supervisor.
11
- # - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
12
- # - work_class: The class inheriting from Fractor::Work (e.g., MyWork).
13
- # - num_workers: The number of Ractors to spawn.
14
- def initialize(worker_class:, work_class:, num_workers: 2)
15
- unless worker_class < Fractor::Worker
16
- raise ArgumentError, "#{worker_class} must inherit from Fractor::Worker"
17
- end
18
- unless work_class < Fractor::Work
19
- raise ArgumentError, "#{work_class} must inherit from Fractor::Work"
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
- @num_workers = num_workers
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
+ unless work.is_a?(Fractor::Work)
41
+ raise ArgumentError, "#{work.class} must be an instance of Fractor::Work"
42
+ end
43
+
44
+ @work_queue << work
45
+ @total_work_count += 1
46
+ return unless ENV["FRACTOR_DEBUG"]
47
+
48
+ puts "Work item added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
49
+ end
50
+
51
+ # Alias for better naming
52
+ alias add_work_item add_work_item
53
+
54
+ # Adds multiple work items to the queue.
55
+ # Each item must be an instance of Fractor::Work or a subclass.
56
+ def add_work_items(works)
57
+ works.each do |work|
58
+ add_work_item(work)
59
+ end
30
60
  end
31
61
 
32
- # Adds work items to the queue.
33
- # Items should be the raw input data, not Work objects yet.
34
- def add_work(items)
35
- items.each { |item| @work_queue << item }
36
- @total_work_count += items.size
37
- puts "Work added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
62
+ # Register a callback to provide new work items
63
+ # The callback should return nil or empty array when no new work is available
64
+ def register_work_source(&callback)
65
+ @work_callbacks << callback
38
66
  end
39
67
 
40
- # Starts the worker Ractors.
68
+ # Starts the worker Ractors for all worker pools.
41
69
  def start_workers
42
- @workers = (1..@num_workers).map do |i|
43
- # Pass the client's worker class (e.g., MyWorker) to WrappedRactor
44
- wrapped_ractor = WrappedRactor.new("worker #{i}", @worker_class)
45
- wrapped_ractor.start # Start the underlying Ractor
46
- # Map the actual Ractor object to the WrappedRactor instance
47
- @ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
48
- wrapped_ractor
70
+ @worker_pools.each do |pool|
71
+ worker_class = pool[:worker_class]
72
+ num_workers = pool[:num_workers]
73
+
74
+ pool[:workers] = (1..num_workers).map do |i|
75
+ wrapped_ractor = WrappedRactor.new("worker #{worker_class}:#{i}", worker_class)
76
+ wrapped_ractor.start # Start the underlying Ractor
77
+ # Map the actual Ractor object to the WrappedRactor instance
78
+ @ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
79
+ wrapped_ractor
80
+ end.compact
49
81
  end
50
- # Filter out any workers that failed to start properly
51
- @workers.compact!
82
+
83
+ # Flatten all workers for easier access
84
+ @workers = @worker_pools.flat_map { |pool| pool[:workers] }
52
85
  @ractors_map.compact! # Ensure map doesn't contain nil keys/values
53
- puts "Workers started: #{@workers.size} active."
86
+ return unless ENV["FRACTOR_DEBUG"]
87
+
88
+ puts "Workers started: #{@workers.size} active across #{@worker_pools.size} pools."
54
89
  end
55
90
 
56
91
  # Sets up a signal handler for graceful shutdown (Ctrl+C).
@@ -61,12 +96,10 @@ module Fractor
61
96
  puts "\nCtrl+C received. Initiating immediate shutdown..."
62
97
  puts "Attempting to close worker Ractors..."
63
98
  workers_ref.each do |w|
64
- begin
65
- w.close # Use the close method of WrappedRactor
66
- puts "Closed Ractor: #{w.name}"
67
- rescue => e
68
- puts "Error closing Ractor #{w.name}: #{e.message}"
69
- end
99
+ w.close # Use the close method of WrappedRactor
100
+ puts "Closed Ractor: #{w.name}"
101
+ rescue StandardError => e
102
+ puts "Error closing Ractor #{w.name}: #{e.message}"
70
103
  end
71
104
  puts "Exiting now."
72
105
  exit(1) # Exit immediately
@@ -78,24 +111,48 @@ module Fractor
78
111
  setup_signal_handler
79
112
  start_workers
80
113
 
114
+ @running = true
81
115
  processed_count = 0
82
- # Main loop: Process events until the number of results equals the initial work count.
83
- while processed_count < @total_work_count
116
+
117
+ # Main loop: Process events until conditions are met for termination
118
+ while @running && (@continuous_mode || processed_count < @total_work_count)
84
119
  processed_count = @results.results.size + @results.errors.size
85
- puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
120
+
121
+ if ENV["FRACTOR_DEBUG"]
122
+ if @continuous_mode
123
+ puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{@work_queue.size}"
124
+ else
125
+ puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
126
+ end
127
+ end
86
128
 
87
129
  # Get active Ractor objects from the map keys
88
- # Use keys from ractors_map for the active ractors
89
130
  active_ractors = @ractors_map.keys
90
131
 
132
+ # Check for new work from callbacks if in continuous mode and queue is empty
133
+ if @continuous_mode && @work_queue.empty? && !@work_callbacks.empty?
134
+ @work_callbacks.each do |callback|
135
+ new_work = callback.call
136
+ add_work_items(new_work) if new_work && !new_work.empty?
137
+ end
138
+ end
139
+
91
140
  # 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
- puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop."
94
- break
141
+ if active_ractors.empty? && @work_queue.empty? && !@continuous_mode && processed_count < @total_work_count
142
+ if ENV["FRACTOR_DEBUG"]
143
+ puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop."
144
+ end
145
+ break
95
146
  end
96
147
 
97
- # Skip selection if no active ractors are available but loop should continue (e.g., waiting for final results)
98
- next if active_ractors.empty?
148
+ # In continuous mode, just wait if no active ractors but keep running
149
+ if active_ractors.empty?
150
+ break unless @continuous_mode
151
+
152
+ sleep(0.1) # Small delay to avoid CPU spinning
153
+ next
154
+
155
+ end
99
156
 
100
157
  # Ractor.select blocks until a message is available from any active Ractor
101
158
  ready_ractor_obj, message = Ractor.select(*active_ractors)
@@ -103,47 +160,75 @@ module Fractor
103
160
  # Find the corresponding WrappedRactor instance
104
161
  wrapped_ractor = @ractors_map[ready_ractor_obj]
105
162
  unless wrapped_ractor
106
- puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring."
163
+ if ENV["FRACTOR_DEBUG"]
164
+ puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring."
165
+ end
107
166
  next
108
167
  end
109
168
 
110
- puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}"
169
+ if ENV["FRACTOR_DEBUG"]
170
+ puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}"
171
+ end
111
172
 
112
173
  # Process the received message
113
174
  case message[:type]
114
175
  when :initialize
115
- puts "Ractor initialized: #{message[:processor]}"
176
+ if ENV["FRACTOR_DEBUG"]
177
+ puts "Ractor initialized: #{message[:processor]}"
178
+ end
116
179
  # Send work immediately upon initialization if available
117
180
  send_next_work_if_available(wrapped_ractor)
118
181
  when :result
119
182
  # The message[:result] should be a WorkResult object
120
183
  work_result = message[:result]
121
- puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}"
184
+ if ENV["FRACTOR_DEBUG"]
185
+ puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}"
186
+ end
122
187
  @results.add_result(work_result)
123
- puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
124
- puts "Aggregated Results: #{@results.inspect}"
188
+ if ENV["FRACTOR_DEBUG"]
189
+ puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}"
190
+ puts "Aggregated Results: #{@results.inspect}" unless @continuous_mode
191
+ end
125
192
  # Send next piece of work
126
193
  send_next_work_if_available(wrapped_ractor)
127
194
  when :error
128
195
  # The message[:result] should be a WorkResult object containing the error
129
196
  error_result = message[:result]
130
- puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}"
197
+ if ENV["FRACTOR_DEBUG"]
198
+ puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}"
199
+ end
131
200
  @results.add_result(error_result) # Add error to aggregator
132
- puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
133
- puts "Aggregated Results (including errors): #{@results.inspect}"
201
+ if ENV["FRACTOR_DEBUG"]
202
+ puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}"
203
+ puts "Aggregated Results (including errors): #{@results.inspect}" unless @continuous_mode
204
+ end
134
205
  # Send next piece of work even after an error
135
206
  send_next_work_if_available(wrapped_ractor)
136
207
  else
137
- puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}"
208
+ if ENV["FRACTOR_DEBUG"]
209
+ puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}"
210
+ end
138
211
  end
139
212
  # Update processed count for the loop condition
140
213
  processed_count = @results.results.size + @results.errors.size
141
214
  end
142
215
 
143
- puts "Main loop finished."
216
+ if ENV["FRACTOR_DEBUG"]
217
+ puts "Main loop finished."
218
+ end
219
+ return if @continuous_mode
220
+
221
+ return unless ENV["FRACTOR_DEBUG"]
222
+
144
223
  puts "Final Aggregated Results: #{@results.inspect}"
145
224
  end
146
225
 
226
+ # Stop the supervisor (for continuous mode)
227
+ def stop
228
+ @running = false
229
+ puts "Stopping supervisor..."
230
+ end
231
+
147
232
  private
148
233
 
149
234
  # Helper method to send the next available work item to a specific Ractor.
@@ -151,21 +236,33 @@ module Fractor
151
236
  # Ensure the wrapped_ractor instance is valid and its underlying ractor is not closed
152
237
  if wrapped_ractor && !wrapped_ractor.closed?
153
238
  if !@work_queue.empty?
154
- raw_input = @work_queue.pop # Get raw input data
155
- # Create an instance of the client's Work class (e.g., MyWork)
156
- work_item = @work_class.new(raw_input)
157
- puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}"
239
+ work_item = @work_queue.pop # Now directly a Work object
240
+
241
+ if ENV["FRACTOR_DEBUG"]
242
+ puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}"
243
+ end
158
244
  wrapped_ractor.send(work_item) # Send the Work object
159
- puts "Work sent to #{wrapped_ractor.name}."
245
+ if ENV["FRACTOR_DEBUG"]
246
+ puts "Work sent to #{wrapped_ractor.name}."
247
+ end
160
248
  else
161
- puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}."
162
- # Consider closing the Ractor if the queue is empty and no more work is expected.
163
- # wrapped_ractor.close
164
- # @ractors_map.delete(wrapped_ractor.ractor)
165
- # puts "Closed idle Ractor: #{wrapped_ractor.name}"
249
+ if ENV["FRACTOR_DEBUG"]
250
+ puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}."
251
+ end
252
+ # In continuous mode, don't close workers as more work may come
253
+ unless @continuous_mode
254
+ # Consider closing the Ractor if the queue is empty and no more work is expected.
255
+ # wrapped_ractor.close
256
+ # @ractors_map.delete(wrapped_ractor.ractor)
257
+ # if ENV["FRACTOR_DEBUG"]
258
+ # puts "Closed idle Ractor: #{wrapped_ractor.name}"
259
+ # end
260
+ end
166
261
  end
167
262
  else
168
- puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || 'unknown'}."
263
+ if ENV["FRACTOR_DEBUG"]
264
+ puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || "unknown"}."
265
+ end
169
266
  # Remove from map if found but closed
170
267
  @ractors_map.delete(wrapped_ractor.ractor) if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
171
268
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fractor
4
4
  # Fractor version
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
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 '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'
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.0
4
+ version: 0.1.1
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-05 00:00:00.000000000 Z
11
+ date: 2025-05-07 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.rb
27
- - examples/producer_subscriber.rb
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