ractorize 0.0.2 → 0.0.4

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: 259d8a0f04fdab353871748575089e83199cfad7d81440d4e6f3a957366c9301
4
- data.tar.gz: e05e67142fdd2c081ec6baf244df8f7dadf991437c12e7d27cc87c793c863956
3
+ metadata.gz: 65a392a1b2b1f65b1886a6650dd712848aa8707f4ab76617e14ba614684660cc
4
+ data.tar.gz: 330eca315af85099f83374687a8526a60a239f9c84e0908b7a77a33b69130722
5
5
  SHA512:
6
- metadata.gz: ebd8a74f6bd6040740c5d41adb81f4caba0508ec28f1025eedb8a2feef826f5f09d764535593d464d3355bebe64941138f5789c9792223df8fe095af7384d985
7
- data.tar.gz: 30b2f4d65a70d7218b5e3d76b1f6c04f3cb0c784fe8cd639d53770705caa61163c5c84ba678cd725f8f19296c1c6d985c6310ff461dbade04015522f96a8b155
6
+ metadata.gz: ec5f2a85e7efcc3c045051cb7a45b83900c82bbc1830deb744fed115c63acf0850067ea03e1b6a18198f19a927a6240ae09999c2fdbf9b316f924eb6fdc48557
7
+ data.tar.gz: c2840a3f37c61353ff66f70996043b78d997cfb969fd6946fc549aa6f572f9257d207e043fe63a388ef486ca58cd21021909b1755c5c06973cecc4d5490b6a56
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.0.4] - 2026-05-23
2
+
3
+ - Prevent thunks from crossing ractor boundaries
4
+ - Create instances of ractorized classes inside the ractor instead of moving them
5
+ - Add support for methods that take blocks
6
+ - Add #to_s/#inspect to RactorizedObject to help with debugging
7
+ - Use #__send__ instead of #send to work with BasicObject
8
+
9
+ ## [0.0.3] - 2026-05-21
10
+
11
+ - Treat ==, !=, and ! as predicates and delegate them to the ractor (as well as #equal?)
12
+ - Don't wrap thunks in more thunks. Unwrap them in RACTOR_PROC which can help with Port#receive issues
13
+ - Send a frozen array to the ractor to avoid dup/clone
14
+ - Something somewhere in other projects calls Thunk#initialize_clone, so make a placeholder for it
15
+ - Do not bother moving shareable objects to the ractor, just send them
16
+
1
17
  ## [0.0.2] - 2026-05-18
2
18
 
3
19
  - Make sure ractorized objects are shareable. This requires them to be unusable after closing (or joining) them.
@@ -1,16 +1,14 @@
1
1
  module Ractorize
2
2
  class RactorizedClass
3
3
  class << self
4
- attr_accessor :target_class
5
-
6
4
  def [](klass)
7
5
  ractorized_class = Class.new(RactorizedClass)
8
- ractorized_class.target_class = klass
6
+ ractorized_class.define_singleton_method(:target_class, Ractor.shareable_proc { klass })
9
7
  ractorized_class
10
8
  end
11
9
 
12
10
  def new(...)
13
- Ractorize.ractorize_object(target_class.new(...))
11
+ RactorizedObject.new(:class, target_class, ...)
14
12
  end
15
13
 
16
14
  def method_missing(method_name, ...)
@@ -3,14 +3,37 @@ require_relative "thunk"
3
3
 
4
4
  module Ractorize
5
5
  class RactorizedObject < BasicObject
6
- def initialize(outside_object)
6
+ def initialize(mode, *args, **opts, &block)
7
7
  @ractor = ::Ractor.new(&RACTOR_PROC)
8
8
 
9
- # It doesn't seem like we have a way to move the object into the ractor via its constructor so do
10
- # it with #<< instead.
11
- @ractor.<<(outside_object, move: true)
9
+ case mode
10
+ when :object
11
+ @ractor << :object
12
+
13
+ outside_object = args.first
14
+
15
+ ::Ractorize.resolve_all_thunks(outside_object)
16
+
17
+ if ::Ractor.shareable?(outside_object)
18
+ @ractor << outside_object
19
+ else
20
+ @ractor.send(outside_object, move: true)
21
+ end
22
+ when :class
23
+ @ractor << :class
24
+
25
+ klass, *args = args
26
+
27
+ ::Ractorize.resolve_all_thunks(args)
28
+ ::Ractorize.resolve_all_thunks(opts)
29
+
30
+ @ractor << [klass, args.freeze, opts.dup.freeze, block].freeze
31
+ else
32
+ # :nocov:
33
+ ::Kernel.raise "Invalid mode #{mode}"
34
+ # :nocov:
35
+ end
12
36
 
13
- # Wow, this works! Scary?
14
37
  ::Object.instance_method(:freeze).bind(self).call
15
38
  end
16
39
 
@@ -22,7 +45,7 @@ module Ractorize
22
45
  self
23
46
  end
24
47
 
25
- def method_missing(method_name, *args, **opts)
48
+ def method_missing(method_name, *args, **opts, &block)
26
49
  if @ractor.default_port.closed?
27
50
  ::Kernel.raise ::Ractor::ClosedError,
28
51
  "You already closed this Ractorized object! No more methods can be sent to it."
@@ -30,12 +53,50 @@ module Ractorize
30
53
 
31
54
  return_port = ::Ractor::Port.new
32
55
 
33
- @ractor << [method_name, args, opts, return_port]
56
+ ::Ractorize.resolve_all_thunks(args)
57
+ ::Ractorize.resolve_all_thunks(opts)
58
+
59
+ @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port, !!block].freeze
60
+
61
+ if block
62
+ stop = false
63
+ value = nil
64
+
65
+ until stop
66
+ data = return_port.receive
34
67
 
