backports 3.17.2 → 3.20.0

Sign up to get free protection for your applications and to get access to all the features.
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