backports 3.18.0 → 3.20.1

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