68
+ # Seems SimpleCov branch coverage doesn't like that we don't test the non-exhaustive
69
+ # pattern path, but since that's purely defensive I have no interest in testing it.
70
+
71
+ # :nocov:
72
+ case data
73
+ # :nocov:
74
+ in :return, value
75
+ stop = true
76
+ in :yield, [yielded_args, yielded_opts, yielded_block], block_result_port
77
+ # TODO: yielded_block likely won't work when actually used
78
+ # so we should probably instead just raise an exception
79
+ # TODO: handle break and also raise in the block
80
+ block_result = block.call(*yielded_args.freeze, **yielded_opts.freeze, &yielded_block)
81
+
82
+ block_result = block_result.__value__ while ::Ractorize::Thunk === block_result
83
+
84
+ block_result_port << [:normal, block_result].freeze
85
+ end
86
+ end
87
+
88
+ value
35
89
  # Let's assume the user would rather block on all predicate methods than
36
90
  # incorrectly get a non-truthy value (thunk is always truthy even if it evaluates as nil/false)
37
- if method_name.end_with?("?")
38
- return_port.receive
91
+ elsif method_name == :== || method_name == :! || method_name == :!= ||
92
+ method_name == :inspect || method_name == :to_s || method_name.end_with?("?")
93
+ value = return_port.receive
94
+
95
+ # :nocov:
96
+ ::Kernel.raise ::Ractorize::Thunk::EscapingRactorError if ::Ractorize::Thunk === value
97
+ # :nocov:
98
+
99
+ value
39
100
  else
40
101
  Thunk.new(return_port)
41
102
  end
@@ -53,5 +114,19 @@ module Ractorize
53
114
  def respond_to_missing?(method_name, include_all = false)
54
115
  method_missing(:respond_to?, method_name, include_all)
55
116
  end
117
+
118
+ def ==(other) = method_missing(:==, other)
119
+ def !=(other) = method_missing(:==, other)
120
+ def ! = method_missing(:!)
121
+ def equal?(other) = method_missing(:equal?, other)
122
+
123
+ def to_s = inspect
124
+
125
+ def inspect
126
+ object_id = ::Object.instance_method(:object_id).bind(self).call
127
+ moved_object_inspect = method_missing(:inspect)
128
+
129
+ "RactorizedObject<#{object_id}>[#{moved_object_inspect}]".freeze
130
+ end
56
131
  end
57
132
  end
@@ -1,13 +1,20 @@
1
1
  module Ractorize
2
2
  class Thunk < BasicObject
3
- attr_accessor :__return_value_port__
3
+ class EscapingRactorError < ::StandardError; end
4
+
5
+ attr_accessor :__return_value_port__, :__ractor__
4
6
 
5
7
  def initialize(return_value_port)
8
+ self.__ractor__ = ::Ractor.current
6
9
  self.__return_value_port__ = return_value_port
7
10
  end
8
11
 
9
- def method_missing(method_name, *)
10
- __value__.send(method_name, *)
12
+ def initialize_clone(...)
13
+ # is this actually necessary?? Seems so?
14
+ end
15
+
16
+ def method_missing(...)
17
+ __value__.__send__(...)
11
18
  end
12
19
 
13
20
  def respond_to_missing?(method_name, include_all = false)
@@ -17,7 +24,23 @@ module Ractorize
17
24
  def __value__
18
25
  return @__value__ if defined?(@__value__)
19
26
 
