ractorize 0.0.8 → 0.0.10

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: 52f6214e369ed8e100b4af65d8cebeeadbfc5ec288b5c93d896f3feb277944fb
4
- data.tar.gz: 463beced042a7da641a824450a00447ba359e942a4a42a1e99723250611a808d
3
+ metadata.gz: f7827fd6bd911724e81479f2966f75aafb825c1320fb870531b08db1079b5b9c
4
+ data.tar.gz: bf7da9b30088e3554637b646ef4bb5ceab085d076f3e0097c68f236d647b3baf
5
5
  SHA512:
6
- metadata.gz: 1e79f4cd57dc32ef2f113620fabc60df2b0811f1e68b7975e3c3109fc210fa2ef24c8b439b3e0664b8619401ee07191ce66a7de7dafc6c606c05426c6fcee088
7
- data.tar.gz: 2927ab40a74c984c46110b3ace84158621dddceb2d45b5125d42f4dcdc9aadd846f364a712329d48054fb645e918abd64d760a6ee21026309c7414211b600647
6
+ metadata.gz: e7858c116a76c408a6c341ee8007a91a50e4caae089978564d2704535845248e14272c18909bd8f4e5458e6dbe97262afb2acc088fd79412f5231ff1f56d3de1
7
+ data.tar.gz: 0e3b0165b66489c83b3dc7ebe37873ad6600e4fb9239eeaa326440d700112bf13255a67532f84fdfd15c3d37afcb93285742a4a50e5eef0993231bab1c5976ff
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.0.10] - 2026-06-30
2
+
3
+ - Handle thunks being cloned
4
+
5
+ ## [0.0.9] - 2026-06-29
6
+
7
+ - Fix bug that results from moving Time, which is neither shareable nor movable!
8
+ - Handle deadlocks that seem to arise from re-entrant #inspect calls
9
+
1
10
  ## [0.0.8] - 2026-06-24
2
11
 
3
12
  - Add GarbageCollector to close abandoned RactorizedObject ractors and Thunk ractors
@@ -2,11 +2,13 @@ module Ractorize
2
2
  module GarbageCollection
3
3
  class Tracker
4
4
  attr_accessor :ractorized_object_id_to_ractor,
5
- :thunk_id_to_ractor
5
+ :thunk_id_to_ractor,
6
+ :ractor_to_thunk_ids
6
7
 
7
8
  def initialize
8
9
  self.ractorized_object_id_to_ractor = ObjectSpace::WeakMap.new
9
10
  self.thunk_id_to_ractor = ObjectSpace::WeakMap.new
11
+ self.ractor_to_thunk_ids = ObjectSpace::WeakKeyMap.new
10
12
  end
11
13
 
12
14
  def track_ractorized_object(ractorized_object)
@@ -24,9 +26,32 @@ module Ractorize
24
26
  thunk_id_to_ractor[thunk_id] = ractor
25
27
  end
26
28
 
29
+ def thunk_cloned(old_thunk_id, new_thunk_id, thunk_ractor)
30
+ thunk_ids = ractor_to_thunk_ids[thunk_ractor]
31
+
32
+ if thunk_ids
33
+ thunk_ids << new_thunk_id
34
+ else
35
+ ractor_to_thunk_ids[thunk_ractor] = [old_thunk_id, new_thunk_id]
36
+ end
37
+
38
+ thunk_id_to_ractor[new_thunk_id] = thunk_ractor
39
+ end
40
+
27
41
  def cleanup_after_thunk(thunk_id)
28
42
  ractor = thunk_id_to_ractor.delete(thunk_id)
29
- ractor&.<<(:__close__)
43
+
44
+ return unless ractor
45
+
46
+ thunk_ids = ractor_to_thunk_ids[ractor]
47
+
48
+ if thunk_ids
49
+ thunk_ids.delete(thunk_id)
50
+
51
+ return unless thunk_ids.empty?
52
+ end
53
+
54
+ ractor << :__close__
30
55
  rescue Ractor::ClosedError
31
56
  # do nothing
32
57
  end
