backports 3.18.0 → 3.23.0
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 +435 -292
- data/Gemfile +3 -16
- data/README.md +306 -156
- data/backports.gemspec +1 -1
- data/lib/backports/2.0.0.rb +1 -1
- data/lib/backports/2.1.0/module/singleton_class.rb +8 -0
- data/lib/backports/2.1.0.rb +1 -1
- data/lib/backports/2.2.0/string/unicode_normalize.rb +3 -3
- data/lib/backports/2.2.0.rb +1 -1
- data/lib/backports/2.3.0/queue/close.rb +48 -0
- data/lib/backports/2.3.0/string.rb +3 -0
- data/lib/backports/2.3.0/struct/dig.rb +2 -0
- data/lib/backports/2.3.0.rb +1 -1
- data/lib/backports/2.4.0/bignum/dup.rb +5 -0
- data/lib/backports/2.4.0/bignum.rb +3 -0
- data/lib/backports/2.4.0/string/unpack1.rb +7 -0
- data/lib/backports/2.4.0.rb +1 -1
- data/lib/backports/2.5.0/dir/children.rb +4 -0
- data/lib/backports/2.5.0/dir/each_child.rb +7 -0
- data/lib/backports/2.5.0/hash/transform_keys.rb +10 -3
- data/lib/backports/2.5.0/string/undump.rb +2 -2
- data/lib/backports/2.5.0.rb +1 -1
- data/lib/backports/2.5.rb +1 -1
- data/lib/backports/2.6.0/enumerable/chain.rb +2 -0
- data/lib/backports/2.6.0.rb +2 -2
- data/lib/backports/2.6.rb +1 -1
- data/lib/backports/2.7.0/complex/{comparision.rb → comparison.rb} +0 -0
- data/lib/backports/2.7.0/enumerable/tally.rb +4 -3
- data/lib/backports/2.7.0/symbol/end_with.rb +9 -0
- data/lib/backports/2.7.0/symbol.rb +3 -0
- data/lib/backports/2.7.0.rb +2 -2
- data/lib/backports/3.0.0/env/except.rb +10 -0
- data/lib/backports/3.0.0/env.rb +3 -0
- data/lib/backports/3.0.0/hash/except.rb +10 -0
- data/lib/backports/3.0.0/hash/transform_keys.rb +48 -0
- data/lib/backports/3.0.0/hash.rb +3 -0
- data/lib/backports/3.0.0/ractor.rb +19 -0
- data/lib/backports/3.0.0/symbol/name.rb +21 -0
- data/lib/backports/3.0.0/symbol.rb +3 -0
- data/lib/backports/3.0.0.rb +3 -0
- data/lib/backports/3.0.rb +1 -0
- data/lib/backports/3.1.0/array/intersect.rb +16 -0
- data/lib/backports/3.1.0/array.rb +3 -0
- data/lib/backports/3.1.0/class/descendants.rb +11 -0
- data/lib/backports/3.1.0/class/subclasses.rb +11 -0
- data/lib/backports/3.1.0/class.rb +3 -0
- data/lib/backports/3.1.0/enumerable/compact.rb +5 -0
- data/lib/backports/3.1.0/enumerable/tally.rb +18 -0
- data/lib/backports/3.1.0/enumerable.rb +3 -0
- data/lib/backports/3.1.0/file/dirname.rb +16 -0
- data/lib/backports/3.1.0/file.rb +3 -0
- data/lib/backports/3.1.0/integer/try_convert.rb +7 -0
- data/lib/backports/3.1.0/integer.rb +3 -0
- data/lib/backports/3.1.0/match_data/match.rb +5 -0
- data/lib/backports/3.1.0/match_data/match_length.rb +6 -0
- data/lib/backports/3.1.0/match_data.rb +3 -0
- data/lib/backports/3.1.0/struct/keyword_init.rb +5 -0
- data/lib/backports/3.1.0/struct.rb +3 -0
- data/lib/backports/3.1.0.rb +3 -0
- data/lib/backports/3.1.rb +1 -0
- data/lib/backports/latest.rb +1 -1
- data/lib/backports/ractor/cloner.rb +103 -0
- data/lib/backports/ractor/errors.rb +20 -0
- data/lib/backports/ractor/filtered_queue.rb +205 -0
- data/lib/backports/ractor/queues.rb +66 -0
- data/lib/backports/ractor/ractor.rb +272 -0
- data/lib/backports/ractor/sharing.rb +97 -0
- data/lib/backports/version.rb +1 -1
- metadata +51 -8
@@ -0,0 +1,103 @@
|
|
1
|
+
# shareable_constant_value: literal
|
2
|
+
|
3
|
+
using ::RubyNext if defined?(::RubyNext)
|
4
|
+
|
5
|
+
module Backports
|
6
|
+
class Ractor
|
7
|
+
class Cloner
|
8
|
+
class << self
|
9
|
+
def deep_clone(obj)
|
10
|
+
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
|
11
|
+
|
12
|
+
new.deep_clone(obj)
|
13
|
+
end
|
14
|
+
|
15
|
+
private :new
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@processed = {}.compare_by_identity
|
20
|
+
@changed = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def deep_clone(obj)
|
24
|
+
result = process(obj) do |r|
|
25
|
+
copy_contents(r)
|
26
|
+
end
|
27
|
+
return result if result
|
28
|
+
|
29
|
+
Ractor.ractor_mark_set_shareable(@processed)
|
30
|
+
obj
|
31
|
+
end
|
32
|
+
|
33
|
+
# Yields a deep copy.
|
34
|
+
# If no deep copy is needed, `obj` is returned and
|
35
|
+
# nothing is yielded
|
36
|
+
private def clone_deeper(obj)
|
37
|
+
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
|
38
|
+
|
39
|
+
result = process(obj) do |r|
|
40
|
+
copy_contents(r)
|
41
|
+
end
|
42
|
+
return obj unless result
|
43
|
+
|
44
|
+
yield result if block_given?
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
# Yields if `obj` is a new structure
|
49
|
+
# Returns the deep copy, or `false` if no deep copy is needed
|
50
|
+
private def process(obj)
|
51
|
+
@processed.fetch(obj) do
|
52
|
+
# For recursive structures, assume that we'll need a duplicate.
|
53
|
+
# If that's not the case, we will have duplicated the whole structure
|
54
|
+
# for nothing...
|
55
|
+
@processed[obj] = result = obj.dup
|
56
|
+
changed = track_change { yield result }
|
57
|
+
return false if obj.frozen? && !changed
|
58
|
+
|
59
|
+
@changed = true
|
60
|
+
result.freeze if obj.frozen?
|
61
|
+
|
62
|
+
result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# returns if the block called `deep clone` and that the deep copy was needed
|
67
|
+
private def track_change
|
68
|
+
prev = @changed
|
69
|
+
@changed = false
|
70
|
+
yield
|
71
|
+
@changed
|
72
|
+
ensure
|
73
|
+
@changed = prev
|
74
|
+
end
|
75
|
+
|
76
|
+
# modifies in place `obj` by calling `deep clone` on its contents
|
77
|
+
private def copy_contents(obj)
|
78
|
+
case obj
|
79
|
+
when ::Hash
|
80
|
+
if obj.default
|
81
|
+
clone_deeper(obj.default) do |copy|
|
82
|
+
obj.default = copy
|
83
|
+
end
|
84
|
+
end
|
85
|
+
obj.transform_keys! { |key| clone_deeper(key) }
|
86
|
+
obj.transform_values! { |value| clone_deeper(value) }
|
87
|
+
when ::Array
|
88
|
+
obj.map! { |item| clone_deeper(item) }
|
89
|
+
when ::Struct
|
90
|
+
obj.each_pair do |key, item|
|
91
|
+
clone_deeper(item) { |copy| obj[key] = copy }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
obj.instance_variables.each do |var|
|
95
|
+
clone_deeper(obj.instance_variable_get(var)) do |copy|
|
96
|
+
obj.instance_variable_set(var, copy)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
private_constant :Cloner
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# shareable_constant_value: literal
|
2
|
+
|
3
|
+
module Backports
|
4
|
+
class Ractor
|
5
|
+
class ClosedError < ::StopIteration
|
6
|
+
end
|
7
|
+
|
8
|
+
class Error < ::StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class RemoteError < Error
|
12
|
+
attr_reader :ractor
|
13
|
+
|
14
|
+
def initialize(message = nil)
|
15
|
+
@ractor = Ractor.current
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# shareable_constant_value: literal
|
2
|
+
|
3
|
+
module Backports
|
4
|
+
# Like ::Queue, but with
|
5
|
+
# - filtering
|
6
|
+
# - timeout
|
7
|
+
# - raises on closed queues
|
8
|
+
#
|
9
|
+
# Independent from other Ractor related backports.
|
10
|
+
class FilteredQueue
|
11
|
+
CONSUME_ON_ESCAPE = true
|
12
|
+
|
13
|
+
class ClosedQueueError < ::ClosedQueueError
|
14
|
+
end
|
15
|
+
|
16
|
+
class TimeoutError < ::ThreadError
|
17
|
+
end
|
18
|
+
|
19
|
+
class Message
|
20
|
+
# Not using Struct as we want comparision by identity
|
21
|
+
attr_reader :value
|
22
|
+
attr_accessor :reserved
|
23
|
+
|
24
|
+
def initialize(value)
|
25
|
+
@value = value
|
26
|
+
@reserved = false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
private_constant :Message
|
30
|
+
|
31
|
+
attr_reader :num_waiting
|
32
|
+
|
33
|
+
# Timeout processing based on https://spin.atomicobject.com/2017/06/28/queue-pop-with-timeout-fixed/
|
34
|
+
def initialize
|
35
|
+
@mutex = ::Mutex.new
|
36
|
+
@queue = []
|
37
|
+
@closed = false
|
38
|
+
@received = ::ConditionVariable.new
|
39
|
+
@num_waiting = 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
@mutex.synchronize do
|
44
|
+
@closed = true
|
45
|
+
@received.broadcast
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def closed?
|
51
|
+
@closed
|
52
|
+
end
|
53
|
+
|
54
|
+
def <<(x)
|
55
|
+
@mutex.synchronize do
|
56
|
+
ensure_open
|
57
|
+
@queue << Message.new(x)
|
58
|
+
@received.signal
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
alias_method :push, :<<
|
63
|
+
|
64
|
+
def clear
|
65
|
+
@mutex.synchronize do
|
66
|
+
@queue.clear
|
67
|
+
end
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def pop(timeout: nil, &block)
|
72
|
+
msg = nil
|
73
|
+
exclude = [] if block # exclusion list of messages rejected by this call
|
74
|
+
timeout_time = timeout + Time.now.to_f if timeout
|
75
|
+
while true do # rubocop:disable Style/InfiniteLoop, Style/WhileUntilDo
|
76
|
+
@mutex.synchronize do
|
77
|
+
reenter if reentrant?
|
78
|
+
msg = acquire!(timeout_time, exclude)
|
79
|
+
return consume!(msg).value unless block
|
80
|
+
end
|
81
|
+
return msg.value if filter?(msg, &block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def empty?
|
86
|
+
avail = @mutex.synchronize do
|
87
|
+
available!
|
88
|
+
end
|
89
|
+
|
90
|
+
!avail
|
91
|
+
end
|
92
|
+
|
93
|
+
protected def timeout_value
|
94
|
+
raise self.class::TimeoutError, "timeout elapsed"
|
95
|
+
end
|
96
|
+
|
97
|
+
protected def closed_queue_value
|
98
|
+
ensure_open
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return if outer message should be consumed or not
|
102
|
+
protected def reenter
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
### private section
|
107
|
+
#
|
108
|
+
# bang methods require synchronization
|
109
|
+
|
110
|
+
# @returns:
|
111
|
+
# * true if message consumed (block result truthy or due to reentrant call)
|
112
|
+
# * false if rejected
|
113
|
+
private def filter?(msg)
|
114
|
+
consume = self.class::CONSUME_ON_ESCAPE
|
115
|
+
begin
|
116
|
+
reentered = consume_on_reentry(msg) do
|
117
|
+
consume = !!(yield msg.value)
|
118
|
+
end
|
119
|
+
reentered ? reenter : consume
|
120
|
+
ensure
|
121
|
+
commit(msg, consume) unless reentered
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @returns msg
|
126
|
+
private def consume!(msg)
|
127
|
+
@queue.delete(msg)
|
128
|
+
end
|
129
|
+
|
130
|
+
private def reject!(msg)
|
131
|
+
msg.reserved = false
|
132
|
+
@received.broadcast
|
133
|
+
end
|
134
|
+
|
135
|
+
private def commit(msg, consume)
|
136
|
+
@mutex.synchronize do
|
137
|
+
if consume
|
138
|
+
consume!(msg)
|
139
|
+
else
|
140
|
+
reject!(msg)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private def consume_on_reentry(msg)
|
146
|
+
q_map = current_filtered_queues
|
147
|
+
if (outer_msg = q_map[self])
|
148
|
+
commit(outer_msg, reenter)
|
149
|
+
end
|
150
|
+
q_map[self] = msg
|
151
|
+
begin
|
152
|
+
yield
|
153
|
+
ensure
|
154
|
+
reentered = !q_map.delete(self)
|
155
|
+
end
|
156
|
+
reentered
|
157
|
+
end
|
158
|
+
|
159
|
+
private def reentrant?
|
160
|
+
!!current_filtered_queues[self]
|
161
|
+
end
|
162
|
+
|
163
|
+
# @returns Hash { FilteredQueue => Message }
|
164
|
+
private def current_filtered_queues
|
165
|
+
t = Thread.current
|
166
|
+
t.thread_variable_get(:backports_currently_filtered_queues) or
|
167
|
+
t.thread_variable_set(:backports_currently_filtered_queues, {}.compare_by_identity)
|
168
|
+
end
|
169
|
+
|
170
|
+
# private methods assume @mutex synchonized
|
171
|
+
# adds to exclude list
|
172
|
+
private def acquire!(timeout_time, exclude = nil)
|
173
|
+
while true do # rubocop:disable Style/InfiniteLoop, Style/WhileUntilDo
|
174
|
+
if (msg = available!(exclude))
|
175
|
+
msg.reserved = true
|
176
|
+
exclude << msg if exclude
|
177
|
+
return msg
|
178
|
+
end
|
179
|
+
return closed_queue_value if @closed
|
180
|
+
# wait for element or timeout
|
181
|
+
if timeout_time
|
182
|
+
remaining_time = timeout_time - ::Time.now.to_f
|
183
|
+
return timeout_value if remaining_time <= 0
|
184
|
+
end
|
185
|
+
begin
|
186
|
+
@num_waiting += 1
|
187
|
+
@received.wait(@mutex, remaining_time)
|
188
|
+
ensure
|
189
|
+
@num_waiting -= 1
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private def available!(exclude = nil)
|
195
|
+
@queue.find do |msg|
|
196
|
+
next if exclude && exclude.include?(msg)
|
197
|
+
!msg.reserved
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
private def ensure_open
|
202
|
+
raise self.class::ClosedQueueError, 'queue closed' if @closed
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# shareable_constant_value: literal
|
2
|
+
|
3
|
+
require_relative 'filtered_queue'
|
4
|
+
|
5
|
+
module Backports
|
6
|
+
class Ractor
|
7
|
+
# Standard ::Queue but raises if popping and closed
|
8
|
+
class BaseQueue < FilteredQueue
|
9
|
+
ClosedQueueError = Ractor::ClosedError
|
10
|
+
|
11
|
+
# yields message (if any)
|
12
|
+
def pop_non_blocking
|
13
|
+
yield pop(timeout: 0)
|
14
|
+
rescue TimeoutError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class IncomingQueue < BaseQueue
|
20
|
+
TYPE = :incoming
|
21
|
+
|
22
|
+
protected def reenter
|
23
|
+
raise Ractor::Error, 'Can not reenter'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# * Wraps exception
|
28
|
+
# * Add `ack: ` to push (blocking)
|
29
|
+
class OutgoingQueue < BaseQueue
|
30
|
+
TYPE = :outgoing
|
31
|
+
|
32
|
+
WrappedException = ::Struct.new(:exception, :ractor)
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@ack_queue = ::Queue.new
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def pop(timeout: nil, ack: true)
|
40
|
+
r = super(timeout: timeout)
|
41
|
+
@ack_queue << :done if ack
|
42
|
+
raise r.exception if WrappedException === r
|
43
|
+
|
44
|
+
r
|
45
|
+
end
|
46
|
+
|
47
|
+
def close(how = :hard)
|
48
|
+
super()
|
49
|
+
return if how == :soft
|
50
|
+
|
51
|
+
clear
|
52
|
+
@ack_queue.close
|
53
|
+
end
|
54
|
+
|
55
|
+
def push(obj, ack:)
|
56
|
+
super(obj)
|
57
|
+
if ack
|
58
|
+
r = @ack_queue.pop # block until popped
|
59
|
+
raise ClosedError, "The #{self.class::TYPE}-port is already closed" unless r == :done
|
60
|
+
end
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
private_constant :BaseQueue, :OutgoingQueue, :IncomingQueue
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# shareable_constant_value: literal
|
2
|
+
|
3
|
+
# Ruby 2.0+ backport of `Ractor` class
|
4
|
+
# Extra private methods and instance variables all start with `ractor_`
|
5
|
+
module Backports
|
6
|
+
class Ractor
|
7
|
+
require_relative '../tools/arguments'
|
8
|
+
|
9
|
+
require_relative 'cloner'
|
10
|
+
require_relative 'errors'
|
11
|
+
require_relative 'queues'
|
12
|
+
require_relative 'sharing'
|
13
|
+
|
14
|
+
RactorThreadGroups = ::ObjectSpace::WeakMap.new # ThreadGroup => Ractor
|
15
|
+
private_constant :RactorThreadGroups
|
16
|
+
# Implementation notes
|
17
|
+
#
|
18
|
+
# Uses one `Thread` for each `Ractor`, as well as queues for communication
|
19
|
+
#
|
20
|
+
# The incoming queue is strict: contrary to standard queue, you can't pop from an empty closed queue.
|
21
|
+
# Since standard queues return `nil` is those conditions, we wrap/unwrap `nil` values and consider
|
22
|
+
# all `nil` values to be results of closed queues. `ClosedQueueError` are re-raised as `Ractor::ClosedError`
|
23
|
+
#
|
24
|
+
# The outgoing queue is strict and blocking. Same wrapping / raising as incoming,
|
25
|
+
# with an extra queue to acknowledge when a value has been read (or if the port is closed while waiting).
|
26
|
+
#
|
27
|
+
# The last result is a bit tricky as it needs to be pushed on the outgoing queue but can not be blocking.
|
28
|
+
# For this, we "soft close" the outgoing port.
|
29
|
+
|
30
|
+
def initialize(*args, &block)
|
31
|
+
@ractor_incoming_queue = IncomingQueue.new
|
32
|
+
@ractor_outgoing_queue = OutgoingQueue.new
|
33
|
+
raise ::ArgumentError, 'must be called with a block' unless block
|
34
|
+
|
35
|
+
kw = args.last
|
36
|
+
if kw.is_a?(::Hash) && kw.size == 1 && kw.key?(:name)
|
37
|
+
args.pop
|
38
|
+
name = kw[:name]
|
39
|
+
end
|
40
|
+
@ractor_name = name && Backports.coerce_to_str(name)
|
41
|
+
|
42
|
+
@id = Ractor.ractor_next_id
|
43
|
+
if Ractor.main == nil # then initializing main Ractor
|
44
|
+
@ractor_thread = ::Thread.current
|
45
|
+
@ractor_origin = nil
|
46
|
+
@ractor_thread.thread_variable_set(:backports_ractor, self)
|
47
|
+
else
|
48
|
+
@ractor_origin = caller(1, 1).first.split(':in `').first
|
49
|
+
|
50
|
+
args.map! { |a| Ractor.ractor_isolate(a, false) }
|
51
|
+
ractor_thread_start(args, block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private def ractor_thread_start(args, block)
|
56
|
+
::Thread.new do
|
57
|
+
@ractor_thread = ::Thread.current
|
58
|
+
@ractor_thread_group = ::ThreadGroup.new
|
59
|
+
RactorThreadGroups[@ractor_thread_group] = self
|
60
|
+
@ractor_thread_group.add(@ractor_thread)
|
61
|
+
::Thread.current.thread_variable_set(:backports_ractor, self)
|
62
|
+
result = nil
|
63
|
+
begin
|
64
|
+
result = instance_exec(*args, &block)
|
65
|
+
rescue ::Exception => err # rubocop:disable Lint/RescueException
|
66
|
+
begin
|
67
|
+
raise RemoteError, "thrown by remote Ractor: #{err.message}"
|
68
|
+
rescue RemoteError => e # Hack to create exception with `cause`
|
69
|
+
result = OutgoingQueue::WrappedException.new(e)
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
ractor_thread_terminate(result)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private def ractor_thread_terminate(result)
|
78
|
+
begin
|
79
|
+
ractor_outgoing_queue.push(result, ack: false) unless ractor_outgoing_queue.closed?
|
80
|
+
rescue ::ClosedQueueError
|
81
|
+
return # ignore
|
82
|
+
end
|
83
|
+
ractor_incoming_queue.close
|
84
|
+
ractor_outgoing_queue.close(:soft)
|
85
|
+
ensure
|
86
|
+
# TODO: synchronize?
|
87
|
+
@ractor_thread_group.list.each do |thread|
|
88
|
+
thread.kill unless thread == Thread.current
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def send(obj, move: false)
|
93
|
+
ractor_incoming_queue << Ractor.ractor_isolate(obj, move)
|
94
|
+
self
|
95
|
+
rescue ::ClosedQueueError
|
96
|
+
raise ClosedError, 'The incoming-port is already closed'
|
97
|
+
end
|
98
|
+
alias_method :<<, :send
|
99
|
+
|
100
|
+
def take
|
101
|
+
ractor_outgoing_queue.pop(ack: true)
|
102
|
+
end
|
103
|
+
|
104
|
+
def name
|
105
|
+
@ractor_name
|
106
|
+
end
|
107
|
+
|
108
|
+
RACTOR_STATE = {
|
109
|
+
'sleep' => 'blocking',
|
110
|
+
'run' => 'running',
|
111
|
+
'aborting' => 'aborting',
|
112
|
+
false => 'terminated',
|
113
|
+
nil => 'terminated',
|
114
|
+
}.freeze
|
115
|
+
private_constant :RACTOR_STATE
|
116
|
+
|
117
|
+
def inspect
|
118
|
+
state = RACTOR_STATE[@ractor_thread ? @ractor_thread.status : 'run']
|
119
|
+
info = [
|
120
|
+
"Ractor:##{@id}",
|
121
|
+
name,
|
122
|
+
@ractor_origin,
|
123
|
+
state,
|
124
|
+
].compact.join(' ')
|
125
|
+
|
126
|
+
"#<#{info}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
def close_incoming
|
130
|
+
r = ractor_incoming_queue.closed?
|
131
|
+
ractor_incoming_queue.close
|
132
|
+
r
|
133
|
+
end
|
134
|
+
|
135
|
+
def close_outgoing
|
136
|
+
r = ractor_outgoing_queue.closed?
|
137
|
+
ractor_outgoing_queue.close
|
138
|
+
r
|
139
|
+
end
|
140
|
+
|
141
|
+
private def receive
|
142
|
+
ractor_incoming_queue.pop
|
143
|
+
end
|
144
|
+
|
145
|
+
private def receive_if(&block)
|
146
|
+
raise ::ArgumentError, 'no block given' unless block
|
147
|
+
ractor_incoming_queue.pop(&block)
|
148
|
+
end
|
149
|
+
|
150
|
+
def [](key)
|
151
|
+
Ractor.current.ractor_locals[key]
|
152
|
+
end
|
153
|
+
|
154
|
+
def []=(key, value)
|
155
|
+
Ractor.current.ractor_locals[key] = value
|
156
|
+
end
|
157
|
+
|
158
|
+
# @api private
|
159
|
+
def ractor_locals
|
160
|
+
@ractor_locals ||= {}.compare_by_identity
|
161
|
+
end
|
162
|
+
|
163
|
+
class << self
|
164
|
+
def yield(value, move: false)
|
165
|
+
value = ractor_isolate(value, move)
|
166
|
+
current.ractor_outgoing_queue.push(value, ack: true)
|
167
|
+
rescue ::ClosedQueueError
|
168
|
+
raise ClosedError, 'The outgoing-port is already closed'
|
169
|
+
end
|
170
|
+
|
171
|
+
def receive
|
172
|
+
current.__send__(:receive)
|
173
|
+
end
|
174
|
+
alias_method :recv, :receive
|
175
|
+
|
176
|
+
def receive_if(&block)
|
177
|
+
current.__send__(:receive_if, &block)
|
178
|
+
end
|
179
|
+
|
180
|
+
def select(*ractors, yield_value: not_given = true, move: false)
|
181
|
+
cur = Ractor.current
|
182
|
+
queues = ractors.map do |r|
|
183
|
+
r == cur ? r.ractor_incoming_queue : r.ractor_outgoing_queue
|
184
|
+
end
|
185
|
+
if !not_given
|
186
|
+
out = current.ractor_outgoing_queue
|
187
|
+
yield_value = ractor_isolate(yield_value, move)
|
188
|
+
elsif ractors.empty?
|
189
|
+
raise ::ArgumentError, 'specify at least one ractor or `yield_value`'
|
190
|
+
end
|
191
|
+
|
192
|
+
while true # rubocop:disable Style/InfiniteLoop
|
193
|
+
# Don't `loop`, in case of `ClosedError` (not that there should be any)
|
194
|
+
queues.each_with_index do |q, i|
|
195
|
+
q.pop_non_blocking do |val|
|
196
|
+
r = ractors[i]
|
197
|
+
return [r == cur ? :receive : r, val]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
if out && out.num_waiting > 0
|
202
|
+
# Not quite atomic...
|
203
|
+
out.push(yield_value, ack: true)
|
204
|
+
return [:yield, nil]
|
205
|
+
end
|
206
|
+
|
207
|
+
sleep(0.001)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def make_shareable(obj)
|
212
|
+
return obj if ractor_check_shareability?(obj, true)
|
213
|
+
|
214
|
+
raise Ractor::Error, '#freeze does not freeze object correctly'
|
215
|
+
end
|
216
|
+
|
217
|
+
def shareable?(obj)
|
218
|
+
ractor_check_shareability?(obj, false)
|
219
|
+
end
|
220
|
+
|
221
|
+
def current
|
222
|
+
::Thread.current.thread_variable_get(:backports_ractor) ||
|
223
|
+
::Thread.current.thread_variable_set(:backports_ractor, ractor_find_current)
|
224
|
+
end
|
225
|
+
|
226
|
+
def count
|
227
|
+
::ObjectSpace.each_object(Ractor).count(&:ractor_live?)
|
228
|
+
end
|
229
|
+
|
230
|
+
# @api private
|
231
|
+
def ractor_reset
|
232
|
+
::ObjectSpace.each_object(Ractor).each do |r|
|
233
|
+
next if r == Ractor.current
|
234
|
+
next unless (th = r.ractor_thread)
|
235
|
+
|
236
|
+
th.kill
|
237
|
+
th.join
|
238
|
+
end
|
239
|
+
Ractor.current.ractor_incoming_queue.clear
|
240
|
+
end
|
241
|
+
|
242
|
+
# @api private
|
243
|
+
def ractor_next_id
|
244
|
+
@id ||= 0
|
245
|
+
@id += 1
|
246
|
+
end
|
247
|
+
|
248
|
+
attr_reader :main
|
249
|
+
|
250
|
+
private def ractor_init
|
251
|
+
@ractor_shareable = ::ObjectSpace::WeakMap.new
|
252
|
+
@main = Ractor.new { nil }
|
253
|
+
RactorThreadGroups[::ThreadGroup::Default] = @main
|
254
|
+
end
|
255
|
+
|
256
|
+
private def ractor_find_current
|
257
|
+
RactorThreadGroups[Thread.current.group]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# @api private
|
262
|
+
def ractor_live?
|
263
|
+
!defined?(@ractor_thread) || # May happen if `count` is called from another thread before `initialize` has completed
|
264
|
+
@ractor_thread.status
|
265
|
+
end
|
266
|
+
|
267
|
+
# @api private
|
268
|
+
attr_reader :ractor_outgoing_queue, :ractor_incoming_queue, :ractor_thread
|
269
|
+
|
270
|
+
ractor_init
|
271
|
+
end
|
272
|
+
end
|