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,209 @@
|
|
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
|
+
# TODO: Create a class that contains the work queue and the results
|
138
|
+
q = Queue.new
|
139
|
+
(1..10).each do |i|
|
140
|
+
q << i
|
141
|
+
end
|
142
|
+
|
143
|
+
puts "Queue: #{q.inspect}"
|
144
|
+
puts "Queue size: #{q.size}"
|
145
|
+
|
146
|
+
# Create Ractors
|
147
|
+
workers = (1..2).map do |i|
|
148
|
+
MyRactor.new("worker #{i}")
|
149
|
+
end
|
150
|
+
|
151
|
+
results = ResultAggregator.new
|
152
|
+
result_number = q.size
|
153
|
+
|
154
|
+
workers.each(&:start)
|
155
|
+
puts "Ractors started"
|
156
|
+
|
157
|
+
CR = Ractor.current
|
158
|
+
|
159
|
+
Signal.trap("INT") do
|
160
|
+
puts "\nCtrl+C received. Requesting graceful shutdown..."
|
161
|
+
CR.send({ type: :shutdown })
|
162
|
+
puts "Shutdown request sent to Ractor #{CR}"
|
163
|
+
# exit(1)
|
164
|
+
# Do not exit here, let the main loop break
|
165
|
+
end
|
166
|
+
|
167
|
+
while !q.empty?
|
168
|
+
puts "Queue not empty, still contains #{q.size} items"
|
169
|
+
|
170
|
+
puts "Selecting Ractor"
|
171
|
+
ractor, completed_work = Ractor.select(*workers.map(&:ractor), CR)
|
172
|
+
|
173
|
+
puts "Selected Ractor returned: #{ractor}, completed work: #{completed_work}"
|
174
|
+
|
175
|
+
case completed_work[:type]
|
176
|
+
when :shutdown
|
177
|
+
puts "Shutdown request received in Ractor: #{completed_work[:processor]}"
|
178
|
+
break
|
179
|
+
when :initialize
|
180
|
+
puts "Initializing Ractor: #{completed_work[:processor]}"
|
181
|
+
when :result
|
182
|
+
puts "Completed work: #{completed_work[:result]} in Ractor: #{completed_work[:processor]}"
|
183
|
+
results.add_result(completed_work[:result])
|
184
|
+
puts "Results: #{results.inspect}"
|
185
|
+
|
186
|
+
# TODO: Instead of using a separate variable, we should rely on an object
|
187
|
+
# that contains both the work queue and their results
|
188
|
+
result_number -= 1
|
189
|
+
break if result_number == 0
|
190
|
+
when :error
|
191
|
+
# TODO: We have to put these on a "Failed Work" queue
|
192
|
+
puts "Error processing work #{completed_work[:work]} in Ractor: #{completed_work[:processor]}: #{completed_work[:error]}"
|
193
|
+
errors << { error: completed_work[:error], work: completed_work[:work] }
|
194
|
+
result_number -= 1
|
195
|
+
break if result_number == 0
|
196
|
+
else
|
197
|
+
puts "Unknown message type received: #{completed_work[:type]}"
|
198
|
+
end
|
199
|
+
|
200
|
+
puts "Sending work to Ractor"
|
201
|
+
puts "Queue: #{q.inspect}"
|
202
|
+
queued_work = q.pop
|
203
|
+
puts "Popped work: #{queued_work} from queue"
|
204
|
+
puts "Sending work to Ractor: #{ractor}"
|
205
|
+
ractor.send(MyWork.new(queued_work))
|
206
|
+
puts "Work sent to Ractor: #{ractor}"
|
207
|
+
end
|
208
|
+
|
209
|
+
puts "Results: #{results.inspect}"
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fractor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ronald Tse
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Fractor is a lightweight Ruby framework designed to simplify the process
|
14
|
+
of distributing computational work across multiple Ractors.
|
15
|
+
email:
|
16
|
+
- ronald.tse@ribose.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rspec"
|
22
|
+
- ".rubocop.yml"
|
23
|
+
- CODE_OF_CONDUCT.md
|
24
|
+
- README.adoc
|
25
|
+
- Rakefile
|
26
|
+
- examples/hierarchical_hasher.rb
|
27
|
+
- examples/producer_subscriber.rb
|
28
|
+
- lib/fractor.rb
|
29
|
+
- lib/fractor/result_aggregator.rb
|
30
|
+
- lib/fractor/supervisor.rb
|
31
|
+
- lib/fractor/version.rb
|
32
|
+
- lib/fractor/work.rb
|
33
|
+
- lib/fractor/work_result.rb
|
34
|
+
- lib/fractor/worker.rb
|
35
|
+
- lib/fractor/wrapped_ractor.rb
|
36
|
+
- sample.rb
|
37
|
+
- sig/fractor.rbs
|
38
|
+
- tests/sample.rb.bak
|
39
|
+
- tests/sample_working.rb.bak
|
40
|
+
homepage: https://github.com/ribose/fractor
|
41
|
+
licenses: []
|
42
|
+
metadata:
|
43
|
+
allowed_push_host: https://rubygems.org
|
44
|
+
homepage_uri: https://github.com/ribose/fractor
|
45
|
+
source_code_uri: https://github.com/ribose/fractor
|
46
|
+
changelog_uri: https://github.com/ribose/fractor/blob/main/CHANGELOG.md
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 3.0.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.5.22
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Function-driven Ractors framework for Ruby
|
66
|
+
test_files: []
|