@@ -0,0 +1,40 @@
1
+ require_relative "../base_ractor"
2
+ require_relative "tracker"
3
+
4
+ module Ractorize
5
+ module GarbageCollection
6
+ class TrackingRactor < BaseRactor
7
+ class << self
8
+ def new
9
+ super(name: "GarbageCollection::TrackingRactor") do
10
+ tracker = Tracker.new
11
+
12
+ loop do
13
+ # SimpleCov branch coverage doesn't like that we aren't testing not matching anything
14
+ # but this does result in an error unlike case/when so no point in checking that.
15
+ # :nocov:
16
+ case receive
17
+ # :nocov:
18
+ in :track_ractorized_object, ractorized_object
19
+ tracker.track_ractorized_object(ractorized_object)
20
+ in :cleanup_after_ractorized_object, ractorized_object_id
21
+ tracker.cleanup_after_ractorized_object(ractorized_object_id)
22
+ in :track_thunk, thunk_id, thunk_ractor
23
+ tracker.track_thunk(thunk_id, thunk_ractor)
24
+ in :thunk_cloned, old_thunk_id, new_thunk_id, thunk_ractor
25
+ tracker.thunk_cloned(old_thunk_id, new_thunk_id, thunk_ractor)
26
+ in :cleanup_after_thunk, thunk_id
27
+ tracker.cleanup_after_thunk(thunk_id)
28
+ end
29
+ rescue TrackingRactor::ClosedError
30
+ # do nothing
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ TRACKING_RACTOR = TrackingRactor.new
38
+ private_constant :TRACKING_RACTOR
39
+ end
40
+ end
@@ -1,7 +1,5 @@
1
1
  module Ractorize
2
2
  module GarbageCollection
3
- class TrackingRactor < BaseRactor; end
4
-
5
3
  class << self
6
4
  def track_ractorized_object(ractorized_object)
7
5
  # We have to define the finalizer here, not in the tracker, because it's not frozen yet
@@ -16,7 +14,7 @@ module Ractorize
16
14
  end
17
15
 
18
16
  def track_thunk(thunk)
19
- # We have to define the finalizer here, not in the tracker, because it's not frozen yet
17
+ # We have to define the finalizer here, not in the tracker, because it's not shareable
20
18
  ObjectSpace.define_finalizer(thunk, &finalize_thunk_proc)
21
19
 
22
20
  begin
@@ -26,6 +24,24 @@ module Ractorize
26
24
  end
27
25
  end
28
26
 
27
+ def thunk_cloned(old_thunk, new_thunk)
28
+ ractor = old_thunk.__thunk_ractor__
29
+
30
+ # We have to define the finalizer here, not in the tracker, because it's not shareable
31
+ # ObjectSpace.define_finalizer(new_thunk, &finalize_thunk_proc)
32
+
33
+ begin
34
+ TRACKING_RACTOR << [
35
+ :thunk_cloned,
36
+ old_thunk.__object_id__,
37
+ new_thunk.__object_id__,
38
+ ractor
39
+ ].freeze
40
+ rescue TrackingRactor::ClosedError
41
+ # do nothing
42
+ end
43
+ end
44
+
29
45
  def cleanup_after_ractorized_object(ractorized_object_id)
30
46
  TRACKING_RACTOR << [:cleanup_after_ractorized_object, ractorized_object_id].freeze
31
47
  rescue Ractor::ClosedError
@@ -52,30 +68,5 @@ module Ractorize
52
68
  end
53
69
  end
54
70
  end
55
-
56
- TRACKING_RACTOR = TrackingRactor.new do
57
- tracker = Tracker.new
58
-
59
- loop do
60
- # SimpleCov branch coverage doesn't like that we aren't testing not matching anything
61
- # but this does result in an error unlike case/when so no point in checking that.
62
- # :nocov:
63
- case receive
64
- # :nocov:
65
- in :track_ractorized_object, ractorized_object
66
- tracker.track_ractorized_object(ractorized_object)
67
- in :cleanup_after_ractorized_object, ractorized_object_id
68
- tracker.cleanup_after_ractorized_object(ractorized_object_id)
69
- in :track_thunk, thunk_id, thunk_ractor
70
- tracker.track_thunk(thunk_id, thunk_ractor)
71
- in :cleanup_after_thunk, thunk_id
72
- tracker.cleanup_after_thunk(thunk_id)
73
- end
74
- rescue TrackingRactor::ClosedError
75
- # do nothing
76
- end
77
- end
78
-
79
- private_constant :TRACKING_RACTOR
80
71
  end
81
72
  end
