ractorize 0.0.4 → 0.0.5

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: 65a392a1b2b1f65b1886a6650dd712848aa8707f4ab76617e14ba614684660cc
4
- data.tar.gz: 330eca315af85099f83374687a8526a60a239f9c84e0908b7a77a33b69130722
3
+ metadata.gz: 717f8ece3185d394b66aade1e78947eb5bb859535a8fa4afdaa88e33ad821cc9
4
+ data.tar.gz: af1aeaf9cc3c4b76625ba228ed5ed60cd77afde597cbcafd4ca059ca2ec52488
5
5
  SHA512:
6
- metadata.gz: ec5f2a85e7efcc3c045051cb7a45b83900c82bbc1830deb744fed115c63acf0850067ea03e1b6a18198f19a927a6240ae09999c2fdbf9b316f924eb6fdc48557
7
- data.tar.gz: c2840a3f37c61353ff66f70996043b78d997cfb969fd6946fc549aa6f572f9257d207e043fe63a388ef486ca58cd21021909b1755c5c06973cecc4d5490b6a56
6
+ metadata.gz: 188fa21adfbdeff96e906f1c38ed41beeab1f4e41d6581a4494cd3245611ae457848b8f0571cd04a83ba453a539073ff4152c2ac5b72ea395d40ed4ada0df4f4
7
+ data.tar.gz: 2c5ffbd40dbc7f2399eed5a8350eaa86a372e2cdc2efec07389c4a16be9deeeba9c9dea1490784306126629682b4444a8b0ffc6b321469b647e71a45c2c5db16
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.0.5] - 2026-05-30
2
+
3
+ - Support auto-freezing certain method-arguments
4
+ - Support moving method arguments if they are not shareable
5
+ - Give ractors a name to help with debugging
6
+
1
7
  ## [0.0.4] - 2026-05-23
2
8
 
3
9
  - Prevent thunks from crossing ractor boundaries
data/README.md CHANGED
@@ -68,9 +68,160 @@ took 0.195 seconds
68
68
  $
