backports 3.18.2 → 3.21.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 +36 -2
- data/Gemfile +2 -2
- data/README.md +35 -14
- data/lib/backports/2.0.0.rb +1 -1
- data/lib/backports/2.1.0.rb +1 -1
- data/lib/backports/2.2.0.rb +1 -1
- data/lib/backports/2.2.0/string/unicode_normalize.rb +3 -3
- data/lib/backports/2.3.0.rb +1 -1
- data/lib/backports/2.3.0/queue/close.rb +48 -0
- data/lib/backports/2.4.0.rb +1 -1
- 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.4.0/string/unpack1.rb +7 -0
- data/lib/backports/2.5.0.rb +1 -1
- 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.rb +1 -1
- data/lib/backports/2.6.0.rb +2 -2
- 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 +2 -2
- 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 +19 -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 +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 +23 -3
@@ -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,19 @@
|
|
1
|
+
if RUBY_VERSION < '2'
|
2
|
+
warn 'Ractor not backported to Ruby 1.x'
|
3
|
+
elsif defined?(Ractor.current)
|
4
|
+
# all good
|
5
|
+
else
|
6
|
+
# Cloner:
|
7
|
+
require_relative '../2.4.0/hash/transform_values'
|
8
|
+
require_relative '../2.5.0/hash/transform_keys'
|
9
|
+
# Queues & FilteredQueue
|
10
|
+
require_relative '../2.3.0/queue/close'
|
11
|
+
|
12
|
+
class Ractor
|
13
|
+
end
|
14
|
+
|
15
|
+
module Backports
|
16
|
+
Ractor = ::Ractor
|
17
|
+
end
|
18
|
+
require_relative '../ractor/ractor'
|
19
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'backports/3.0.0'
|
@@ -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
|