backports 3.18.0 → 3.20.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -1
  3. data/Gemfile +3 -16
  4. data/README.md +31 -11
  5. data/backports.gemspec +1 -1
  6. data/lib/backports/2.0.0.rb +1 -1
  7. data/lib/backports/2.1.0.rb +1 -1
  8. data/lib/backports/2.2.0.rb +1 -1
  9. data/lib/backports/2.2.0/string/unicode_normalize.rb +3 -3
  10. data/lib/backports/2.3.0.rb +1 -1
  11. data/lib/backports/2.3.0/queue/close.rb +48 -0
  12. data/lib/backports/2.3.0/string.rb +3 -0
  13. data/lib/backports/2.4.0.rb +1 -1
  14. data/lib/backports/2.4.0/bignum.rb +3 -0
  15. data/lib/backports/2.4.0/bignum/dup.rb +5 -0
  16. data/lib/backports/2.5.0.rb +1 -1
  17. data/lib/backports/2.5.0/hash/transform_keys.rb +10 -3
  18. data/lib/backports/2.5.0/integer/sqrt.rb +1 -1
  19. data/lib/backports/2.5.0/string/undump.rb +2 -2
  20. data/lib/backports/2.5.rb +1 -1
  21. data/lib/backports/2.6.0.rb +2 -2
  22. data/lib/backports/2.6.0/enumerable/chain.rb +2 -0
  23. data/lib/backports/2.6.rb +1 -1
  24. data/lib/backports/2.7.0.rb +2 -2
  25. data/lib/backports/3.0.0.rb +3 -0
  26. data/lib/backports/3.0.0/env.rb +3 -0
  27. data/lib/backports/3.0.0/env/except.rb +10 -0
  28. data/lib/backports/3.0.0/hash.rb +3 -0
  29. data/lib/backports/3.0.0/hash/except.rb +10 -0
  30. data/lib/backports/3.0.0/hash/transform_keys.rb +48 -0
  31. data/lib/backports/3.0.0/ractor.rb +5 -0
  32. data/lib/backports/3.0.0/symbol.rb +3 -0
  33. data/lib/backports/3.0.0/symbol/name.rb +11 -0
  34. data/lib/backports/3.0.rb +1 -0
  35. data/lib/backports/ractor/cloner.rb +91 -0
  36. data/lib/backports/ractor/errors.rb +16 -0
  37. data/lib/backports/ractor/queues.rb +62 -0
  38. data/lib/backports/ractor/ractor.rb +261 -0
  39. data/lib/backports/ractor/sharing.rb +93 -0
  40. data/lib/backports/tools/filtered_queue.rb +202 -0
  41. data/lib/backports/version.rb +1 -1
  42. metadata +27 -7
