backports 3.18.1 → 3.20.2

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