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.
@@ -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: []