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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/Gemfile +3 -16
  4. data/README.md +31 -11
  5. data/backports.gemspec +1 -1
  6. data/lib/backports/1.8.7.rb +5 -4
  7. data/lib/backports/1.9.1.rb +6 -1
  8. data/lib/backports/1.9.2.rb +6 -1
  9. data/lib/backports/1.9.3.rb +6 -1
  10. data/lib/backports/2.0.0.rb +7 -2
  11. data/lib/backports/2.1.0.rb +2 -2
  12. data/lib/backports/2.2.0.rb +2 -2
  13. data/lib/backports/2.2.0/string/unicode_normalize.rb +3 -3
  14. data/lib/backports/2.3.0.rb +2 -2
  15. data/lib/backports/2.3.0/queue/close.rb +48 -0
  16. data/lib/backports/2.3.0/string.rb +3 -0
  17. data/lib/backports/2.4.0.rb +2 -2
  18. data/lib/backports/2.4.0/bignum.rb +3 -0
  19. data/lib/backports/2.4.0/bignum/dup.rb +5 -0
  20. data/lib/backports/2.5.0.rb +2 -2
  21. data/lib/backports/2.5.0/hash/transform_keys.rb +10 -3
  22. data/lib/backports/2.5.0/integer/sqrt.rb +1 -1
  23. data/lib/backports/2.5.0/string/undump.rb +2 -2
  24. data/lib/backports/2.5.rb +1 -1
  25. data/lib/backports/2.6.0.rb +3 -3
  26. data/lib/backports/2.6.0/enumerable/chain.rb +2 -0
  27. data/lib/backports/2.6.rb +1 -1
  28. data/lib/backports/2.7.0.rb +3 -3
  29. data/lib/backports/3.0.0.rb +3 -0
  30. data/lib/backports/3.0.0/env.rb +3 -0
  31. data/lib/backports/3.0.0/env/except.rb +10 -0
  32. data/lib/backports/3.0.0/hash.rb +3 -0
  33. data/lib/backports/3.0.0/hash/except.rb +10 -0
  34. data/lib/backports/3.0.0/hash/transform_keys.rb +48 -0
  35. data/lib/backports/3.0.0/ractor.rb +5 -0
  36. data/lib/backports/3.0.0/symbol.rb +3 -0
  37. data/lib/backports/3.0.0/symbol/name.rb +11 -0
  38. data/lib/backports/3.0.rb +1 -0
  39. data/lib/backports/ractor/cloner.rb +91 -0
  40. data/lib/backports/ractor/errors.rb +16 -0
  41. data/lib/backports/ractor/queues.rb +62 -0
  42. data/lib/backports/ractor/ractor.rb +238 -0
  43. data/lib/backports/ractor/sharing.rb +93 -0
  44. data/lib/backports/tools/filtered_queue.rb +202 -0
  45. data/lib/backports/tools/require_relative_dir.rb +6 -1
  46. data/lib/backports/version.rb +1 -1
  47. metadata +27 -7
@@ -1,3 +1,3 @@
1
- # require this file to load all the backports up to Ruby 2.5
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|
@@ -1,2 +1,2 @@
1
- # require this file to load all the backports of Ruby 2.4 and below
1
+ # require this file to load all the backports of Ruby 2.6 and below
2
2
  require 'backports/2.6.0'
@@ -1,3 +1,3 @@
1
- # require this file to load all the backports up to Ruby 2.5
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,3 @@
1
+ # require this file to load all the backports up to Ruby 3.0
2
+ require 'backports/2.7.0'
3
+ Backports.require_relative_dir if RUBY_VERSION < '3.0'
@@ -0,0 +1,3 @@
1
+ require 'backports/tools/require_relative_dir'
2
+
3
+ Backports.require_relative_dir
@@ -0,0 +1,10 @@
1
+ class << ENV
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 ENV.respond_to? :except
@@ -0,0 +1,3 @@
1
+ require 'backports/tools/require_relative_dir'
2
+
3
+ Backports.require_relative_dir
@@ -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,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