@@ -0,0 +1,133 @@
1
+ require_relative "../base_ractor"
2
+
3
+ module Ractorize
4
+ class RactorizedObject < BasicObject
5
+ class RactorizedRactor < ::BaseRactor
6
+ class << self
7
+ def new(name: nil)
8
+ super do
9
+ mode = receive
10
+
11
+ object = case mode
12
+ when :class
13
+ klass, args, opts, block = receive
14
+ target_class = klass
15
+ klass.new(*args.freeze, **opts.freeze, &block)
16
+ when :object
17
+ o = receive
18
+ target_class = o.class
19
+ o
20
+ when :class_arg_by_arg
21
+ klass = receive
22
+ target_class = klass
23
+
24
+ args, opts, block = Ractorize.extract_args(self)
25
+
26
+ klass.new(*args.freeze, **opts.freeze, &block)
27
+ else
28
+ # :nocov:
29
+ raise "Invalid mode #{mode}"
30
+ # :nocov:
31
+ end
32
+
33
+ loop do
34
+ # rubocop:disable Lint/UselessAssignment
35
+ value = method_name = method_args = opts = return_port = thunk_ractor = block_given = nil
36
+ # rubocop:enable Lint/UselessAssignment
37
+ method_name, method_args, opts, return_port, thunk_ractor, block_given = receive
38
+
39
+ case method_name
40
+ when :__close__
41
+ begin
42
+ # Time is not movable but also not shareable!
43
+ move = !Ractor.shareable?(object) && !(Time === object)
44
+ thunk_ractor&.send([:success, object].freeze, move:)
45
+ rescue RactorizedRactor::ClosedError
46
+ # do nothing
47
+ end
48
+
49
+ object = nil
50
+ close
51
+ break
52
+ when :__target_object_id__
53
+ return_port = method_args
54
+
55
+ target_object_id = ::Object.instance_method(:object_id).bind_call(object)
56
+ return_port << target_object_id
57
+ else
58
+ if method_name == :__invoke_arg_by_arg__
59
+ args_port = Ractorize::RactorizedObject::RactorizedRactor::Port.new
60
+ return_port << args_port
61
+
62
+ method_name = args_port.receive
63
+ method_args, opts = Ractorize.extract_args(args_port)
64
+ end
65
+
66
+ if block_given
67
+ block_result_port = Ractorize::RactorizedObject::RactorizedRactor::Port.new
68
+
69
+ value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
70
+ Ractorize.prepare_args(target_class, args, opts, skip_move: true)
71
+
72
+ return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
73
+
74
+ outcome_type, return_value = block_result_port.receive
75
+
76
+ case outcome_type
77
+ when :normal
78
+ return_value
79
+ when :break
80
+ # TODO: handle error situation
81
+ break
82
+ else
83
+ # :nocov:
84
+ raise "Not sure how to handle outcome_type #{outcome_type}"
85
+ # :nocov:
86
+ end
87
+ end
88
+
89
+ return_port << [:return, value].freeze
90
+ else
91
+ value = object.__send__(method_name, *method_args, **opts)
92
+ value = value.__value__ while Ractorize::Thunk === value
93
+
94
+ begin
95
+ if thunk_ractor
96
+ thunk_ractor.send([:success, value].freeze)
97
+ else
98
+ return_port << value
99
+ end
100
+ rescue IOError => e
101
+ # Unclear why this sometimes manifests as this error instead of ClosedError but
102
+ # need to handle them both.
103
+ # :nocov:
104
+ raise unless e.message == "closed stream"
105
+ # :nocov:
106
+ rescue Ractor::ClosedError
107
+ # Whoa... this error inherits from StopIteration and will kill the loop!!!
108
+ # Nothing really to do here but keep the loop going and handle other
109
+ # method calls to the ractorized object from other ractors.
110
+ end
111
+ end
112
+ end
113
+
114
+ nil
115
+ end
116
+
117
+ nil
118
+ # object
119
+ rescue => e
120
+ # :nocov:
121
+ puts
122
+ puts "an unhandled error!!! #{e.class} #{e.message} #{e}"
123
+ puts e.backtrace
124
+ puts
125
+
126
+ raise
127
+ # :nocov:
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -3,8 +3,6 @@ require_relative "thunk"
3
3
 
4
4
  module Ractorize
5
5
  class RactorizedObject < BasicObject
6
- class RactorizedRactor < ::BaseRactor; end
7
-
8
6
  class << self
9
7
  def method_should_use_thunk?(method_symbol)
10
8
  method_symbol != :== && method_symbol != :! && method_symbol != :!= &&
@@ -16,14 +14,14 @@ module Ractorize
16
14
  attr_reader :__object_id__, :__ractor__
17
15
 
18
16
  def initialize(mode, *args, **opts, &block)
19
- @__ractor__ = RactorizedRactor.new(name: "#{args.first}<#{args.first.object_id}>", &RACTOR_PROC)
17
+ @__ractor__ = RactorizedRactor.new(name: "#{args.first}<#{args.first.object_id}>".freeze)
20
18
 
21
19
  case mode
22
20
  when :object
23
21
  @__ractor__ << :object
24
22
 
25
23
  outside_object = args.first
26
-
24
+ @__target_object_id__ = ::Object.instance_method(:object_id).bind_call(outside_object)
27
25
  @__target_class__ = outside_object.class
28
26
 
29
27
  if ::Ractor.shareable?(outside_object)
@@ -64,6 +62,10 @@ module Ractorize
64
62
  @__ractor__ << :class
65
63
  @__ractor__ << [klass, args.freeze, opts.dup.freeze, block].freeze
66
64
  end
65
+
66
+ return_port = ::Ractor::Port.new
67
+ @__ractor__ << [:__target_object_id__, return_port].freeze
68
+ @__target_object_id__ = return_port.receive
67
69
  else
68
70
  # :nocov:
69
71
  ::Kernel.raise "Invalid mode #{mode}"
@@ -192,11 +194,11 @@ module Ractorize
192
194
  # def equal?(other) = method_missing(:equal?, other) || super
193
195
  def to_s = inspect
194
196
 
197
+ # delegating this to the target object increases risk of a deadlock since
198
+ # sometimes #inspect calls #inspect on other objects and can lead to reentry and thus deadlock
195
199
  def inspect
196
200
  object_id = ::Object.instance_method(:object_id).bind(self).call
197
- moved_object_inspect = method_missing(:inspect)
198
-
199
- "RactorizedObject<#{object_id}>[#{moved_object_inspect}]".freeze
201
+ "RactorizedObject<#{object_id}>[#{@__target_class__}<#{@__target_object_id__}>]}"
200
202
  end
201
203
  end
202
204
  end
@@ -10,13 +10,20 @@ module Ractorize
10
10
  ::Ractorize::GarbageCollection.track_thunk(self)
11
11
  end
12
12
 
13
- def initialize_clone(...)
14
- # :nocov:
15
- raise "CAREFUL! THUNK CLONED!!"
16
- # :nocov:
17
- # is this actually necessary?? Seems so?
13
+ def initialize_clone(original_thunk)
14
+ if __resolved__? && original_thunk.__resolved__?
15
+ # Seems this could happen if we had a frozen thunk whose @__value__ is not shareable
16
+ # Since the thunk's ractor is already gone nothing to worry about
17
+ return
18
+ end
19
+
20
+ self.__object_id__ = ::Object.instance_method(:object_id).bind_call(self)
21
+
22
+ ::Ractorize::GarbageCollection.thunk_cloned(original_thunk, self)
18
23
  end
19
24
 
25
+ def __resolved__? = defined?(@__value__)
26
+
20
27
  def method_missing(...)
21
28
  __value__.__send__(...)
22
29
  end
data/src/ractorize.rb CHANGED
@@ -4,6 +4,22 @@ require_relative "ractorize/ractorized_class"
4
4
 
5
5
  module Ractorize
6
6
  class << self
7
+ def ractorize_object(object)
8
+ RactorizedObject.new(:object, object)
9
+ end
10
+
11
+ def ractorize_class(klass)
12
+ RactorizedClass[klass]
13
+ end
14
+
15
+ def [](object)
16
+ if object.is_a?(Class)
17
+ ractorize_class(object)
18
+ else
19
+ ractorize_object(object)
20
+ end
21
+ end
22
+
7
23
  # TODO: figure out a way to magically get a ractor-shareable proc from a non-ractor-shareable proc
8
24
  def auto_freeze(target, class_or_proc = nil)
9
25
  @auto_freeze = @auto_freeze ? @auto_freeze.dup : []
@@ -214,139 +230,4 @@ module Ractorize
214
230
  nil
215
231
  end
216
232
  end
217
-
218
- # Putting this in a constant so we can get test coverage on it since not sure how to get coverage
219
- # on something inside a ractor.
220
- RACTOR_PROC = Ractor.shareable_proc do
221
- mode = receive
222
-
223
- object = case mode
224
- when :class
225
- klass, args, opts, block = receive
226
- target_class = klass
227
- klass.new(*args.freeze, **opts.freeze, &block)
228
- when :object
229
- o = receive
230
- target_class = o.class
231
- o
232
- when :class_arg_by_arg
233
- klass = receive
234
- target_class = klass
235
-
236
- args, opts, block = Ractorize.extract_args(self)
237
-
238
- klass.new(*args.freeze, **opts.freeze, &block)
239
- else
240
- # :nocov:
241
- raise "Invalid mode #{mode}"
242
- # :nocov:
243
- end
244
-
245
- loop do
246
- # rubocop:disable Lint/UselessAssignment
247
- value = method_name = method_args = opts = return_port = thunk_ractor = block_given = nil
248
- # rubocop:enable Lint/UselessAssignment
249
- method_name, method_args, opts, return_port, thunk_ractor, block_given = receive
250
-
251
- case method_name
252
- when :__close__
253
- begin
254
- thunk_ractor&.send([:success, object].freeze, move: true)
255
- rescue RactorizedRactor::ClosedError
256
- # do nothing
257
- end
258
-
259
- object = nil
260
- close
261
- break
262
- else
263
- if method_name == :__invoke_arg_by_arg__
264
- args_port = Ractorize::RactorizedObject::RactorizedRactor::Port.new
265
- return_port << args_port
266
-
267
- method_name = args_port.receive
268
- method_args, opts = Ractorize.extract_args(args_port)
269
- end
270
-
271
- if block_given
272
- block_result_port = Ractorize::RactorizedObject::RactorizedRactor::Port.new
273
-
274
- value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
275
- Ractorize.prepare_args(target_class, args, opts, skip_move: true)
276
-
277
- return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
278
-
279
- outcome_type, return_value = block_result_port.receive
280
-
281
- case outcome_type
282
- when :normal
283
- return_value
284
- when :break
285
- # TODO: handle error situation
286
- break
287
- else
288
- # :nocov:
289
- raise "Not sure how to handle outcome_type #{outcome_type}"
290
- # :nocov:
291
- end
292
- end
293
-
294
- return_port << [:return, value].freeze
295
- else
296
- value = object.__send__(method_name, *method_args, **opts)
297
- value = value.__value__ while Ractorize::Thunk === value
298
-
299
- begin
300
- if thunk_ractor
301
- thunk_ractor.send([:success, value].freeze)
302
- else
303
- return_port << value
304
- end
305
- rescue IOError => e
306
- # Unclear why this sometimes manifests as this error instead of ClosedError but
307
- # need to handle them both.
308
- # :nocov:
309
- raise unless e.message == "closed stream"
310
- # :nocov:
311
- rescue Ractor::ClosedError
312
- # Whoa... this error inherits from StopIteration and will kill the loop!!!
313
- # Nothing really to do here but keep the loop going and handle other
314
- # method calls to the ractorized object from other ractors.
315
- end
316
- end
317
- end
318
-
319
- nil
320
- end
321
-
322
- nil
323
- # object
324
- rescue => e
325
- # :nocov:
326
- puts
327
- puts "an unhandled error!!! #{e.class} #{e.message} #{e}"
328
- puts e.backtrace
329
- puts
330
-
331
- raise
332
- # :nocov:
333
- end
334
-
335
- class << self
336
- def ractorize_object(object)
337
- RactorizedObject.new(:object, object)
338
- end
339
-
340
- def ractorize_class(klass)
341
- RactorizedClass[klass]
342
- end
343
-
344
- def [](object)
345
- if object.is_a?(Class)
346
- ractorize_class(object)
347
- else
348
- ractorize_object(object)
349
- end
350
- end
351
- end
352
233
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ractorize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -24,8 +24,10 @@ files:
24
24
  - src/ractorize/base_ractor.rb
25
25
  - src/ractorize/garbage_collection.rb
26
26
  - src/ractorize/garbage_collection/tracker.rb
27
+ - src/ractorize/garbage_collection/tracking_ractor.rb
27
28
  - src/ractorize/ractorized_class.rb
28
29
  - src/ractorize/ractorized_object.rb
30
+ - src/ractorize/ractorized_object/ractorized_ractor.rb
29
31
  - src/ractorize/thunk.rb
30
32
  - src/ractorize/thunk/thunk_ractor.rb
31
33
  homepage: https://github.com/ractor-shack/ractorize