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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +82 -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 +167 -70
- data/lib/fractor/version.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
@@ -1,300 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../lib/fractor'
|
4
|
-
|
5
|
-
module ProducerSubscriber
|
6
|
-
# Define work types
|
7
|
-
class InitialWork < Fractor::Work
|
8
|
-
work_type :initial_processing
|
9
|
-
attr_reader :data, :depth
|
10
|
-
|
11
|
-
def initialize(data, depth = 0)
|
12
|
-
super(data: { data: data, depth: depth })
|
13
|
-
@data = data
|
14
|
-
@depth = depth
|
15
|
-
@retry_count = 0
|
16
|
-
@max_retries = 2
|
17
|
-
end
|
18
|
-
|
19
|
-
def should_retry?
|
20
|
-
@retry_count < @max_retries
|
21
|
-
end
|
22
|
-
|
23
|
-
def failed
|
24
|
-
@retry_count += 1
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class SubWork < Fractor::Work
|
29
|
-
work_type :sub_processing
|
30
|
-
attr_reader :data, :parent_id, :depth
|
31
|
-
|
32
|
-
def initialize(data, parent_id, depth)
|
33
|
-
super(data: { data: data, parent_id: parent_id, depth: depth })
|
34
|
-
@data = data
|
35
|
-
@parent_id = parent_id
|
36
|
-
@depth = depth
|
37
|
-
@retry_count = 0
|
38
|
-
@max_retries = 2
|
39
|
-
end
|
40
|
-
|
41
|
-
def should_retry?
|
42
|
-
@retry_count < @max_retries
|
43
|
-
end
|
44
|
-
|
45
|
-
def failed
|
46
|
-
@retry_count += 1
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Define work results
|
51
|
-
class InitialWorkResult < Fractor::WorkResult
|
52
|
-
attr_reader :processed_data, :sub_works
|
53
|
-
|
54
|
-
def initialize(work, processed_data, sub_works = [])
|
55
|
-
super(work, processed_data)
|
56
|
-
@processed_data = processed_data
|
57
|
-
@sub_works = sub_works
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class SubWorkResult < Fractor::WorkResult
|
62
|
-
attr_reader :processed_data, :parent_id
|
63
|
-
|
64
|
-
def initialize(work, processed_data)
|
65
|
-
super(work, processed_data)
|
66
|
-
@processed_data = processed_data
|
67
|
-
@parent_id = work.parent_id
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
# Define worker that can handle both types of work
|
72
|
-
class MultiWorker < Fractor::Worker
|
73
|
-
work_type_accepted [:initial_processing, :sub_processing]
|
74
|
-
|
75
|
-
def process_work(work)
|
76
|
-
case work.work_type
|
77
|
-
when :initial_processing
|
78
|
-
process_initial_work(work)
|
79
|
-
when :sub_processing
|
80
|
-
process_sub_work(work)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def process_initial_work(work)
|
87
|
-
# Simulate processing time
|
88
|
-
sleep(rand(0.01..0.05))
|
89
|
-
|
90
|
-
# Process the data
|
91
|
-
processed_data = "Processed: #{work.data}"
|
92
|
-
|
93
|
-
# Create sub-works if we're not too deep
|
94
|
-
sub_works = []
|
95
|
-
if work.depth < 2 # Limit recursion depth
|
96
|
-
# Split the work into smaller chunks
|
97
|
-
3.times do |i|
|
98
|
-
sub_data = "#{work.data}-#{i}"
|
99
|
-
sub_works << SubWork.new(sub_data, work.object_id, work.depth + 1)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Return result with sub-works
|
104
|
-
InitialWorkResult.new(work, processed_data, sub_works)
|
105
|
-
end
|
106
|
-
|
107
|
-
def process_sub_work(work)
|
108
|
-
# Simulate processing time
|
109
|
-
sleep(rand(0.01..0.03))
|
110
|
-
|
111
|
-
# Process the data
|
112
|
-
processed_data = "Sub-processed: #{work.data} (depth: #{work.depth})"
|
113
|
-
|
114
|
-
# Create more sub-works if we're not too deep
|
115
|
-
sub_works = []
|
116
|
-
if work.depth < 3 # Limit recursion depth
|
117
|
-
# Create fewer sub-works as we go deeper
|
118
|
-
(4 - work.depth).times do |i|
|
119
|
-
sub_data = "#{work.data}-#{i}"
|
120
|
-
sub_works << SubWork.new(sub_data, work.parent_id, work.depth + 1)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Return result with any new sub-works
|
125
|
-
result = SubWorkResult.new(work, processed_data)
|
126
|
-
[result, sub_works]
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
# Define result assembler
|
131
|
-
class TreeResultAssembler < Fractor::ResultAssembler
|
132
|
-
def initialize
|
133
|
-
super()
|
134
|
-
@result_tree = {}
|
135
|
-
@pending_work_count = 0
|
136
|
-
end
|
137
|
-
|
138
|
-
def track_new_work(count = 1)
|
139
|
-
@pending_work_count += count
|
140
|
-
end
|
141
|
-
|
142
|
-
def work_completed
|
143
|
-
@pending_work_count -= 1
|
144
|
-
end
|
145
|
-
|
146
|
-
def add_result(result)
|
147
|
-
super(result)
|
148
|
-
work_completed
|
149
|
-
|
150
|
-
case result
|
151
|
-
when InitialWorkResult
|
152
|
-
@result_tree[result.work.object_id] = {
|
153
|
-
data: result.processed_data,
|
154
|
-
children: []
|
155
|
-
}
|
156
|
-
when SubWorkResult
|
157
|
-
parent = @result_tree[result.parent_id]
|
158
|
-
if parent
|
159
|
-
parent[:children] << result.processed_data
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def all_work_complete?
|
165
|
-
@pending_work_count <= 0
|
166
|
-
end
|
167
|
-
|
168
|
-
def finalize
|
169
|
-
# Build a formatted tree representation
|
170
|
-
format_tree
|
171
|
-
end
|
172
|
-
|
173
|
-
private
|
174
|
-
|
175
|
-
def format_tree
|
176
|
-
result = []
|
177
|
-
@result_tree.each do |id, node|
|
178
|
-
result << "Root: #{node[:data]}"
|
179
|
-
node[:children].each_with_index do |child, index|
|
180
|
-
result << " ├─ Child #{index + 1}: #{child}"
|
181
|
-
end
|
182
|
-
result << ""
|
183
|
-
end
|
184
|
-
result.join("\n")
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
# Define supervisor
|
189
|
-
class TreeSupervisor < Fractor::Supervisor
|
190
|
-
def initialize(initial_data, worker_count = 4)
|
191
|
-
super()
|
192
|
-
@initial_data = initial_data
|
193
|
-
@worker_count = worker_count
|
194
|
-
@assembler = TreeResultAssembler.new
|
195
|
-
|
196
|
-
# Create a queue that can handle both work types
|
197
|
-
work_queue = Fractor::Queue.new(work_types: [:initial_processing, :sub_processing])
|
198
|
-
add_queue(work_queue)
|
199
|
-
|
200
|
-
# Create a worker pool
|
201
|
-
worker_pool = Fractor::Pool.new(size: @worker_count)
|
202
|
-
@worker_count.times do
|
203
|
-
worker_pool.add_worker(MultiWorker.new)
|
204
|
-
end
|
205
|
-
add_pool(worker_pool)
|
206
|
-
|
207
|
-
# Create initial work
|
208
|
-
initial_data.each do |data|
|
209
|
-
work = InitialWork.new(data)
|
210
|
-
@queues.first.push(work)
|
211
|
-
@assembler.track_new_work
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def process_results
|
216
|
-
until @assembler.all_work_complete? && @queues.all?(&:empty?)
|
217
|
-
result_data = next_result
|
218
|
-
next if result_data.nil?
|
219
|
-
|
220
|
-
type, *data = result_data
|
221
|
-
|
222
|
-
case type
|
223
|
-
when :result
|
224
|
-
result = data.first
|
225
|
-
|
226
|
-
if result.is_a?(Array)
|
227
|
-
# Handle the case where we get a result and sub-works
|
228
|
-
sub_result, sub_works = result
|
229
|
-
@assembler.add_result(sub_result)
|
230
|
-
|
231
|
-
# Add new sub-works to the queue
|
232
|
-
if sub_works && !sub_works.empty?
|
233
|
-
@assembler.track_new_work(sub_works.size)
|
234
|
-
sub_works.each do |work|
|
235
|
-
@queues.first.push(work)
|
236
|
-
end
|
237
|
-
end
|
238
|
-
else
|
239
|
-
# Handle regular result
|
240
|
-
@assembler.add_result(result)
|
241
|
-
|
242
|
-
# If this result generated sub-works, add them to the queue
|
243
|
-
if result.respond_to?(:sub_works) && !result.sub_works.empty?
|
244
|
-
@assembler.track_new_work(result.sub_works.size)
|
245
|
-
result.sub_works.each do |work|
|
246
|
-
@queues.first.push(work)
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
when :error
|
252
|
-
work, error = data
|
253
|
-
@assembler.add_failed_work(work, error)
|
254
|
-
@assembler.work_completed
|
255
|
-
end
|
256
|
-
|
257
|
-
# Small sleep to prevent CPU spinning
|
258
|
-
sleep 0.001
|
259
|
-
end
|
260
|
-
|
261
|
-
# Return the final assembled result
|
262
|
-
@assembler.finalize
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
# Example usage: Document processing system
|
268
|
-
if __FILE__ == $PROGRAM_NAME
|
269
|
-
puts "Starting producer-subscriber example: Document Processing System"
|
270
|
-
puts "This example simulates a document processing system where:"
|
271
|
-
puts "1. Initial documents are broken down into sections"
|
272
|
-
puts "2. Sections are further broken down into paragraphs"
|
273
|
-
puts "3. Paragraphs are processed individually"
|
274
|
-
puts "4. Results are assembled into a hierarchical structure"
|
275
|
-
puts
|
276
|
-
|
277
|
-
# Sample documents to process
|
278
|
-
documents = [
|
279
|
-
"Annual Report 2025",
|
280
|
-
"Technical Documentation",
|
281
|
-
"Research Paper"
|
282
|
-
]
|
283
|
-
|
284
|
-
worker_count = 4
|
285
|
-
puts "Using #{worker_count} workers to process #{documents.size} documents"
|
286
|
-
puts
|
287
|
-
|
288
|
-
start_time = Time.now
|
289
|
-
supervisor = ProducerSubscriber::TreeSupervisor.new(documents, worker_count)
|
290
|
-
result = supervisor.start
|
291
|
-
end_time = Time.now
|
292
|
-
|
293
|
-
puts "Processing Results:"
|
294
|
-
puts "==================="
|
295
|
-
puts result
|
296
|
-
puts
|
297
|
-
puts "Processing completed in #{end_time - start_time} seconds"
|
298
|
-
|
299
|
-
supervisor.shutdown
|
300
|
-
end
|
data/sample.rb
DELETED
@@ -1,64 +0,0 @@
|
|
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
|