20
- @__value__ = __return_value_port__.receive
27
+ value = if ::Ractor.current == __ractor__
28
+ __return_value_port__.receive
29
+ else
30
+ # :nocov:
31
+ raise "Somehow this thunk was passed between ractors but wasn't resolved first."
32
+ # :nocov:
33
+ end
34
+
35
+ # :nocov:
36
+ ::Kernel.raise EscapingRactorError if ::Ractorize::Thunk === value
37
+ # :nocov:
38
+
39
+ @__value__ = value
40
+
41
+ ::Object.instance_method(:freeze).bind(self).call
42
+
43
+ value
21
44
  end
22
45
 
23
46
  def !
data/src/ractorize.rb CHANGED
@@ -3,13 +3,64 @@ require_relative "ractorize/ractorized_object"
3
3
  require_relative "ractorize/ractorized_class"
4
4
 
5
5
  module Ractorize
6
+ class << self
7
+ def any_thunks?(structure)
8
+ # rubocop:disable Lint/UnreachableLoop
9
+ each_thunk(structure) { return true }
10
+ # rubocop:enable Lint/UnreachableLoop
11
+ false
12
+ end
13
+
14
+ # Unfortunately, can't read from a port that a different ractor made.
15
+ # Not sure why that is but we need to handle that case.
16
+ def resolve_all_thunks(structure)
17
+ each_thunk(structure, &:__value__)
18
+ end
19
+
20
+ def each_thunk(structure, seen = Set.new, &block)
21
+ return block.call(structure) if Thunk === structure
22
+ return if seen.include?(structure)
23
+
24
+ seen << structure
25
+
26
+ case structure
27
+ when Array
28
+ structure.each { each_thunk(it, seen, &block) }
29
+ when Hash
30
+ each_thunk(structure.keys, seen, &block)
31
+ each_thunk(structure.values, seen, &block)
32
+ when Struct
33
+ each_thunk(structure.values, seen, &block)
34
+ else
35
+ ivarsget = ::Object.instance_method(:instance_variables)
36
+ iget = ::Object.instance_method(:instance_variable_get)
37
+
38
+ ivarsget.bind(structure).call.each do |var|
39
+ each_thunk(iget.bind(structure).call(var), seen, &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
6
45
  # Putting this in a constant so we can get test coverage on it since not sure how to get coverage
7
46
  # on something inside a ractor.
8
47
  RACTOR_PROC = proc do
9
- object = receive
48
+ mode = receive
49
+
50
+ object = case mode
51
+ when :class
52
+ klass, args, opts, block = receive
53
+ klass.new(*args, **opts, &block)
54
+ when :object
55
+ receive
56
+ else
57
+ # :nocov:
58
+ ::Kernel.raise "Invalid mode #{mode}"
59
+ # :nocov:
60
+ end
10
61
 
11
62
  loop do
12
- method_name, method_args, opts, return_port = receive
63
+ method_name, method_args, opts, return_port, block_given = receive
13
64
 
14
65
  case method_name
15
66
  when :__close__
@@ -17,18 +68,55 @@ module Ractorize
17
68
  close
18
69
  break
19
70
  else
20
- value = object.__send__(method_name, *method_args, **opts)
71
+ if block_given
72
+ block_result_port = Ractor::Port.new
73
+
74
+ value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
75
+ ::Ractorize.resolve_all_thunks(args)
76
+ ::Ractorize.resolve_all_thunks(opts)
77
+
78
+ return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
21
79
 
22
- return_port << value
80
+ outcome_type, return_value = block_result_port.receive
81
+
82
+ case outcome_type
83
+ when :normal
84
+ return_value
85
+ when :break
86
+ break return_value
87
+ else
88
+ # :nocov:
89
+ ::Kernel.raise "Not sure how to handle outcome_type #{outcome_type}"
90
+ # :nocov:
91
+ end
92
+ end
93
+
94
+ return_port << [:return, value].freeze
95
+ else
96
+ value = object.__send__(method_name, *method_args, **opts)
97
+
98
+ value = value.__value__ while Thunk === value
99
+
100
+ return_port << value
101
+ end
23
102
  end
24
103
  end
25
104
 
26
105
  object
106
+ rescue => e
107
+ # :nocov:
108
+ ::Kernel.puts
109
+ ::Kernel.puts "an error!!! #{e.class} #{e.message} #{e}"
110
+ ::Kernel.puts e.backtrace
111
+ ::Kernel.puts
112
+
113
+ raise
114
+ # :nocov:
27
115
  end
28
116
 
29
117
  class << self
30
118
  def ractorize_object(object)
31
- RactorizedObject.new(object)
119
+ RactorizedObject.new(:object, object)
32
120
  end
33
121
 
34
122
  def ractorize_class(klass)
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.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi