ractorize 0.0.3 → 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 +4 -4
- data/CHANGELOG.md +8 -0
- data/src/ractorize/ractorized_class.rb +2 -4
- data/src/ractorize/ractorized_object.rb +77 -11
- data/src/ractorize/thunk.rb +23 -4
- data/src/ractorize.rb +92 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65a392a1b2b1f65b1886a6650dd712848aa8707f4ab76617e14ba614684660cc
|
|
4
|
+
data.tar.gz: 330eca315af85099f83374687a8526a60a239f9c84e0908b7a77a33b69130722
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec5f2a85e7efcc3c045051cb7a45b83900c82bbc1830deb744fed115c63acf0850067ea03e1b6a18198f19a927a6240ae09999c2fdbf9b316f924eb6fdc48557
|
|
7
|
+
data.tar.gz: c2840a3f37c61353ff66f70996043b78d997cfb969fd6946fc549aa6f572f9257d207e043fe63a388ef486ca58cd21021909b1755c5c06973cecc4d5490b6a56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
## [0.0.3] - 2026-05-21
|
|
2
10
|
|
|
3
11
|
- Treat ==, !=, and ! as predicates and delegate them to the ractor (as well as #equal?)
|
|
@@ -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
|
|
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
|
-
|
|
11
|
+
RactorizedObject.new(:class, target_class, ...)
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def method_missing(method_name, ...)
|
|
@@ -3,18 +3,37 @@ require_relative "thunk"
|
|
|
3
3
|
|
|
4
4
|
module Ractorize
|
|
5
5
|
class RactorizedObject < BasicObject
|
|
6
|
-
def initialize(
|
|
6
|
+
def initialize(mode, *args, **opts, &block)
|
|
7
7
|
@ractor = ::Ractor.new(&RACTOR_PROC)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
31
|
else
|
|
14
|
-
|
|
32
|
+
# :nocov:
|
|
33
|
+
::Kernel.raise "Invalid mode #{mode}"
|
|
34
|
+
# :nocov:
|
|
15
35
|
end
|
|
16
36
|
|
|
17
|
-
# Wow, this works! Scary?
|
|
18
37
|
::Object.instance_method(:freeze).bind(self).call
|
|
19
38
|
end
|
|
20
39
|
|
|
@@ -26,7 +45,7 @@ module Ractorize
|
|
|
26
45
|
self
|
|
27
46
|
end
|
|
28
47
|
|
|
29
|
-
def method_missing(method_name, *args, **opts)
|
|
48
|
+
def method_missing(method_name, *args, **opts, &block)
|
|
30
49
|
if @ractor.default_port.closed?
|
|
31
50
|
::Kernel.raise ::Ractor::ClosedError,
|
|
32
51
|
"You already closed this Ractorized object! No more methods can be sent to it."
|
|
@@ -34,12 +53,50 @@ module Ractorize
|
|
|
34
53
|
|
|
35
54
|
return_port = ::Ractor::Port.new
|
|
36
55
|
|
|
37
|
-
|
|
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
|
|
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.
|
|
38
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
|
|
39
89
|
# Let's assume the user would rather block on all predicate methods than
|
|
40
90
|
# incorrectly get a non-truthy value (thunk is always truthy even if it evaluates as nil/false)
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
100
|
else
|
|
44
101
|
Thunk.new(return_port)
|
|
45
102
|
end
|
|
@@ -62,5 +119,14 @@ module Ractorize
|
|
|
62
119
|
def !=(other) = method_missing(:==, other)
|
|
63
120
|
def ! = method_missing(:!)
|
|
64
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
|
|
65
131
|
end
|
|
66
132
|
end
|
data/src/ractorize/thunk.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module Ractorize
|
|
2
2
|
class Thunk < BasicObject
|
|
3
|
-
|
|
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
|
|
|
@@ -10,8 +13,8 @@ module Ractorize
|
|
|
10
13
|
# is this actually necessary?? Seems so?
|
|
11
14
|
end
|
|
12
15
|
|
|
13
|
-
def method_missing(
|
|
14
|
-
__value__.
|
|
16
|
+
def method_missing(...)
|
|
17
|
+
__value__.__send__(...)
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def respond_to_missing?(method_name, include_all = false)
|
|
@@ -21,7 +24,23 @@ module Ractorize
|
|
|
21
24
|
def __value__
|
|
22
25
|
return @__value__ if defined?(@__value__)
|
|
23
26
|
|
|
24
|
-
|
|
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
|
|
25
44
|
end
|
|
26
45
|
|
|
27
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
|
-
|
|
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,20 +68,55 @@ module Ractorize
|
|
|
17
68
|
close
|
|
18
69
|
break
|
|
19
70
|
else
|
|
20
|
-
|
|
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)
|
|
21
77
|
|
|
22
|
-
|
|
78
|
+
return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
|
|
23
79
|
|
|
24
|
-
|
|
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
|
|
25
102
|
end
|
|
26
103
|
end
|
|
27
104
|
|
|
28
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:
|
|
29
115
|
end
|
|
30
116
|
|
|
31
117
|
class << self
|
|
32
118
|
def ractorize_object(object)
|
|
33
|
-
RactorizedObject.new(object)
|
|
119
|
+
RactorizedObject.new(:object, object)
|
|
34
120
|
end
|
|
35
121
|
|
|
36
122
|
def ractorize_class(klass)
|