ractor-shim 0.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5132b882154ee4c0af587fb5e4b2f10030ef9834237abbbae5417e3fb51b62a8
4
- data.tar.gz: 0ed22147461e8c6d3feae42d18fd06247ed6d8df800dc6fb2211e56ba9f807dc
3
+ metadata.gz: ba3ed0a7cfb270c23651d31c50286068defa1b0492459286cabc055da64ae2c8
4
+ data.tar.gz: 98ed41fb52ac1676960b4d6a49d49e66920c4465a779da9b1e5f12073427cee6
5
5
  SHA512:
6
- metadata.gz: bbabf61d3ee523150b793e177a2c60d14cad3ed94848a2290bd9d02f86175f251a641a24cb1841eb01a749e14b7dcbbb94fdcac1c8f23cc1b927a3ac3d533b4e
7
- data.tar.gz: 9264645ee001fbb8f301163a28f88e1e3431639312ad4c249060a75d80f45d69afaae5b7504a3b3c6175eac7325d204a31e1a78a264c7d50a47052be4b7a18b7
6
+ metadata.gz: 55647c412c0fdecac6feb0b7fcf353c86d2114979eb6aa9ee8c1c0521c3a05e35123ea9db82ba2d808f483bfe244add8b2140cad1267121d8f1cfdeb329250c9
7
+ data.tar.gz: 10dd59ab7425af30137bb2fa0edf416447cdd01a16aa6601d3df57a396b1833638e78197ca4e827169223ab23d9a94fe48f7ef6ff9f8e85522303a6cd8e9dc6a
data/README.md CHANGED
@@ -1,18 +1,39 @@
1
1
  # ractor-shim
2
2
 
3
- A shim to define `Ractor` by using `Thread`, if `Ractor` is not already defined.
3
+ A shim to define `Ractor` by using `Thread`, `Queue`, etc if `Ractor` is not already defined.
4
4
 
5
- This is notably useful to run programs needing `Ractor` on Ruby implementations which don't define `Ractor.
5
+ This is notably useful to run programs relying on `Ractor` on Ruby implementations which don't define `Ractor` such as TruffleRuby and JRuby.
6
+
7
+ Note that TruffleRuby and JRuby both run Ruby code in threads in parallel, so this gem enables using the Ractor API on these Rubies and run Ractors in parallel.
8
+
9
+ The gem also provides the Ruby 3.5 Ractor API (`Ractor::Port`, `Ractor#{join,value,monitor}`, etc) for CRuby 2.7 to 3.4.
10
+
11
+ When `Ractor` is not already defined, this gem implements `Ractor.make_shareable(object)` by just returning `object` to avoid unnecessary overhead.
12
+ The main reason is that the CRuby implementation of `Ractor.make_shareable` returns an immutable object, so it won't be mutated by the program anyway and there is no need to deep freeze.
13
+ Another reason is many gems do `Ractor.make_shareable(object) if defined?(Ractor)` and so that becomes dependent on whether `Ractor` is defined before that gem loads. That problem disappears if `Ractor.make_shareable(object)` is noop.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ $ gem install ractor-shim
19
+ ```
20
+ or
21
+ ```ruby
22
+ # In Gemfile
23
+ gem "ractor-shim"
24
+ ```
6
25
 
7
26
  ## Usage
8
27
 
9
28
  ```ruby
10
29
  require 'ractor/shim'
30
+
31
+ Ractor.new { ... }
11
32
  ```
12
33
 
13
34
  ## Development
14
35
 
15
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
36
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `ractor-shim.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
16
37
 
17
38
  ## Contributing
18
39
 
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ task default: [:test]
5
+
6
+ task :test do
7
+ exec RbConfig.ruby, "-Ilib", "test/run_tests.rb"
8
+ end
data/lib/ractor/shim.rb CHANGED
@@ -1,3 +1,424 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shim/version"
3
+ builtin_ractor = !!defined?(Ractor)
4
+ class Ractor
5
+ end
6
+ Ractor.define_singleton_method(:builtin?) { builtin_ractor }
7
+ Ractor.define_singleton_method(:shim?) { !builtin_ractor }
8
+
9
+ # Ractor::Port
10
+
11
+ if Ractor.builtin?
12
+ class Ractor::Port
13
+ QUIT = Object.new.freeze
14
+
15
+ def initialize
16
+ @pipe = Ractor.new do
17
+ while true
18
+ msg = Ractor.receive
19
+ break if QUIT.equal?(msg)
20
+ Ractor.yield msg
21
+ end
22
+ end
23
+ end
24
+
25
+ def send(message)
26
+ @pipe.send(message)
27
+ end
28
+ alias_method :<<, :send
29
+
30
+ def receive
31
+ @pipe.take
32
+ end
33
+
34
+ def close
35
+ @pipe.send(QUIT)
36
+ end
37
+
38
+ def closed?
39
+ @pipe.inspect.end_with?(' terminated>')
40
+ end
41
+
42
+ def inspect
43
+ super
44
+ end
45
+ end unless defined?(Ractor::Port)
46
+ else
47
+ class Ractor::Port
48
+ attr_reader :queue
49
+ private :queue
50
+
51
+ def initialize
52
+ @queue = Queue.new
53
+ end
54
+
55
+ def send(message, move: false)
56
+ Ractor::SELECT_MUTEX.synchronize {
57
+ @queue << message
58
+ Ractor::SELECT_CV.broadcast
59
+ }
60
+ self
61
+ end
62
+ alias_method :<<, :send
63
+
64
+ def receive
65
+ @queue.pop
66
+ end
67
+
68
+ def close
69
+ Ractor::SELECT_MUTEX.synchronize {
70
+ @queue.close
71
+ Ractor::SELECT_CV.broadcast
72
+ }
73
+ self
74
+ end
75
+
76
+ def closed?
77
+ @queue.closed?
78
+ end
79
+
80
+ def inspect
81
+ super
82
+ end
83
+ end
84
+ end
85
+
86
+ if Ractor.shim?
87
+ class Ractor
88
+ class Error < RuntimeError
89
+ end
90
+
91
+ class RemoteError < Error
92
+ attr_reader :ractor
93
+ def initialize(ractor)
94
+ @ractor = ractor
95
+ end
96
+ end
97
+
98
+ class ClosedError < StopIteration
99
+ end
100
+
101
+ @count = 1
102
+ COUNT_MUTEX = Mutex.new
103
+ CHANGE_COUNT = -> delta {
104
+ COUNT_MUTEX.synchronize { @count += delta }
105
+ }
106
+
107
+ @id = 0
108
+ ID_MUTEX = Mutex.new
109
+ GET_ID = -> {
110
+ ID_MUTEX.synchronize { @id += 1 }
111
+ }
112
+
113
+ SELECT_MUTEX = Mutex.new
114
+ SELECT_CV = ConditionVariable.new
115
+
116
+ def self.main
117
+ MAIN_RACTOR
118
+ end
119
+
120
+ def self.main?
121
+ current == main
122
+ end
123
+
124
+ def self.current
125
+ Thread.current.thread_variable_get(:current_ractor) or raise "Could not find current Ractor"
126
+ end
127
+
128
+ def self.count
129
+ COUNT_MUTEX.synchronize { @count }
130
+ end
131
+
132
+ def self.receive
133
+ Ractor.current.__send__(:receive)
134
+ end
135
+
136
+ class << self
137
+ alias_method :recv, :receive
138
+
139
+ def new(...)
140
+ super(...)
141
+ end
142
+ end
143
+
144
+ # def self.select_simple_spinning(*ractors)
145
+ # raise ArgumentError, "specify at least one ractor or `yield_value`" if ractors.empty?
146
+ # while true
147
+ # ractors.each do |ractor_or_port|
148
+ # if Ractor === ractor_or_port
149
+ # queue = ractor_or_port.out_queue
150
+ # elsif Ractor::Port === ractor_or_port
151
+ # queue = ractor_or_port.queue
152
+ # else
153
+ # raise ArgumentError, "Unexpected argument for Ractor.select: #{ractor_or_port}"
154
+ # end
155
+ #
156
+ # begin
157
+ # value = queue.pop(true)
158
+ # return [ractor_or_port, value]
159
+ # rescue ThreadError
160
+ # Thread.pass
161
+ # end
162
+ # end
163
+ # end
164
+ # end
165
+
166
+ def self.select(*ractors)
167
+ raise ArgumentError, "specify at least one ractor or `yield_value`" if ractors.empty?
168
+
169
+ SELECT_MUTEX.synchronize do
170
+ while true
171
+ ractors.each do |ractor_or_port|
172
+ if Ractor === ractor_or_port
173
+ queue = ractor_or_port.__send__(:out_queue)
174
+ elsif Ractor::Port === ractor_or_port
175
+ queue = ractor_or_port.__send__(:queue)
176
+ else
177
+ raise ArgumentError, "Unexpected argument for Ractor.select: #{ractor_or_port}"
178
+ end
179
+
180
+ begin
181
+ value = queue.pop(true)
182
+ return [ractor_or_port, value]
183
+ rescue ThreadError
184
+ # keep looping
185
+ end
186
+ end
187
+
188
+ # Wait until an item is added to a relevant Queue
189
+ SELECT_CV.wait(SELECT_MUTEX)
190
+ end
191
+ end
192
+ end
193
+
194
+ def self.make_shareable(object, copy: false)
195
+ # no copy, just return the object
196
+ object
197
+ end
198
+
199
+ def self.shareable?(object)
200
+ true
201
+ end
202
+
203
+ def self.[](var)
204
+ Ractor.current[var]
205
+ end
206
+
207
+ def self.[]=(var, value)
208
+ Ractor.current[var] = value
209
+ end
210
+
211
+ def self.store_if_absent(var, &block)
212
+ Ractor.current.__send__(:store_if_absent, var, &block)
213
+ end
214
+
215
+ def self._require(feature)
216
+ Kernel.require(feature)
217
+ end
218
+
219
+ attr_reader :name, :default_port
220
+
221
+ attr_reader :out_queue
222
+ private :out_queue
223
+
224
+ def initialize(*args, name: nil, &block)
225
+ raise ArgumentError, "must be called with a block" unless block
226
+ initialize_common(name, block)
227
+ CHANGE_COUNT.call(1)
228
+
229
+ @thread = Thread.new {
230
+ Thread.current.thread_variable_set(:current_ractor, self)
231
+ begin
232
+ result = self.instance_exec(*args, &block)
233
+ SELECT_MUTEX.synchronize {
234
+ unless @out_queue.closed?
235
+ @out_queue << result
236
+ SELECT_CV.broadcast
237
+ end
238
+ }
239
+ result
240
+ rescue Exception => e
241
+ @exception = e
242
+ nil
243
+ ensure
244
+ @termination_mutex.synchronize {
245
+ CHANGE_COUNT.call(-1)
246
+ @status = :terminated
247
+
248
+ monitor_message = @exception ? :aborted : :exited
249
+ @monitors.each { |monitor|
250
+ monitor << monitor_message
251
+ }
252
+ @monitors.clear
253
+ }
254
+ end
255
+ }
256
+ end
257
+
258
+ private def initialize_main
259
+ initialize_common(nil, nil)
260
+ @thread = nil
261
+ end
262
+
263
+ private def initialize_common(name, block)
264
+ @name = name.nil? ? nil : (String.try_convert(name) or raise TypeError)
265
+ @id = GET_ID.call
266
+ @status = :running
267
+ @from = block ? block.source_location.join(":") : nil
268
+ @default_port = Ractor::Port.new
269
+ @out_queue = Queue.new
270
+ @storage = {}
271
+ @exception = nil
272
+ @monitors = []
273
+ @termination_mutex = Mutex.new
274
+ end
275
+
276
+ def [](var)
277
+ raise "Cannot get ractor local storage for non-current ractor" unless Ractor.current == self
278
+ @storage[var]
279
+ end
280
+
281
+ def []=(var, value)
282
+ raise "Cannot set ractor local storage for non-current ractor" unless Ractor.current == self
283
+ @storage[var] = value
284
+ end
285
+
286
+ private def store_if_absent(var, &block)
287
+ if value = @storage[var]
288
+ value
289
+ else
290
+ value = block.call
291
+ @storage[var] = value
292
+ value
293
+ end
294
+ end
295
+
296
+ def send(message, ...)
297
+ raise Ractor::ClosedError, "The port was already closed" if @status == :terminated || @default_port.closed?
298
+ @default_port.send(message, ...)
299
+ self
300
+ end
301
+ alias_method :<<, :send
302
+
303
+ private def receive
304
+ @default_port.receive
305
+ end
306
+
307
+ # def take
308
+ # @out_queue.pop
309
+ # end
310
+
311
+ private def close_incoming
312
+ @default_port.close
313
+ self
314
+ end
315
+
316
+ private def close_outgoing
317
+ SELECT_MUTEX.synchronize {
318
+ @out_queue.close
319
+ SELECT_CV.broadcast
320
+ }
321
+ self
322
+ end
323
+
324
+ def close
325
+ close_incoming
326
+ close_outgoing
327
+ end
328
+
329
+ def monitor(port)
330
+ @termination_mutex.synchronize {
331
+ if @status == :terminated
332
+ port << (@exception ? :aborted : :exited)
333
+ false
334
+ else
335
+ @monitors << port
336
+ end
337
+ }
338
+ end
339
+
340
+ def unmonitor(port)
341
+ @termination_mutex.synchronize {
342
+ @monitors.delete port
343
+ }
344
+ end
345
+
346
+ def join
347
+ value
348
+ self
349
+ end
350
+
351
+ def value
352
+ @thread.join
353
+
354
+ if exc = @exception
355
+ remote_error = RemoteError.new(self)
356
+ raise remote_error, cause: exc
357
+ end
358
+
359
+ @thread.value
360
+ end
361
+
362
+ def inspect
363
+ ["#<Ractor:##{@id}", @name, @from, "#{@status}>"].compact.join(' ')
364
+ end
365
+ alias_method :to_s, :inspect
366
+
367
+ MAIN_RACTOR = Ractor.allocate
368
+ MAIN_RACTOR.__send__(:initialize_main)
369
+ Thread.main.thread_variable_set(:current_ractor, MAIN_RACTOR)
370
+ end
371
+ end
372
+
373
+ if Ractor.builtin?
374
+ class Ractor
375
+ unless method_defined?(:join)
376
+ alias_method :join, :take
377
+ end
378
+
379
+ unless method_defined?(:value)
380
+ alias_method :value, :take
381
+ end
382
+
383
+ unless respond_to?(:main?)
384
+ def self.main?
385
+ self == Ractor.main
386
+ end
387
+ end
388
+
389
+ unless method_defined?(:close)
390
+ def close
391
+ close_incoming
392
+ close_outgoing
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ # Ractor.{shareable_proc,shareable_lambda}
399
+
400
+ class << Ractor
401
+ if Ractor.builtin?
402
+ unless method_defined?(:shareable_proc)
403
+ def shareable_proc(&b)
404
+ Ractor.make_shareable(b)
405
+ end
406
+ end
407
+
408
+ unless method_defined?(:shareable_lambda)
409
+ def shareable_lambda(&b)
410
+ Ractor.make_shareable(b)
411
+ end
412
+ end
413
+ else
414
+ unless method_defined?(:shareable_proc)
415
+ alias_method :shareable_proc, :proc
416
+ public :shareable_proc
417
+ end
418
+
419
+ unless method_defined?(:shareable_lambda)
420
+ alias_method :shareable_lambda, :lambda
421
+ public :shareable_lambda
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,16 @@
1
+ []
2
+ []=
3
+ _require
4
+ count
5
+ current
6
+ main
7
+ main?
8
+ make_shareable
9
+ new
10
+ receive
11
+ recv
12
+ select
13
+ shareable?
14
+ shareable_lambda
15
+ shareable_proc
16
+ store_if_absent
@@ -0,0 +1,13 @@
1
+ <<
2
+ []
3
+ []=
4
+ close
5
+ default_port
6
+ inspect
7
+ join
8
+ monitor
9
+ name
10
+ send
11
+ to_s
12
+ unmonitor
13
+ value
File without changes
@@ -0,0 +1,6 @@
1
+ <<
2
+ close
3
+ closed?
4
+ inspect
5
+ receive
6
+ send
@@ -0,0 +1,83 @@
1
+ # Based on https://github.com/truffleruby/truffleruby/blob/master/spec/truffle/methods_spec.rb
2
+
3
+ # How to regenerate files:
4
+ #
5
+ # - switch to MRI, the version we are compatible with
6
+ # - run `jt -u ruby test spec/truffle/methods_spec.rb`
7
+
8
+ # jt test and jt tag can be used as normal,
9
+ # but instead of jt untag, jt purge must be used to remove tags:
10
+ # $ jt purge spec/truffle/methods_spec.rb
11
+
12
+ # socket modules are found with:
13
+ # m1=ObjectSpace.each_object(Module).to_a; require "socket"; m2=ObjectSpace.each_object(Module).to_a; p m2-m1
14
+
15
+ modules = %w[
16
+ Ractor Ractor.singleton_class
17
+ Ractor::Port Ractor::Port.singleton_class
18
+ ]
19
+
20
+ def ruby(code, *flags)
21
+ IO.popen([RbConfig.ruby, *flags], "r+") { |pipe|
22
+ pipe.write code
23
+ pipe.close_write
24
+ pipe.read
25
+ }
26
+ end
27
+
28
+ if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "3.5"
29
+ modules.each do |mod|
30
+ file = "#{__dir__}/methods/#{mod}.txt"
31
+ code = "puts #{mod}.public_instance_methods(false).sort"
32
+ methods = ruby(code)
33
+ methods = methods.lines.map { |line| line.chomp.to_sym }
34
+ contents = methods.map { |meth| "#{meth}\n" }.join
35
+ File.write file, contents
36
+ end
37
+ end
38
+
39
+ code = <<-RUBY
40
+ require "ractor/shim"
41
+ #{modules.inspect}.each { |m|
42
+ puts m
43
+ puts eval(m).public_instance_methods(false).sort
44
+ puts
45
+ }
46
+ RUBY
47
+ all_methods = {}
48
+ ruby(code, "-I#{File.dirname(__dir__)}/lib").rstrip.split("\n\n").each do |group|
49
+ mod, *methods = group.lines.map(&:chomp)
50
+ all_methods[mod] = methods.map(&:to_sym)
51
+ end
52
+
53
+ modules.each do |mod|
54
+ file = "#{__dir__}/methods/#{mod}.txt"
55
+ expected = File.readlines(file).map { |line| line.chomp.to_sym }
56
+ methods = all_methods[mod]
57
+
58
+ if methods != expected
59
+ extras = methods - expected
60
+ missing = expected - methods
61
+
62
+ if mod == "Ractor.singleton_class"
63
+ extras -= %i[builtin? shim?] # intended extras
64
+ end
65
+
66
+ if RUBY_ENGINE == "ruby" and RUBY_VERSION < "3.5"
67
+ if mod == "Ractor"
68
+ extras -= %i[take close_incoming close_outgoing] # expected extra on those versions
69
+ missing -= %i[default_port monitor unmonitor] # hard to implement on those versions
70
+ elsif mod == "Ractor.singleton_class"
71
+ extras -= %i[yield receive_if] # expected extra on those versions
72
+ if RUBY_VERSION < "3.4"
73
+ missing -= %i[[] []= store_if_absent _require] # hard to implement on those versions
74
+ end
75
+ end
76
+ end
77
+
78
+ raise "#{mod} methods should not include #{extras}" unless extras.empty?
79
+ raise "#{mod} methods should include #{missing}" unless missing.empty?
80
+ end
81
+ end
82
+
83
+ puts "#{__FILE__}: OK"