ractorize 0.0.7 → 0.0.9

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: 02d57de76c96c80c8fa5d7a49dcc54a12e9c7f849ff8951faba91806ff5dadbb
4
- data.tar.gz: e6ae43cf484790673a4010505622b7255a09b7a5d04f358a2b5a8bec0ab6b917
3
+ metadata.gz: 72a53fba452511ad30a39dc46714c285570a2904f167c960e97d519e63ee1829
4
+ data.tar.gz: 77c170a01ff7ec50bfb63c8c70b905be6d999bdaa2b8cd0e3826afd7bc5bb9d1
5
5
  SHA512:
6
- metadata.gz: 5034e676239f3e6ccd3ea1aa83a0923c0b439800127fd1a3444c08ce93de706f48b05f87e68d697cddb6612d42d7ca057b1b29adef5954ca5b51575cd1320050
7
- data.tar.gz: 461059bac8aeea154ec67ed22ed530ac7f8921902bef688d7a5d21927e67adaaab582e2bc528b7a556b4c1fb733a37b9a9202bf261eb6d048125761d516a720f
6
+ metadata.gz: 3655da0fc231e1d6b5c9bd063cf8e676ab6a9ce7f3b09421e48d085978257574bbbf988d538acb190dd0512d3838b5fe49a9436fa85e65f67a09aebfe137ff8b
7
+ data.tar.gz: 0dad22a3bf13eedff038063711e18e473b25f5e6e760b3b400de4603996240f71d3635788c8f06e2705da2a8103df236c5c76f94303ff44900122baedd368a9d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.0.9] - 2026-06-29
2
+
3
+ - Fix bug that results from moving Time, which is neither shareable nor movable!
4
+ - Handle deadlocks that seem to arise from re-entrant #inspect calls
5
+
6
+ ## [0.0.8] - 2026-06-24
7
+
8
+ - Add GarbageCollector to close abandoned RactorizedObject ractors and Thunk ractors
9
+ - Switch thunks to work off of Ractor instead of Ractor::Port due to port/ractor leaks
10
+ - Block on #hash and delegate to super in #==/#!=
11
+ - Significant test suite improvements:
12
+ - Remove awkward RACTORIZE_PROC tests and instead use the shmactor gem to get to 100% branch coverage
13
+ - Parallelize the build and run a shmactor build to get full coverage of ractor procs
14
+ - Check for leaked ractors/ports after test suite and fail the build on any memory leaks
15
+
1
16
  ## [0.0.7] - 2026-06-06
2
17
 
3
18
  - Closed Ractor::Ports sometimes give IOError instead of Ractor::ClosedError so handle both
@@ -0,0 +1,2 @@
1
+ # We do it this way to allow easily swapping out with Shmactor
2
+ BaseRactor ||= Ractor # rubocop:disable Lint/OrAssignmentToConstant
@@ -0,0 +1,35 @@
1
+ module Ractorize
2
+ module GarbageCollection
3
+ class Tracker
4
+ attr_accessor :ractorized_object_id_to_ractor,
5
+ :thunk_id_to_ractor
6
+
7
+ def initialize
8
+ self.ractorized_object_id_to_ractor = ObjectSpace::WeakMap.new
9
+ self.thunk_id_to_ractor = ObjectSpace::WeakMap.new
10
+ end
11
+
12
+ def track_ractorized_object(ractorized_object)
13
+ ractorized_object_id_to_ractor[ractorized_object.__object_id__] = ractorized_object.__ractor__
14
+ end
15
+
16
+ def cleanup_after_ractorized_object(ractorized_object_id)
17
+ ractor = ractorized_object_id_to_ractor.delete(ractorized_object_id)
18
+ ractor&.<<(:__close__)
19
+ rescue Ractor::ClosedError
20
+ # do nothing
21
+ end
22
+
23
+ def track_thunk(thunk_id, ractor)
24
+ thunk_id_to_ractor[thunk_id] = ractor
25
+ end
26
+
27
+ def cleanup_after_thunk(thunk_id)
28
+ ractor = thunk_id_to_ractor.delete(thunk_id)
29
+ ractor&.<<(:__close__)
30
+ rescue Ractor::ClosedError
31
+ # do nothing
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
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 :cleanup_after_thunk, thunk_id
25
+ tracker.cleanup_after_thunk(thunk_id)
26
+ end
27
+ rescue TrackingRactor::ClosedError
28
+ # do nothing
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ TRACKING_RACTOR = TrackingRactor.new
36
+ private_constant :TRACKING_RACTOR
37
+ end
38
+ end
@@ -0,0 +1,54 @@
1
+ module Ractorize
2
+ module GarbageCollection
3
+ class << self
4
+ def track_ractorized_object(ractorized_object)
5
+ # We have to define the finalizer here, not in the tracker, because it's not frozen yet
6
+ ObjectSpace.define_finalizer(ractorized_object, &finalize_proc)
7
+ Object.instance_method(:freeze).bind_call(ractorized_object)
8
+
9
+ begin
10
+ TRACKING_RACTOR << [:track_ractorized_object, ractorized_object].freeze
11
+ rescue TrackingRactor::ClosedError
12
+ # do nothing
13
+ end
14
+ end
15
+
16
+ def track_thunk(thunk)
17
+ # We have to define the finalizer here, not in the tracker, because it's not frozen yet
18
+ ObjectSpace.define_finalizer(thunk, &finalize_thunk_proc)
19
+
20
+ begin
21
+ TRACKING_RACTOR << [:track_thunk, thunk.__object_id__, thunk.__thunk_ractor__].freeze
22
+ rescue TrackingRactor::ClosedError
23
+ # do nothing
24
+ end
25
+ end
26
+
27
+ def cleanup_after_ractorized_object(ractorized_object_id)
28
+ TRACKING_RACTOR << [:cleanup_after_ractorized_object, ractorized_object_id].freeze
29
+ rescue Ractor::ClosedError
30
+ # do nothing
31
+ end
32
+
33
+ def cleanup_after_thunk(thunk_id)
34
+ TRACKING_RACTOR << [:cleanup_after_thunk, thunk_id].freeze
35
+ rescue Ractor::ClosedError
36
+ # do nothing
37
+ end
38
+
39
+ private
40
+
41
+ def finalize_proc
42
+ proc do |ractorized_object_id|
43
+ ::Ractorize::GarbageCollection.cleanup_after_ractorized_object(ractorized_object_id)
44
+ end
45
+ end
46
+
47
+ def finalize_thunk_proc
48
+ proc do |thunk_id|
49
+ ::Ractorize::GarbageCollection.cleanup_after_thunk(thunk_id)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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,22 +3,32 @@ require_relative "thunk"
3
3
 
4
4
  module Ractorize
5
5
  class RactorizedObject < BasicObject
6
+ class << self
7
+ def method_should_use_thunk?(method_symbol)
8
+ method_symbol != :== && method_symbol != :! && method_symbol != :!= &&
9
+ method_symbol != :inspect && method_symbol != :to_s &&
10
+ !method_symbol.end_with?("?") && method_symbol != :hash
11
+ end
12
+ end
13
+
14
+ attr_reader :__object_id__, :__ractor__
15
+
6
16
  def initialize(mode, *args, **opts, &block)
7
- @ractor = ::Ractor.new(name: "#{args.first}<#{args.first.object_id}>", &RACTOR_PROC)
17
+ @__ractor__ = RactorizedRactor.new(name: "#{args.first}<#{args.first.object_id}>".freeze)
8
18
 
9
19
  case mode
10
20
  when :object
11
- @ractor << :object
21
+ @__ractor__ << :object
12
22
 
13
23
  outside_object = args.first
14
-
24
+ @__target_object_id__ = ::Object.instance_method(:object_id).bind_call(outside_object)
15
25
  @__target_class__ = outside_object.class
16
26
 
17
27
  if ::Ractor.shareable?(outside_object)
18
- @ractor << outside_object
28
+ @__ractor__ << outside_object
19
29
  else
20
30
  ::Ractorize.resolve_all_thunks(outside_object)
21
- @ractor.send(outside_object, move: true)
31
+ @__ractor__.send(outside_object, move: true)
22
32
  end
23
33
  when :class
24
34
  klass, *args = args
@@ -28,60 +38,69 @@ module Ractorize
28
38
  to_move = ::Ractorize.prepare_args(@__target_class__, args, opts)
29
39
 
30
40
  if to_move&.any?
31
- @ractor << :class_arg_by_arg
32
- @ractor << klass
41
+ @__ractor__ << :class_arg_by_arg
42
+ @__ractor__ << klass
33
43
 
34
44
  args.each do |arg|
35
- @ractor << :arg
36
- @ractor.send(arg, move: to_move.include?(arg))
45
+ @__ractor__ << :arg
46
+ @__ractor__.send(arg, move: to_move.include?(arg))
37
47
  end
38
48
 
39
49
  opts.each_pair do |name, value|
40
- @ractor << :kwarg
41
- @ractor << name
42
- @ractor.send(value, move: to_move.include?(value))
50
+ @__ractor__ << :kwarg
51
+ @__ractor__ << name
52
+ @__ractor__.send(value, move: to_move.include?(value))
43
53
  end
44
54
 
45
55
  if block
46
- @ractor << :block
47
- @ractor << block
56
+ @__ractor__ << :block
57
+ @__ractor__ << block
48
58
  end
49
59
 
50
- @ractor << :done
60
+ @__ractor__ << :done
51
61
  else
52
- @ractor << :class
53
- @ractor << [klass, args.freeze, opts.dup.freeze, block].freeze
62
+ @__ractor__ << :class
63
+ @__ractor__ << [klass, args.freeze, opts.dup.freeze, block].freeze
54
64
  end
65
+
66
+ return_port = ::Ractor::Port.new
67
+ @__ractor__ << [:__target_object_id__, return_port].freeze
68
+ @__target_object_id__ = return_port.receive
55
69
  else
56
70
  # :nocov:
57
71
  ::Kernel.raise "Invalid mode #{mode}"
58
72
  # :nocov:
59
73
  end
60
74
 
61
- ::Object.instance_method(:freeze).bind(self).call
75
+ @__object_id__ = ::Object.instance_method(:object_id).bind_call(self)
76
+
77
+ ::Ractorize::GarbageCollection.track_ractorized_object(self)
62
78
  end
63
79
 
64
80
  def __close__ = method_missing(:__close__)
65
81
 
66
82
  def __join__
67
83
  __close__
68
- @ractor.join
84
+ @__ractor__.join
69
85
  self
70
86
  end
71
87
 
72
88
  def method_missing(method_name, *args, **opts, &block)
73
- if @ractor.default_port.closed?
89
+ if @__ractor__.default_port.closed?
74
90
  ::Kernel.raise ::Ractor::ClosedError,
75
91
  "You already closed this Ractorized instance of #{@__target_class__}!\n" \
76
92
  "No more methods can be sent to it but you sent #{method_name}"
77
93
  end
78
94
 
79
- return_port = ::Ractor::Port.new
95
+ return_port = ::Ractorize::RactorizedObject::RactorizedRactor::Port.new
96
+ thunk_ractor = if !block && RactorizedObject.method_should_use_thunk?(method_name)
97
+ ::Ractorize::Thunk::ThunkRactor.new
98
+ end
80
99
 
81
100
  to_move = ::Ractorize.prepare_args(@__target_class__, args, opts)
82
101
 
83
102
  if to_move&.any?
84
- @ractor << [:__invoke_arg_by_arg__, [].freeze, {}.freeze, return_port, !!block]
103
+ @__ractor__ << [:__invoke_arg_by_arg__, [].freeze, {}.freeze, return_port, thunk_ractor, !!block]
85
104
 
86
105
  args_port = return_port.receive
87
106
  args_port << method_name
@@ -99,7 +118,7 @@ module Ractorize
99
118
 
100
119
  args_port << :done
101
120
  else
102
- @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port, !!block].freeze
121
+ @__ractor__ << [method_name, args.dup.freeze, opts.dup.freeze, return_port, thunk_ractor, !!block].freeze
103
122
  end
104
123
 
105
124
  if block
@@ -121,28 +140,38 @@ module Ractorize
121
140
  # TODO: yielded_block likely won't work when actually used
122
141
  # so we should probably instead just raise an exception
123
142
  # TODO: handle break and also raise in the block
124
- block_result = block.call(*yielded_args.freeze, **yielded_opts.freeze, &yielded_block)
125
-
126
- block_result = block_result.__value__ while ::Ractorize::Thunk === block_result
127
-
128
- block_result_port << [:normal, block_result].freeze
143
+ begin
144
+ broke = true
145
+ block_result = block.call(*yielded_args.freeze, **yielded_opts.freeze, &yielded_block)
146
+ broke = false
147
+ ensure
148
+ block_result = block_result.__value__ while ::Ractorize::Thunk === block_result
149
+
150
+ block_result_port << if broke
151
+ # TODO: handle error situation
152
+ :break
153
+ else
154
+ [:normal, block_result].freeze
155
+ end
156
+ end
129
157
  end
130
158
  end
131
159
 
132
160
  value
133
161
  # Let's assume the user would rather block on all predicate methods than
134
162
  # incorrectly get a non-truthy value (thunk is always truthy even if it evaluates as nil/false)
135
- elsif method_name == :== || method_name == :! || method_name == :!= ||
136
- method_name == :inspect || method_name == :to_s || method_name.end_with?("?")
163
+ elsif thunk_ractor
164
+ return_port.close
165
+ Thunk.new(thunk_ractor)
166
+ else
137
167
  value = return_port.receive
168
+ return_port.close
138
169
 
139
170
  # :nocov:
140
171
  ::Kernel.raise ::Ractorize::Thunk::EscapingRactorError if ::Ractorize::Thunk === value
141
172
  # :nocov:
142
173
 
143
174
  value
144
- else
145
- Thunk.new(return_port)
146
175
  end
147
176
  end
148
177
 
@@ -159,18 +188,17 @@ module Ractorize
159
188
  method_missing(:respond_to?, method_name, include_all)
160
189
  end
161
190
 
162
- def ==(other) = method_missing(:==, other)
163
- def !=(other) = method_missing(:==, other)
191
+ def ==(other) = method_missing(:==, other) || super
192
+ def !=(other) = method_missing(:==, other) || super
164
193
  def ! = method_missing(:!)
165
- def equal?(other) = method_missing(:equal?, other)
166
-
194
+ # def equal?(other) = method_missing(:equal?, other) || super
167
195
  def to_s = inspect
168
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
169
199
  def inspect
170
200
  object_id = ::Object.instance_method(:object_id).bind(self).call
171
- moved_object_inspect = method_missing(:inspect)
172
-
173
- "RactorizedObject<#{object_id}>[#{moved_object_inspect}]".freeze
201
+ "RactorizedObject<#{object_id}>[#{@__target_class__}<#{@__target_object_id__}>]}"
174
202
  end
175
203
  end
176
204
  end
@@ -0,0 +1,23 @@
1
+ require_relative "../base_ractor"
2
+
3
+ module Ractorize
4
+ class Thunk < BasicObject
5
+ class ThunkRactor < ::BaseRactor
6
+ class << self
7
+ def new
8
+ super do
9
+ # SimpleCov seems to want us to handle the case where nothing matches but that would be an error
10
+ # :nocov:
11
+ case receive
12
+ # :nocov:
13
+ in :__close__
14
+ # do nothing
15
+ in :success, value
16
+ value
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,14 +2,18 @@ module Ractorize
2
2
  class Thunk < BasicObject
3
3
  class EscapingRactorError < ::StandardError; end
4
4
 
5
- attr_accessor :__return_value_port__, :__ractor__
5
+ attr_accessor :__thunk_ractor__, :__object_id__
6
6
 
7
- def initialize(return_value_port)
8
- self.__ractor__ = ::Ractor.current
9
- self.__return_value_port__ = return_value_port
7
+ def initialize(return_value_portlike)
8
+ self.__thunk_ractor__ = return_value_portlike
9
+ self.__object_id__ = ::Object.instance_method(:object_id).bind_call(self)
10
+ ::Ractorize::GarbageCollection.track_thunk(self)
10
11
  end
