backports 3.18.2 → 3.21.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/Gemfile +2 -2
  4. data/README.md +35 -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.4.0.rb +1 -1
  12. data/lib/backports/2.4.0/bignum.rb +3 -0
  13. data/lib/backports/2.4.0/bignum/dup.rb +5 -0
  14. data/lib/backports/2.4.0/string/unpack1.rb +7 -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 +103 -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 +23 -3
@@ -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,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,103 @@
1
+ # shareable_constant_value: literal
2
+
3
+ using ::RubyNext if defined?(::RubyNext)
4
+
5
+ module Backports
6
+ class Ractor
7
+ class Cloner
8
+ class << self
9
+ def deep_clone(obj)
10
+ return obj if Ractor.ractor_shareable_self?(obj, false) { false }
11
+
12
+ new.deep_clone(obj)
13
+ end
14
+
15
+ private :new
16
+ end
17
+
18
+ def initialize
19
+ @processed = {}.compare_by_identity
20
+ @changed = nil
21
+ end
22
+
23
+ def deep_clone(obj)
24
+ result = process(obj) do |r|
25
+ copy_contents(r)
26
+ end
27
+ return result if result
28
+
29
+ Ractor.ractor_mark_set_shareable(@processed)
30
+ obj
31
+ end
32
+
33
+ # Yields a deep copy.
34
+ # If no deep copy is needed, `obj` is returned and
35
+ # nothing is yielded
36
+ private def clone_deeper(obj)
37
+ return obj if Ractor.ractor_shareable_self?(obj, false) { false }
38
+
39
+ result = process(obj) do |r|
40
+ copy_contents(r)
41
+ end
42
+ return obj unless result
43
+
44
+ yield result if block_given?
45
+ result
46
+ end
47
+
48
+ # Yields if `obj` is a new structure
49
+ # Returns the deep copy, or `false` if no deep copy is needed
50
+ private def process(obj)
51
+ @processed.fetch(obj) do
52
+ # For recursive structures, assume that we'll need a duplicate.
53
+ # If that's not the case, we will have duplicated the whole structure
54
+ # for nothing...
55
+ @processed[obj] = result = obj.dup
56
+ changed = track_change { yield result }
57
+ return false if obj.frozen? && !changed
58
+
59
+ @changed = true
60
+ result.freeze if obj.frozen?
61
+
62
+ result
63
+ end
64
+ end
65
+
66
+ # returns if the block called `deep clone` and that the deep copy was needed
67
+ private def track_change
68
+ prev = @changed
69
+ @changed = false
70
+ yield
71
+ @changed
72
+ ensure
73
+ @changed = prev
74
+ end
75
+
76
+ # modifies in place `obj` by calling `deep clone` on its contents
77
+ private def copy_contents(obj)
78
+ case obj
79
+ when ::Hash
80
+ if obj.default
81
+ clone_deeper(obj.default) do |copy|
82
+ obj.default = copy
83
+ end
84
+ end
85
+ obj.transform_keys! { |key| clone_deeper(key) }
86
+ obj.transform_values! { |value| clone_deeper(value) }
87
+ when ::Array
88
+ obj.map! { |item| clone_deeper(item) }
89
+ when ::Struct
90
+ obj.each_pair do |key, item|
91
+ clone_deeper(item) { |copy| obj[key] = copy }
92
+ end
93
+ end
94
+ obj.instance_variables.each do |var|
95
+ clone_deeper(obj.instance_variable_get(var)) do |copy|
96
+ obj.instance_variable_set(var, copy)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ private_constant :Cloner
102
+ end
103
+ 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