69
69
  ```
70
70
 
71
+ ## Advanced usage/some niceties
72
+
73
+ ### Auto-freeze non-shareable stuff passed to ractorized objects/methods
74
+
75
+ Not really in the mood to track down all the strings you're sending to your ractorized objects
76
+ that happen to be non-shareable due to not being frozen? Or maybe you're in the mood
77
+ but don't control the code where they are being initialized? You can just auto-freeze them!
78
+
79
+ You can use `Ractorize.auto_freeze` for that.
80
+
81
+ A few flavors:
82
+
83
+ #### Auto-freezing any instance of a class
84
+
85
+ Let's just freeze all strings sent to any ractorized object.
86
+
87
+ ```ruby
88
+ Ractorize.auto_freeze(String)
89
+
90
+ h = Ractorize[{}]
91
+
92
+ key = "foo"
93
+ value = "bar"
94
+
95
+ puts "value frozen? #{value.frozen?}"
96
+ h[key] = value
97
+ puts "value frozen? #{value.frozen?}"
98
+ ```
99
+
100
+ This results in:
101
+
102
+ ```
103
+ key frozen? false value frozen? false
104
+ key frozen? true value frozen? true
105
+ ```
106
+
107
+ #### Only auto-freezing stuff passed to a specific type of ractorized object
108
+
109
+ You can specify that auto-freezing should only apply to ractorized objects of a specific class.
110
+
111
+ Let's say you want to freeze stuff passed to ractorized instances of Array but not interfere with
112
+ anything ractorized instances of Hash might be doing. You can do this like so:
113
+
114
+ ```ruby
115
+ Ractorize.auto_freeze(Array, String)
116
+
117
+ h = Ractorize[{}]
118
+
119
+ key = "foo"
120
+ value = "bar"
121
+
122
+ puts "Before Hash#[]= value frozen? #{value.frozen?}"
123
+ h[key] = value
124
+ puts "After Hash#[]= value frozen? #{value.frozen?}"
125
+
126
+ a = Ractorize[[]]
127
+
128
+ a.push(value)
129
+ puts "After Array#push value frozen? #{value.frozen?}"
130
+ ```
131
+
132
+ This prints out:
133
+
134
+ ```
135
+ Before Hash#[]= value frozen? false
136
+ After Hash#[]= value frozen? false
137
+ After Array#push value frozen? true
138
+ ```
139
+
140
+ So only sending the string to an Array resulted in auto-freezing it.
141
+
142
+ #### programmatically expressing when to auto-freeze
143
+
144
+ You can also pass a proc to express whether or not to autofreeze an object:
145
+
146
+ ```ruby
147
+ Ractorize.auto_freeze(Ractor.shareable_proc { it.is_a?(String) && it =~ /baz/ })
148
+
149
+ a = Ractorize[[]]
150
+
151
+ strings = ["foo", "bar", "baz"]
152
+
153
+ strings.each { a.push(it) }
154
+
155
+ puts strings.map(&:frozen?).inspect
156
+ ```
157
+
158
+ This outputs:
159
+
160
+ ```
161
+ [false, false, true]
162
+ ```
163
+
164
+ Notice that only the last string, which meets the criteria, was frozen.
165
+
166
+ ### How to move arguments to the receiving ractorized object
167
+
168
+ You can express that you'd like an argument to be moved to the receiving ractorized object.
169
+
170
+ This allows you to not have to worry about if the argument is shareable or not.
171
+
172
+ You will get errors, though, when trying to make use of the moved argument in the calling
173
+ code, just like when using ractors directly and moving objects between them.
174
+
175
+ The interface is identical to `.auto_feeze` but through the method `.move_arg`:
176
+
177
+ ```ruby
178
+ class Foo
179
+ def object_id_of(s) = s.object_id
180
+ end
181
+
182
+ foo = Ractorize[Foo.new]
183
+ s = "asdf"
184
+
185
+ puts "calling ractor s.object_id before #push: #{s.object_id}"
186
+ puts "object_id in receiving ractor Foo#object_id_of: #{foo.object_id_of(s)}"
187
+ puts "s.length in calling ractor: #{s.length}"
188
+ puts
189
+
190
+ puts "Configuring all String instances to be moved to receiving ractor"
191
+ puts
192
+ Ractorize.move_arg(String)
193
+
194
+ puts "calling ractor s.object_id before #push: #{s.object_id}"
195
+ puts "object_id in receiving ractor Foo#object_id_of: #{foo.object_id_of(s)}"
196
+ puts "s.length in calling ractor: #{s.length}"
197
+ ```
198
+
199
+ this outputs:
200
+
201
+ ```
202
+ calling ractor s.object_id before #push: 896
203
+ object_id in receiving ractor Foo#object_id_of: 904
204
+ s.length in calling ractor: 4
205
+
206
+ Configuring all String instances to be moved to receiving ractor
207
+
208
+ calling ractor s.object_id before #push: 896
209
+ object_id in receiving ractor Foo#object_id_of: 896
210
+ example_scripts/auto_freeze/move-arg:25:in 'Ractor::MovedObject#method_missing': can not send any methods to a moved object (Ractor::MovedError)
211
+ ```
212
+
213
+ Notice that before we configure String to be moved, `foo` receives a copy of `s`, hence the different
214
+ object_id.
215
+
216
+ But once we configure String to be moved, now `foo` receives `s` instead of a copy, hence the object_id
217
+ being the same.
218
+
219
+ However, then when we try to print out the length of `s` in the calling ractor, we get a `Ractor::MovedError`.
220
+
71
221
  ## Gotchas
72
222
 
73
- ### Predicate methods not ending in "?" will always return truthy values!
223
+ ### Predicate methods not ending in "?" in `if/unless/until/while/case/when/in` statements will always return truthy values!
224
+
74
225
  If you try to use the return value of a ractorized object (or any instance of a ractorized class)
75
226
  in a boolean expression, it will always be truthy!!
76
227
 
@@ -108,6 +259,13 @@ end
108
259
 
109
260
  This will correctly print out `It's not empty!`.