11
12
 
12
13
  def initialize_clone(...)
14
+ # :nocov:
15
+ raise "CAREFUL! THUNK CLONED!!"
16
+ # :nocov:
13
17
  # is this actually necessary?? Seems so?
14
18
  end
15
19
 
@@ -24,24 +28,11 @@ module Ractorize
24
28
  def __value__
25
29
  return @__value__ if defined?(@__value__)
26
30
 
27
- value = if ::Ractor.current == __ractor__
28
- __return_value_port__.receive
29
- else
30
- # :nocov:
31
- ::Kernel.raise EscapingRactorError,
32
- "Somehow this thunk was passed between ractors but wasn't resolved first."
33
- # :nocov:
34
- end
35
-
36
- # :nocov:
37
- ::Kernel.raise EscapingRactorError if ::Ractorize::Thunk === value
38
- # :nocov:
39
-
40
- @__value__ = value
41
-
42
- ::Object.instance_method(:freeze).bind(self).call
31
+ @__value__ = __thunk_ractor__.join.value
32
+ self.__thunk_ractor__ = nil
33
+ ::Object.instance_method(:freeze).bind_call(self)
43
34
 
44
- value
35
+ @__value__
45
36
  end
46
37
 
47
38
  def ! = !__value__
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 : []
@@ -64,6 +80,7 @@ module Ractorize
64
80
  # Not sure why that is but we need to handle that case.
65
81
  def resolve_all_thunks(structure)
66
82
  each_thunk(structure, &:__value__)
83
+ nil
67
84
  end
68
85
 
69
86
  def to_move(target_class, args)
@@ -147,28 +164,8 @@ module Ractorize
147
164
  to_move(target_class, args)
148
165
  end
149
166
 
150
- def each_thunk(structure, seen = Set.new, &block)
151
- return block.call(structure) if Thunk === structure
152
- return if seen.include?(structure)
153
-
154
- seen << structure
155
-
156
- case structure
157
- when Array
158
- structure.each { each_thunk(it, seen, &block) }
159
- when Hash
160
- each_thunk(structure.keys, seen, &block)
161
- each_thunk(structure.values, seen, &block)
162
- when Struct
163
- each_thunk(structure.values, seen, &block)
164
- else
165
- ivarsget = ::Object.instance_method(:instance_variables)
166
- iget = ::Object.instance_method(:instance_variable_get)
167
-
168
- ivarsget.bind(structure).call.each do |var|
169
- each_thunk(iget.bind(structure).call(var), seen, &block)
170
- end
171
- end
167
+ def each_thunk(structure, seen = Set.new, &)
168
+ each_instance_of(Thunk, structure, seen, 0, &)
172
169
  end
173
170
 
174
171
  def extract_args(port_like)
@@ -200,123 +197,37 @@ module Ractorize
200
197
 
201
198
  [args, opts, block]
202
199
  end
203
- end
204
-
205
- # Putting this in a constant so we can get test coverage on it since not sure how to get coverage
206
- # on something inside a ractor.
207
- RACTOR_PROC = proc do
208
- mode = receive
209
-
210
- object = case mode
211
- when :class
212
- klass, args, opts, block = receive
213
- target_class = klass
214
- klass.new(*args.freeze, **opts.freeze, &block)
215
- when :object
216
- o = receive
217
- target_class = o.class
218
- o
219
- when :class_arg_by_arg
220
- klass = receive
221
- target_class = klass
222
-
223
- args, opts, block = Ractorize.extract_args(self)
224
-
225
- klass.new(*args.freeze, **opts.freeze, &block)
226
- else
227
- # :nocov:
228
- raise "Invalid mode #{mode}"
229
- # :nocov:
230
- end
231
-
232
- loop do
233
- method_name, method_args, opts, return_port, block_given = receive
234
-
235
- case method_name
236
- when :__close__
237
- return_port.<<(object, move: true)
238
- close
239
- break
240
- else
241
- if method_name == :__invoke_arg_by_arg__
242
- args_port = Ractor::Port.new
243
- return_port << args_port
244
-
245
- method_name = args_port.receive
246
- method_args, opts = Ractorize.extract_args(args_port)
247
- end
248
-
249
- if block_given
250
- block_result_port = Ractor::Port.new
251
200
 