@@ -0,0 +1,10 @@
1
+ class Hash
2
+ def except(*keys)
3
+ if keys.size > 4 && size > 4 # index if O(m*n) is big
4
+ h = {}
5
+ keys.each { |key| h[key] = true }
6
+ keys = h
7
+ end
8
+ reject { |key, _value| keys.include? key}
9
+ end
10
+ end unless Hash.method_defined? :except
@@ -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,5 @@
1
+ if RUBY_VERSION < '2'
2
+ warn 'Ractor not backported to Ruby 1.x'
3
+ else
4
+ require_relative '../ractor/ractor' unless defined?(Ractor.current)
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'backports/tools/require_relative_dir'
2
+
3
+ Backports.require_relative_dir
@@ -0,0 +1,11 @@
1
+ unless Symbol.method_defined? :name
2
+ def Backports.symbol_names
3
+ @symbol_names ||= ObjectSpace::WeakMap.new
4
+ end
5
+
6
+ class Symbol
7
+ def name
8
+ Backports.symbol_names[self] ||= to_s.freeze
9
+ end
10
+ end
11
+ 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,16 @@
1
+ class Ractor
2
+ class ClosedError < ::StopIteration
3
+ end
4
+
5
+ class Error < ::StandardError
6
+ end
7
+
8
+ class RemoteError < Error
9
+ attr_reader :ractor
10
+
11
+ def initialize(message = nil)
12
+ @ractor = Ractor.current
13
+ super
14
+ end
15
+ end
16
+ 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,261 @@
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
+ RactorThreadGroups = ::ObjectSpace::WeakMap.new # ThreadGroup => Ractor
12
+ private_constant :RactorThreadGroups
13
+ # Implementation notes
14
+ #
15
+ # Uses one `Thread` for each `Ractor`, as well as queues for communication
16
+ #
17
+ # The incoming queue is strict: contrary to standard queue, you can't pop from an empty closed queue.
18
+ # Since standard queues return `nil` is those conditions, we wrap/unwrap `nil` values and consider
19
+ # all `nil` values to be results of closed queues. `ClosedQueueError` are re-raised as `Ractor::ClosedError`
20
+ #
21
+ # The outgoing queue is strict and blocking. Same wrapping / raising as incoming,
22
+ # with an extra queue to acknowledge when a value has been read (or if the port is closed while waiting).
23
+ #
24
+ # The last result is a bit tricky as it needs to be pushed on the outgoing queue but can not be blocking.
25
+ # For this, we "soft close" the outgoing port.
26
+
27
+ def initialize(*args, &block)
28
+ @ractor_incoming_queue = IncomingQueue.new
29
+ @ractor_outgoing_queue = OutgoingQueue.new
30
+ raise ArgumentError, 'must be called with a block' unless block
31
+
32
+ kw = args.last
33
+ if kw.is_a?(Hash) && kw.size == 1 && kw.key?(:name)
34
+ args.pop
35
+ name = kw[:name]
36
+ end
37
+ @ractor_name = name && Backports.coerce_to_str(name)
38
+
39
+ if Ractor.main == nil # then initializing main Ractor
40
+ @ractor_thread = ::Thread.current
41
+ @ractor_origin = nil
42
+ @ractor_thread.thread_variable_set(:backports_ractor, self)
43
+ else
44
+ @ractor_origin = caller(1, 1).first.split(':in `').first
45
+
46
+ args.map! { |a| Ractor.ractor_isolate(a, false) }
47
+ ractor_thread_start(args, block)
48
+ end
49
+ end
50
+
51
+ private def ractor_thread_start(args, block)
52
+ ::Thread.new do
53
+ @ractor_thread = Thread.current
54
+ @ractor_thread_group = ThreadGroup.new
55
+ RactorThreadGroups[@ractor_thread_group] = self
56
+ @ractor_thread_group.add(@ractor_thread)
57
+ ::Thread.current.thread_variable_set(:backports_ractor, self)
58
+ result = nil
59
+ begin
60
+ result = instance_exec(*args, &block)
61
+ rescue ::Exception => err # rubocop:disable Lint/RescueException
62
+ begin
63
+ raise RemoteError, "thrown by remote Ractor: #{err.message}"
64
+ rescue RemoteError => e # Hack to create exception with `cause`
65
+ result = OutgoingQueue::WrappedException.new(e)
66
+ end
67
+ ensure
68
+ ractor_thread_terminate(result)
69
+ end
70
+ end
71
+ end
72
+
73
+ private def ractor_thread_terminate(result)
74
+ begin
75
+ ractor_outgoing_queue.push(result, ack: false) unless ractor_outgoing_queue.closed?
76
+ rescue ClosedQueueError
77
+ return # ignore
78
+ end
79
+ ractor_incoming_queue.close
80
+ ractor_outgoing_queue.close(:soft)
81
+ ensure
82
+ # TODO: synchronize?
83
+ @ractor_thread_group.list.each do |thread|
84
+ thread.kill unless thread == Thread.current
85
+ end
86
+ end
87
+
88
+ def send(obj, move: false)
89
+ ractor_incoming_queue << Ractor.ractor_isolate(obj, move)
90
+ self
91
+ rescue ::ClosedQueueError
92
+ raise ClosedError, 'The incoming-port is already closed'
93
+ end
94
+ alias_method :<<, :send
95
+
96
+ def take
97
+ ractor_outgoing_queue.pop(ack: true)
98
+ end
99
+
100
+ def name
101
+ @ractor_name
102
+ end
103
+
104
+ RACTOR_STATE = {
105
+ 'sleep' => 'blocking',
106
+ 'run' => 'running',
107
+ 'aborting' => 'aborting',
108
+ false => 'terminated',
109
+ nil => 'terminated',
110
+ }.freeze
111
+ private_constant :RACTOR_STATE
112
+
113
+ def inspect
114
+ state = RACTOR_STATE[@ractor_thread ? @ractor_thread.status : 'run']
115
+ info = [
116
+ 'Ractor:#1',
117
+ name,
118
+ @ractor_origin,
119
+ state
120
+ ].compact.join(' ')
121
+
122
+ "#<#{info}>"
123
+ end
124
+
125
+ def close_incoming
126
+ r = ractor_incoming_queue.closed?
127
+ ractor_incoming_queue.close
128
+ r
129
+ end
130
+
131
+ def close_outgoing
132
+ r = ractor_outgoing_queue.closed?
133
+ ractor_outgoing_queue.close
134
+ r
135
+ end
136
+
137
+ private def receive
138
+ ractor_incoming_queue.pop
139
+ end
140
+
141
+ private def receive_if(&block)
142
+ raise ArgumentError, 'no block given' unless block
143
+ ractor_incoming_queue.pop(&block)
144
+ end
145
+
146
+ def [](key)
147
+ Ractor.current.ractor_locals[key]
148
+ end
149
+
150
+ def []=(key, value)
151
+ Ractor.current.ractor_locals[key] = value
152
+ end
153
+
154
+ # @api private
155
+ def ractor_locals
156
+ @ractor_locals ||= {}.compare_by_identity
157
+ end
158
+
159
+ class << self
160
+ def yield(value, move: false)
161
+ value = ractor_isolate(value, move)
162
+ current.ractor_outgoing_queue.push(value, ack: true)
163
+ rescue ClosedQueueError
164
+ raise ClosedError, 'The outgoing-port is already closed'
165
+ end
166
+
167
+ def receive
168
+ current.__send__(:receive)
169
+ end
170
+ alias_method :recv, :receive
171
+
172
+ def receive_if(&block)
173
+ current.__send__(:receive_if, &block)
174
+ end
175
+
176
+ def select(*ractors, yield_value: not_given = true, move: false)
177
+ cur = Ractor.current
178
+ queues = ractors.map do |r|
179
+ r == cur ? r.ractor_incoming_queue : r.ractor_outgoing_queue
180
+ end
181
+ if !not_given
182
+ out = current.ractor_outgoing_queue
183
+ yield_value = ractor_isolate(yield_value, move)
184
+ elsif ractors.empty?
185
+ raise ArgumentError, 'specify at least one ractor or `yield_value`'
186
+ end
187
+
188
+ while true # rubocop:disable Style/InfiniteLoop
189
+ # Don't `loop`, in case of `ClosedError` (not that there should be any)
190
+ queues.each_with_index do |q, i|
191
+ q.pop_non_blocking do |val|
192
+ r = ractors[i]
193
+ return [r == cur ? :receive : r, val]
194
+ end
195
+ end
196
+
197
+ if out && out.num_waiting > 0
198
+ # Not quite atomic...
199
+ out.push(yield_value, ack: true)
200
+ return [:yield, nil]
201
+ end
202
+
203
+ sleep(0.001)
204
+ end
205
+ end
206
+
207
+ def make_shareable(obj)
208
+ return obj if ractor_check_shareability?(obj, true)
209
+
210
+ raise Ractor::Error, '#freeze does not freeze object correctly'
211
+ end
212
+
213
+ def shareable?(obj)
214
+ ractor_check_shareability?(obj, false)
215
+ end
216
+
217
+ def current
218
+ Thread.current.thread_variable_get(:backports_ractor) ||
219
+ Thread.current.thread_variable_set(:backports_ractor, ractor_find_current)
220
+ end
221
+
222
+ def count
223
+ ObjectSpace.each_object(Ractor).count(&:ractor_live?)
224
+ end
225
+
226
+ # @api private
227
+ def ractor_reset
228
+ ObjectSpace.each_object(Ractor).each do |r|
229
+ next if r == Ractor.current
230
+ next unless (th = r.ractor_thread)
231
+
232
+ th.kill
233
+ th.join
234
+ end
235
+ Ractor.current.ractor_incoming_queue.clear
236
+ end
237
+
238
+ attr_reader :main
239
+
240
+ private def ractor_init
241
+ @ractor_shareable = ::ObjectSpace::WeakMap.new
242
+ @main = Ractor.new { nil }
243
+ RactorThreadGroups[::ThreadGroup::Default] = @main
244
+ end
245
+
246
+ private def ractor_find_current
247
+ RactorThreadGroups[Thread.current.group]
248
+ end
249
+ end
250
+
251
+ # @api private
252
+ def ractor_live?
253
+ !defined?(@ractor_thread) || # May happen if `count` is called from another thread before `initialize` has completed
254
+ @ractor_thread.status
255
+ end
256
+
257
+ # @api private
258
+ attr_reader :ractor_outgoing_queue, :ractor_incoming_queue, :ractor_thread
259
+
260
+ ractor_init
261
+ end