110
261
 
262
+ Note that this isn't necessary with methods ending in "?" as this will automatically block
263
+ and return the boolean value.
264
+
265
+ Also, the predicate methods `==`, `!=` and `!` also will automatically block and return the boolean
266
+ value, just like methods ending in "?". So you can freely do `if Ractorize["asdf"] == "asdf"` works
267
+ perfectly fine just like predicate methods ending in "?".
268
+
111
269
  ### Calling a method on a closed ractorized object might result in a deadlock!
112
270
 
113
271
  It will usually raise a `Ractor::CloseError` but once in a while it can deadlock.
@@ -4,7 +4,7 @@ require_relative "thunk"
4
4
  module Ractorize
5
5
  class RactorizedObject < BasicObject
6
6
  def initialize(mode, *args, **opts, &block)
7
- @ractor = ::Ractor.new(&RACTOR_PROC)
7
+ @ractor = ::Ractor.new(name: "#{args.first}<#{args.first.object_id}>", &RACTOR_PROC)
8
8
 
9
9
  case mode
10
10
  when :object
@@ -12,22 +12,46 @@ module Ractorize
12
12
 
13
13
  outside_object = args.first
14
14
 
15
- ::Ractorize.resolve_all_thunks(outside_object)
15
+ @__target_class__ = outside_object.class
16
16
 
17
17
  if ::Ractor.shareable?(outside_object)
18
18
  @ractor << outside_object
19
19
  else
20
+ ::Ractorize.resolve_all_thunks(outside_object)
20
21
  @ractor.send(outside_object, move: true)
21
22
  end
22
23
  when :class
23
- @ractor << :class
24
-
25
24
  klass, *args = args
26
25
 
27
- ::Ractorize.resolve_all_thunks(args)
28
- ::Ractorize.resolve_all_thunks(opts)
26
+ @__target_class__ = klass
27
+
28
+ to_move = ::Ractorize.prepare_args(@__target_class__, args, opts)
29
+
30
+ if to_move&.any?
31
+ @ractor << :class_arg_by_arg
32
+ @ractor << klass
33
+
34
+ args.each do |arg|
35
+ @ractor << :arg
36
+ @ractor.send(arg, move: to_move.include?(arg))
37
+ end
38
+
39
+ opts.each_pair do |name, value|
40
+ @ractor << :kwarg
41
+ @ractor << name
42
+ @ractor.send(value, move: to_move.include?(value))
43
+ end
29
44
 
30
- @ractor << [klass, args.freeze, opts.dup.freeze, block].freeze
45
+ if block
46
+ @ractor << :block
47
+ @ractor << block
48
+ end
49
+
50
+ @ractor << :done
51
+ else
52
+ @ractor << :class
53
+ @ractor << [klass, args.freeze, opts.dup.freeze, block].freeze
54
+ end
31
55
  else
32
56
  # :nocov:
33
57
  ::Kernel.raise "Invalid mode #{mode}"
@@ -53,10 +77,29 @@ module Ractorize
53
77
 
54
78
  return_port = ::Ractor::Port.new
55
79
 
56
- ::Ractorize.resolve_all_thunks(args)
57
- ::Ractorize.resolve_all_thunks(opts)
80
+ to_move = ::Ractorize.prepare_args(@__target_class__, args, opts)
81
+
82
+ if to_move&.any?
83
+ @ractor << [:__invoke_arg_by_arg__, [].freeze, {}.freeze, return_port, !!block]
84
+
85
+ args_port = return_port.receive
86
+ args_port << method_name
87
+
88
+ args.each do |arg|
89
+ args_port << :arg
90
+ args_port.send(arg, move: to_move.include?(arg))
91
+ end
58
92
 
59
- @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port, !!block].freeze
93
+ opts.each_pair do |name, value|
94
+ args_port << :kwarg
95
+ args_port << name
96
+ args_port.send(value, move: to_move.include?(value))
97
+ end
98
+
99
+ args_port << :done
100
+ else
101
+ @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port, !!block].freeze
102
+ end
60
103
 
61
104
  if block
62
105
  stop = false
@@ -28,7 +28,8 @@ module Ractorize
28
28
  __return_value_port__.receive
29
29
  else
30
30
  # :nocov:
31
- raise "Somehow this thunk was passed between ractors but wasn't resolved first."
31
+ ::Kernel.raise EscapingRactorError,
32
+ "Somehow this thunk was passed between ractors but wasn't resolved first."
32
33
  # :nocov:
33
34
  end
34
35
 
@@ -43,12 +44,9 @@ module Ractorize
43
44
  value
44
45
  end
45
46
 
46
- def !
47
- !__value__
48
- end
49
-
50
- def ==(other)
51
- __value__ == other || super
52
- end
47
+ def ! = !__value__
48
+ def ==(other) = __value__ == other || super
49
+ def !=(other) = __value__ != other || super
50
+ def equal?(other) = __value__.equal?(other) || super
53
51
  end
54
52
  end
data/src/ractorize.rb CHANGED
@@ -4,6 +4,55 @@ require_relative "ractorize/ractorized_class"
4
4
 
5
5
  module Ractorize
6
6
  class << self
7
+ # TODO: figure out a way to magically get a ractor-shareable proc from a non-ractor-shareable proc
8
+ def auto_freeze(target, class_or_proc = nil)
9
+ @auto_freeze = @auto_freeze ? @auto_freeze.dup : []
10
+
11
+ unless Ractor.shareable?(target)
12
+ # :nocov:
13
+ raise "#{target} isn't shareable so can't use it to auto-freeze"
14
+ # :nocov:
15
+ end
16
+
17
+ @auto_freeze << if class_or_proc
18
+ unless Ractor.shareable?(class_or_proc)
19
+ # :nocov:
20
+ raise "#{class_or_proc} isn't shareable so can't use it to auto-freeze"
21
+ # :nocov:
22
+ end
23
+
24
+ [target, class_or_proc]
25
+ else
26
+ target
27
+ end
28
+
29
+ @auto_freeze.freeze
30
+ end
31
+
32
+ def move_arg(target, class_or_proc = nil)
33
+ @move_arg = @move_arg ? @move_arg.dup : []
34
+
35
+ unless Ractor.shareable?(target)
36
+ # :nocov:
37
+ raise "#{target} isn't shareable so can't use it to auto-freeze"
38
+ # :nocov:
39
+ end
40
+
41
+ @move_arg << if class_or_proc
42
+ unless Ractor.shareable?(class_or_proc)
43
+ # :nocov:
44
+ raise "#{class_or_proc} isn't shareable so can't use it to auto-freeze"
45
+ # :nocov:
46
+ end
47
+
48
+ [target, class_or_proc]
49
+ else
50
+ target
51
+ end
52
+
53
+ @move_arg.freeze
54
+ end
55
+
7
56
  def any_thunks?(structure)
8
57
  # rubocop:disable Lint/UnreachableLoop
9
58
  each_thunk(structure) { return true }
@@ -17,6 +66,87 @@ module Ractorize
17
66
  each_thunk(structure, &:__value__)
18
67
  end
19
68
 
69
+ def to_move(target_class, args)
70
+ return unless @move_arg
71
+
72
+ move_set = nil
73
+
74
+ args.each do |arg|
75
+ next if Ractor.shareable?(arg)
76
+
77
+ @move_arg.each do |rule|
78
+ if rule.is_a?(::Array)
79
+ target, rule = rule
80
+ next unless target == target_class
81
+ end
82
+
83
+ move_it = if rule.is_a?(::Proc)
84
+ rule.call(arg)
85
+ else
86
+ rule === arg
87
+ end
88
+
89
+ if move_it
90
+ move_set ||= Set.new
91
+ move_set << arg
92
+ break
93
+ end
94
+ end
95
+ end
96
+
97
+ move_set
98
+ end
99
+
100
+ def apply_auto_freeze(target_class, arg)
101
+ return unless @auto_freeze
102
+ return if Ractor.shareable?(arg)
103
+
104
+ # TODO: should we handle instance variables like we do with thunks?
105
+ case arg
106
+ when ::Hash
107
+ arg.each_pair do |key, value|
108
+ apply_auto_freeze(target_class, key)
109
+ apply_auto_freeze(target_class, value)
110
+ end
111
+ when ::Array
112
+ arg.each { apply_auto_freeze(target_class, it) }
113
+ end
114
+
115
+ return if Ractor.shareable?(arg)
116
+
117
+ @auto_freeze.each do |rule|
118
+ if rule.is_a?(::Array)
119
+ target, rule = rule
120
+ next unless target == target_class
121
+ end
122
+
123
+ freeze_it = if rule.is_a?(::Proc)
124
+ rule.call(arg)
125
+ else
126
+ rule === arg
127
+ end
128
+
129
+ if freeze_it
130
+ arg.freeze
131
+ break
132
+ end
133
+ end
134
+ end
135
+
136
+ def prepare_args(target_class, args, opts, skip_move: false)
137
+ unless opts.empty?
138
+ args = [*args, *opts.values]
139
+ end
140
+
141
+ args.each { apply_auto_freeze(target_class, it) }
142
+
143
+ ::Ractorize.resolve_all_thunks(args)
144
+
145
+ return nil if skip_move
146
+
147
+ to_move(target_class, args)
148
+ end
149
+
20
150
  def each_thunk(structure, seen = Set.new, &block)
21
151
  return block.call(structure) if Thunk === structure
22
152
  return if seen.include?(structure)
@@ -40,6 +170,36 @@ module Ractorize
40
170
  end
41
171
  end
42
172
  end
173
+
174
+ def extract_args(port_like)
175
+ args = []
176
+ opts = {}
177
+ block = nil
178
+
179
+ loop do
180
+ arg_type = port_like.__send__(:receive)
181
+
182
+ case arg_type
183
+ when :arg
184
+ args << port_like.__send__(:receive)
185
+ when :kwarg
186
+ name = port_like.__send__(:receive)
187
+ value = port_like.__send__(:receive)
188
+
189
+ opts[name] = value
190
+ when :block
191
+ block = port_like.__send__(:receive)
192
+ when :done
193
+ break
194
+ else
195
+ # :nocov:
196
+ ::Kernel.raise "Unknown class_by_arg arg type #{arg_type}"
197
+ # :nocov:
198
+ end
199
+ end
200
+
201
+ [args, opts, block]
202
+ end
43
203
  end
44
204
 
45
205
  # Putting this in a constant so we can get test coverage on it since not sure how to get coverage
@@ -50,9 +210,19 @@ module Ractorize
50
210
  object = case mode
51
211
  when :class
52
212
  klass, args, opts, block = receive
53
- klass.new(*args, **opts, &block)
213
+ target_class = klass
214
+ klass.new(*args.freeze, **opts.freeze, &block)
54
215
  when :object
55
- receive
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)
56
226
  else
57
227
  # :nocov:
58
228
  ::Kernel.raise "Invalid mode #{mode}"
@@ -68,12 +238,19 @@ module Ractorize
68
238
  close
69
239
  break
70
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
+
71
249
  if block_given
72
- block_result_port = Ractor::Port.new
250
+ block_result_port = ::Ractor::Port.new
73
251
 
74
252
  value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
75
- ::Ractorize.resolve_all_thunks(args)
76
- ::Ractorize.resolve_all_thunks(opts)
253
+ ::Ractorize.prepare_args(target_class, args, opts, skip_move: true)
77
254
 
78
255
  return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
79
256
 
@@ -95,7 +272,7 @@ module Ractorize
95
272
  else
96
273
  value = object.__send__(method_name, *method_args, **opts)
97
274
 
98
- value = value.__value__ while Thunk === value
275
+ value = value.__value__ while ::Ractorize::Thunk === value
99
276
 
100
277
  return_port << value
101
278
  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.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi