backports 3.17.2 → 3.20.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 +52 -1
- data/Gemfile +3 -16
- data/README.md +31 -11
- data/backports.gemspec +1 -1
- data/lib/backports/1.8.7.rb +5 -4
- data/lib/backports/1.9.1.rb +6 -1
- data/lib/backports/1.9.2.rb +6 -1
- data/lib/backports/1.9.3.rb +6 -1
- data/lib/backports/2.0.0.rb +7 -2
- data/lib/backports/2.1.0.rb +2 -2
- data/lib/backports/2.2.0.rb +2 -2
- data/lib/backports/2.2.0/string/unicode_normalize.rb +3 -3
- data/lib/backports/2.3.0.rb +2 -2
- 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.4.0.rb +2 -2
- data/lib/backports/2.4.0/bignum.rb +3 -0
- data/lib/backports/2.4.0/bignum/dup.rb +5 -0
- data/lib/backports/2.5.0.rb +2 -2
- data/lib/backports/2.5.0/hash/transform_keys.rb +10 -3
- data/lib/backports/2.5.0/integer/sqrt.rb +1 -1
- data/lib/backports/2.5.0/string/undump.rb +2 -2
- data/lib/backports/2.5.rb +1 -1
- data/lib/backports/2.6.0.rb +3 -3
- data/lib/backports/2.6.0/enumerable/chain.rb +2 -0
- data/lib/backports/2.6.rb +1 -1
- data/lib/backports/2.7.0.rb +3 -3
- data/lib/backports/3.0.0.rb +3 -0
- data/lib/backports/3.0.0/env.rb +3 -0
- data/lib/backports/3.0.0/env/except.rb +10 -0
- data/lib/backports/3.0.0/hash.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/ractor.rb +5 -0
- data/lib/backports/3.0.0/symbol.rb +3 -0
- data/lib/backports/3.0.0/symbol/name.rb +11 -0
- data/lib/backports/3.0.rb +1 -0
- data/lib/backports/ractor/cloner.rb +91 -0
- data/lib/backports/ractor/errors.rb +16 -0
- data/lib/backports/ractor/queues.rb +62 -0
- data/lib/backports/ractor/ractor.rb +238 -0
- data/lib/backports/ractor/sharing.rb +93 -0
- data/lib/backports/tools/filtered_queue.rb +202 -0
- data/lib/backports/tools/require_relative_dir.rb +6 -1
- data/lib/backports/version.rb +1 -1
- metadata +27 -7
data/lib/backports/2.6.0.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
# require this file to load all the backports up to Ruby 2.
|
2
|
-
require 'backports/2.5'
|
3
|
-
Backports.require_relative_dir
|
1
|
+
# require this file to load all the backports up to Ruby 2.6
|
2
|
+
require 'backports/2.5.0'
|
3
|
+
Backports.require_relative_dir if RUBY_VERSION < '2.6'
|
@@ -8,6 +8,7 @@ unless Enumerable.method_defined? :chain
|
|
8
8
|
Enumerator = Enumerable::Enumerator unless Object.const_defined? :Enumerator # For 1.8.x
|
9
9
|
|
10
10
|
class Enumerator::Chain < Enumerator
|
11
|
+
# rubocop:disable Lint/MissingSuper
|
11
12
|
def initialize(*enums)
|
12
13
|
@enums = enums
|
13
14
|
@rewindable = -1
|
@@ -16,6 +17,7 @@ unless Enumerable.method_defined? :chain
|
|
16
17
|
# ...it checks what call of #initialize on non-initalized object returns
|
17
18
|
self # rubocop:disable Lint/Void
|
18
19
|
end
|
20
|
+
# rubocop:enable Lint/MissingSuper
|
19
21
|
|
20
22
|
def each(*args, &block)
|
21
23
|
@enums.each_with_index do |enum, i|
|
data/lib/backports/2.6.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
# require this file to load all the backports of Ruby 2.
|
1
|
+
# require this file to load all the backports of Ruby 2.6 and below
|
2
2
|
require 'backports/2.6.0'
|
data/lib/backports/2.7.0.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
# require this file to load all the backports up to Ruby 2.
|
2
|
-
require 'backports/2.6'
|
3
|
-
Backports.require_relative_dir
|
1
|
+
# require this file to load all the backports up to Ruby 2.7
|
2
|
+
require 'backports/2.6.0'
|
3
|
+
Backports.require_relative_dir if RUBY_VERSION < '2.7'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Hash
|
2
|
+
unless ({}.transform_keys(:x => 1) rescue false)
|
3
|
+
require 'backports/2.5.0/hash/transform_keys'
|
4
|
+
require 'backports/tools/alias_method_chain'
|
5
|
+
|
6
|
+
def transform_keys_with_hash_arg(hash = not_given = true, &block)
|
7
|
+
return to_enum(:transform_keys) { size } if not_given && !block
|
8
|
+
|
9
|
+
return transform_keys_without_hash_arg(&block) if not_given
|
10
|
+
|
11
|
+
h = {}
|
12
|
+
if block_given?
|
13
|
+
each do |key, value|
|
14
|
+
h[hash.fetch(key) { yield key }] = value
|
15
|
+
end
|
16
|
+
else
|
17
|
+
each do |key, value|
|
18
|
+
h[hash.fetch(key, key)] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
h
|
22
|
+
end
|
23
|
+
Backports.alias_method_chain self, :transform_keys, :hash_arg
|
24
|
+
|
25
|
+
def transform_keys_with_hash_arg!(hash = not_given = true, &block)
|
26
|
+
return enum_for(:transform_keys!) { size } if not_given && !block
|
27
|
+
|
28
|
+
return transform_keys_without_hash_arg!(&block) if not_given
|
29
|
+
|
30
|
+
h = {}
|
31
|
+
begin
|
32
|
+
if block_given?
|
33
|
+
each do |key, value|
|
34
|
+
h[hash.fetch(key) { yield key }] = value
|
35
|
+
end
|
36
|
+
else
|
37
|
+
each do |key, value|
|
38
|
+
h[hash.fetch(key, key)] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
replace(h)
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
Backports.alias_method_chain self, :transform_keys!, :hash_arg
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'backports/3.0.0'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative '../2.4.0/hash/transform_values'
|
2
|
+
require_relative '../2.5.0/hash/transform_keys'
|
3
|
+
|
4
|
+
class Ractor
|
5
|
+
module Cloner
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def deep_clone(obj)
|
9
|
+
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
|
10
|
+
|
11
|
+
@processed = {}.compare_by_identity
|
12
|
+
@changed = nil
|
13
|
+
result = process(obj) do |r|
|
14
|
+
copy_contents(r)
|
15
|
+
end
|
16
|
+
return result if result
|
17
|
+
|
18
|
+
Ractor.ractor_mark_set_shareable(@processed)
|
19
|
+
obj
|
20
|
+
end
|
21
|
+
|
22
|
+
# Yields a deep copy.
|
23
|
+
# If no deep copy is needed, `obj` is returned and
|
24
|
+
# nothing is yielded
|
25
|
+
private def clone_deeper(obj)
|
26
|
+
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
|
27
|
+
|
28
|
+
result = process(obj) do |r|
|
29
|
+
copy_contents(r)
|
30
|
+
end
|
31
|
+
return obj unless result
|
32
|
+
|
33
|
+
yield result if block_given?
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Yields if `obj` is a new structure
|
38
|
+
# Returns the deep copy, or `false` if no deep copy is needed
|
39
|
+
private def process(obj)
|
40
|
+
@processed.fetch(obj) do
|
41
|
+
# For recursive structures, assume that we'll need a duplicate.
|
42
|
+
# If that's not the case, we will have duplicated the whole structure
|
43
|
+
# for nothing...
|
44
|
+
@processed[obj] = result = obj.dup
|
45
|
+
changed = track_change { yield result }
|
46
|
+
return false if obj.frozen? && !changed
|
47
|
+
|
48
|
+
@changed = true
|
49
|
+
result.freeze if obj.frozen?
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns if the block called `deep clone` and that the deep copy was needed
|
56
|
+
private def track_change
|
57
|
+
prev = @changed
|
58
|
+
@changed = false
|
59
|
+
yield
|
60
|
+
@changed
|
61
|
+
ensure
|
62
|
+
@changed = prev
|
63
|
+
end
|
64
|
+
|
65
|
+
# modifies in place `obj` by calling `deep clone` on its contents
|
66
|
+
private def copy_contents(obj)
|
67
|
+
case obj
|
68
|
+
when ::Hash
|
69
|
+
if obj.default
|
70
|
+
clone_deeper(obj.default) do |copy|
|
71
|
+
obj.default = copy
|
72
|
+
end
|
73
|
+
end
|
74
|
+
obj.transform_keys! { |key| clone_deeper(key) }
|
75
|
+
obj.transform_values! { |value| clone_deeper(value) }
|
76
|
+
when ::Array
|
77
|
+
obj.map! { |item| clone_deeper(item) }
|
78
|
+
when ::Struct
|
79
|
+
obj.each_pair do |key, item|
|
80
|
+
clone_deeper(item) { |copy| obj[key] = copy }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
obj.instance_variables.each do |var|
|
84
|
+
clone_deeper(obj.instance_variable_get(var)) do |copy|
|
85
|
+
obj.instance_variable_set(var, copy)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
private_constant :Cloner
|
91
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative '../tools/filtered_queue'
|
2
|
+
|
3
|
+
class Ractor
|
4
|
+
# Standard ::Queue but raises if popping and closed
|
5
|
+
class BaseQueue < ::Backports::FilteredQueue
|
6
|
+
ClosedQueueError = ::Ractor::ClosedError
|
7
|
+
|
8
|
+
# yields message (if any)
|
9
|
+
def pop_non_blocking
|
10
|
+
yield pop(timeout: 0)
|
11
|
+
rescue TimeoutError
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class IncomingQueue < BaseQueue
|
17
|
+
TYPE = :incoming
|
18
|
+
|
19
|
+
protected def reenter
|
20
|
+
raise ::Ractor::Error, 'Can not reenter'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# * Wraps exception
|
25
|
+
# * Add `ack: ` to push (blocking)
|
26
|
+
class OutgoingQueue < BaseQueue
|
27
|
+
TYPE = :outgoing
|
28
|
+
|
29
|
+
WrappedException = ::Struct.new(:exception, :ractor)
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@ack_queue = ::Queue.new
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def pop(timeout: nil, ack: true)
|
37
|
+
r = super(timeout: timeout)
|
38
|
+
@ack_queue << :done if ack
|
39
|
+
raise r.exception if WrappedException === r
|
40
|
+
|
41
|
+
r
|
42
|
+
end
|
43
|
+
|
44
|
+
def close(how = :hard)
|
45
|
+
super()
|
46
|
+
return if how == :soft
|
47
|
+
|
48
|
+
clear
|
49
|
+
@ack_queue.close
|
50
|
+
end
|
51
|
+
|
52
|
+
def push(obj, ack:)
|
53
|
+
super(obj)
|
54
|
+
if ack
|
55
|
+
r = @ack_queue.pop # block until popped
|
56
|
+
raise ClosedError, "The #{self.class::TYPE}-port is already closed" unless r == :done
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
private_constant :BaseQueue, :OutgoingQueue, :IncomingQueue
|
62
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# Ruby 2.0+ backport of `Ractor` class
|
2
|
+
# Extra private methods and instance variables all start with `ractor_`
|
3
|
+
class Ractor
|
4
|
+
require_relative '../tools/arguments'
|
5
|
+
|
6
|
+
require_relative 'cloner'
|
7
|
+
require_relative 'errors'
|
8
|
+
require_relative 'queues'
|
9
|
+
require_relative 'sharing'
|
10
|
+
|
11
|
+
# Implementation notes
|
12
|
+
#
|
13
|
+
# Uses one `Thread` for each `Ractor`, as well as queues for communication
|
14
|
+
#
|
15
|
+
# The incoming queue is strict: contrary to standard queue, you can't pop from an empty closed queue.
|
16
|
+
# Since standard queues return `nil` is those conditions, we wrap/unwrap `nil` values and consider
|
17
|
+
# all `nil` values to be results of closed queues. `ClosedQueueError` are re-raised as `Ractor::ClosedError`
|
18
|
+
#
|
19
|
+
# The outgoing queue is strict and blocking. Same wrapping / raising as incoming,
|
20
|
+
# with an extra queue to acknowledge when a value has been read (or if the port is closed while waiting).
|
21
|
+
#
|
22
|
+
# The last result is a bit tricky as it needs to be pushed on the outgoing queue but can not be blocking.
|
23
|
+
# For this, we "soft close" the outgoing port.
|
24
|
+
|
25
|
+
def initialize(*args, &block)
|
26
|
+
@ractor_incoming_queue = IncomingQueue.new
|
27
|
+
@ractor_outgoing_queue = OutgoingQueue.new
|
28
|
+
raise ArgumentError, 'must be called with a block' unless block
|
29
|
+
|
30
|
+
kw = args.last
|
31
|
+
if kw.is_a?(Hash) && kw.size == 1 && kw.key?(:name)
|
32
|
+
args.pop
|
33
|
+
name = kw[:name]
|
34
|
+
end
|
35
|
+
@ractor_name = name && Backports.coerce_to_str(name)
|
36
|
+
|
37
|
+
if Ractor.main == nil # then initializing main Ractor
|
38
|
+
@ractor_thread = ::Thread.current
|
39
|
+
@ractor_origin = nil
|
40
|
+
@ractor_thread.thread_variable_set(:ractor, self)
|
41
|
+
else
|
42
|
+
@ractor_origin = caller(1, 1).first.split(':in `').first
|
43
|
+
|
44
|
+
args.map! { |a| Ractor.ractor_isolate(a, false) }
|
45
|
+
ractor_thread_start(args, block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private def ractor_thread_start(args, block)
|
50
|
+
Thread.new do
|
51
|
+
@ractor_thread = Thread.current
|
52
|
+
@ractor_thread_group = ThreadGroup.new.add(@ractor_thread)
|
53
|
+
::Thread.current.thread_variable_set(:ractor, self)
|
54
|
+
result = nil
|
55
|
+
begin
|
56
|
+
result = instance_exec(*args, &block)
|
57
|
+
rescue ::Exception => err # rubocop:disable Lint/RescueException
|
58
|
+
begin
|
59
|
+
raise RemoteError, "thrown by remote Ractor: #{err.message}"
|
60
|
+
rescue RemoteError => e # Hack to create exception with `cause`
|
61
|
+
result = OutgoingQueue::WrappedException.new(e)
|
62
|
+
end
|
63
|
+
ensure
|
64
|
+
ractor_thread_terminate(result)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private def ractor_thread_terminate(result)
|
70
|
+
begin
|
71
|
+
ractor_outgoing_queue.push(result, ack: false) unless ractor_outgoing_queue.closed?
|
72
|
+
rescue ClosedQueueError
|
73
|
+
return # ignore
|
74
|
+
end
|
75
|
+
ractor_incoming_queue.close
|
76
|
+
ractor_outgoing_queue.close(:soft)
|
77
|
+
ensure
|
78
|
+
# TODO: synchronize?
|
79
|
+
@ractor_thread_group.list.each do |thread|
|
80
|
+
thread.kill unless thread == Thread.current
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def send(obj, move: false)
|
85
|
+
ractor_incoming_queue << Ractor.ractor_isolate(obj, move)
|
86
|
+
self
|
87
|
+
rescue ::ClosedQueueError
|
88
|
+
raise ClosedError, 'The incoming-port is already closed'
|
89
|
+
end
|
90
|
+
alias_method :<<, :send
|
91
|
+
|
92
|
+
def take
|
93
|
+
ractor_outgoing_queue.pop(ack: true)
|
94
|
+
end
|
95
|
+
|
96
|
+
def name
|
97
|
+
@ractor_name
|
98
|
+
end
|
99
|
+
|
100
|
+
RACTOR_STATE = {
|
101
|
+
'sleep' => 'blocking',
|
102
|
+
'run' => 'running',
|
103
|
+
'aborting' => 'aborting',
|
104
|
+
false => 'terminated',
|
105
|
+
nil => 'terminated',
|
106
|
+
}.freeze
|
107
|
+
private_constant :RACTOR_STATE
|
108
|
+
|
109
|
+
def inspect
|
110
|
+
state = RACTOR_STATE[@ractor_thread ? @ractor_thread.status : 'run']
|
111
|
+
info = [
|
112
|
+
'Ractor:#1',
|
113
|
+
name,
|
114
|
+
@ractor_origin,
|
115
|
+
state
|
116
|
+
].compact.join(' ')
|
117
|
+
|
118
|
+
"#<#{info}>"
|
119
|
+
end
|
120
|
+
|
121
|
+
def close_incoming
|
122
|
+
r = ractor_incoming_queue.closed?
|
123
|
+
ractor_incoming_queue.close
|
124
|
+
r
|
125
|
+
end
|
126
|
+
|
127
|
+
def close_outgoing
|
128
|
+
r = ractor_outgoing_queue.closed?
|
129
|
+
ractor_outgoing_queue.close
|
130
|
+
r
|
131
|
+
end
|
132
|
+
|
133
|
+
private def receive
|
134
|
+
ractor_incoming_queue.pop
|
135
|
+
end
|
136
|
+
|
137
|
+
private def receive_if(&block)
|
138
|
+
raise ArgumentError, 'no block given' unless block
|
139
|
+
ractor_incoming_queue.pop(&block)
|
140
|
+
end
|
141
|
+
|
142
|
+
class << self
|
143
|
+
def yield(value, move: false)
|
144
|
+
value = ractor_isolate(value, move)
|
145
|
+
current.ractor_outgoing_queue.push(value, ack: true)
|
146
|
+
rescue ClosedQueueError
|
147
|
+
raise ClosedError, 'The outgoing-port is already closed'
|
148
|
+
end
|
149
|
+
|
150
|
+
def receive
|
151
|
+
current.__send__(:receive)
|
152
|
+
end
|
153
|
+
alias_method :recv, :receive
|
154
|
+
|
155
|
+
def receive_if(&block)
|
156
|
+
current.__send__(:receive_if, &block)
|
157
|
+
end
|
158
|
+
|
159
|
+
def select(*ractors, yield_value: not_given = true, move: false)
|
160
|
+
cur = Ractor.current
|
161
|
+
queues = ractors.map do |r|
|
162
|
+
r == cur ? r.ractor_incoming_queue : r.ractor_outgoing_queue
|
163
|
+
end
|
164
|
+
if !not_given
|
165
|
+
out = current.ractor_outgoing_queue
|
166
|
+
yield_value = ractor_isolate(yield_value, move)
|
167
|
+
elsif ractors.empty?
|
168
|
+
raise ArgumentError, 'specify at least one ractor or `yield_value`'
|
169
|
+
end
|
170
|
+
|
171
|
+
while true # rubocop:disable Style/InfiniteLoop
|
172
|
+
# Don't `loop`, in case of `ClosedError` (not that there should be any)
|
173
|
+
queues.each_with_index do |q, i|
|
174
|
+
q.pop_non_blocking do |val|
|
175
|
+
r = ractors[i]
|
176
|
+
return [r == cur ? :receive : r, val]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
if out && out.num_waiting > 0
|
181
|
+
# Not quite atomic...
|
182
|
+
out.push(yield_value, ack: true)
|
183
|
+
return [:yield, nil]
|
184
|
+
end
|
185
|
+
|
186
|
+
sleep(0.001)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def make_shareable(obj)
|
191
|
+
return obj if ractor_check_shareability?(obj, true)
|
192
|
+
|
193
|
+
raise Ractor::Error, '#freeze does not freeze object correctly'
|
194
|
+
end
|
195
|
+
|
196
|
+
def shareable?(obj)
|
197
|
+
ractor_check_shareability?(obj, false)
|
198
|
+
end
|
199
|
+
|
200
|
+
def current
|
201
|
+
Thread.current.thread_variable_get(:ractor)
|
202
|
+
end
|
203
|
+
|
204
|
+
def count
|
205
|
+
ObjectSpace.each_object(Ractor).count(&:ractor_live?)
|
206
|
+
end
|
207
|
+
|
208
|
+
# @api private
|
209
|
+
def ractor_reset
|
210
|
+
ObjectSpace.each_object(Ractor).each do |r|
|
211
|
+
next if r == Ractor.current
|
212
|
+
next unless (th = r.ractor_thread)
|
213
|
+
|
214
|
+
th.kill
|
215
|
+
th.join
|
216
|
+
end
|
217
|
+
Ractor.current.ractor_incoming_queue.clear
|
218
|
+
end
|
219
|
+
|
220
|
+
attr_reader :main
|
221
|
+
|
222
|
+
private def ractor_init
|
223
|
+
@ractor_shareable = ::ObjectSpace::WeakMap.new
|
224
|
+
@main = Ractor.new { nil }
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# @api private
|
229
|
+
def ractor_live?
|
230
|
+
!defined?(@ractor_thread) || # May happen if `count` is called from another thread before `initialize` has completed
|
231
|
+
@ractor_thread.status
|
232
|
+
end
|
233
|
+
|
234
|
+
# @api private
|
235
|
+
attr_reader :ractor_outgoing_queue, :ractor_incoming_queue, :ractor_thread
|
236
|
+
|
237
|
+
ractor_init
|
238
|
+
end
|