252
- value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
253
- Ractorize.prepare_args(target_class, args, opts, skip_move: true)
201
+ private
254
202
 
255
- return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
203
+ def each_instance_of(klass, structure, seen = Set.new, depth = 0, &block)
204
+ depth += 1
205
+ if klass === structure
206
+ block.call(structure)
207
+ end
208
+ return if seen.include?(structure)
256
209
 
257
- outcome_type, return_value = block_result_port.receive
210
+ seen << structure
258
211
 
259
- case outcome_type
260
- when :normal
261
- return_value
262
- when :break
263
- break return_value
264
- else
265
- # :nocov:
266
- raise "Not sure how to handle outcome_type #{outcome_type}"
267
- # :nocov:
268
- end
269
- end
212
+ case structure
213
+ when Array
214
+ structure.each { each_instance_of(klass, it, seen, depth, &block) }
215
+ when Hash
216
+ each_instance_of(klass, structure.keys, seen, depth, &block)
217
+ each_instance_of(klass, structure.values, seen, depth, &block)
218
+ when Struct
219
+ each_instance_of(klass, structure.values, seen, depth, &block)
220
+ else
221
+ ivarsget = ::Object.instance_method(:instance_variables)
222
+ iget = ::Object.instance_method(:instance_variable_get)
270
223
 
271
- return_port << [:return, value].freeze
272
- else
273
- value = object.__send__(method_name, *method_args, **opts)
274
- value = value.__value__ while Ractorize::Thunk === value
275
-
276
- begin
277
- return_port.send(value)
278
- rescue IOError => e
279
- # Unclear why this sometimes manifests as this error instead of ClosedError but
280
- # need to handle them both.
281
- # :nocov:
282
- raise unless e.message == "closed stream"
283
- # :nocov:
284
- rescue Ractor::ClosedError
285
- # Whoa... this error inherits from StopIteration and will kill the loop!!!
286
- # Nothing really to do here but keep the loop going and handle other
287
- # method calls to the ractorized object from other ractors.
288
- end
224
+ ivarsget.bind(structure).call.each do |var|
225
+ value = iget.bind(structure).call(var)
226
+ each_instance_of(klass, value, seen, depth, &block)
289
227
  end
290
228
  end
291
- end
292
-
293
- object
294
- rescue => e
295
- # :nocov:
296
- puts
297
- puts "an unhandled error!!! #{e.class} #{e.message} #{e}"
298
- puts e.backtrace
299
- puts
300
229
 
301
- raise
302
- # :nocov:
303
- end
304
-
305
- class << self
306
- def ractorize_object(object)
307
- RactorizedObject.new(:object, object)
308
- end
309
-
310
- def ractorize_class(klass)
311
- RactorizedClass[klass]
312
- end
313
-
314
- def [](object)
315
- if object.is_a?(Class)
316
- ractorize_class(object)
317
- else
318
- ractorize_object(object)
319
- end
230
+ nil
320
231
  end
321
232
  end
322
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.7
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -21,9 +21,15 @@ files:
21
21
  - README.md
22
22
  - lib/ractorize.rb
23
23
  - src/ractorize.rb
24
+ - src/ractorize/base_ractor.rb
25
+ - src/ractorize/garbage_collection.rb
26
+ - src/ractorize/garbage_collection/tracker.rb
27
+ - src/ractorize/garbage_collection/tracking_ractor.rb
24
28
  - src/ractorize/ractorized_class.rb
25
29
  - src/ractorize/ractorized_object.rb
30
+ - src/ractorize/ractorized_object/ractorized_ractor.rb
26
31
  - src/ractorize/thunk.rb
32
+ - src/ractorize/thunk/thunk_ractor.rb
27
33
  homepage: https://github.com/ractor-shack/ractorize
28
34
  licenses:
29
35
  - MPL-2.0