backports 3.19.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -2
- data/Gemfile +1 -1
- data/README.md +14 -9
- data/lib/backports/3.0.0/ractor.rb +5 -0
- data/lib/backports/ractor/cloner.rb +91 -0
- data/lib/backports/ractor/errors.rb +16 -0
- data/lib/backports/ractor/queues.rb +62 -0
- data/lib/backports/ractor/ractor.rb +238 -0
- data/lib/backports/ractor/sharing.rb +93 -0
- data/lib/backports/tools/filtered_queue.rb +202 -0
- data/lib/backports/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3d95602bc2a5f1d8835d424036e6d51671baa7c8d46f4482c61b6519044d493
|
4
|
+
data.tar.gz: '059fa80520c54c368ad38f6477bbb2b9debe734970b0e007ba04be2b37c0138f'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b4031dc752ebb19df160368c272e37b0bf9a939288ccccc9d5043567efed030d157d2da1668ec0787216ab05f730b3ff8d7c23b78a5e2400719cd5183232a69
|
7
|
+
data.tar.gz: c92bbe1f69c27f5ed6e18162e8fe96665a9828480bd2c10a13750f1b8190db53a0ef25b2009f2414cb3d193e22c0810b12a259ef84d079bac6f53dde9aafcd96
|
data/CHANGELOG.md
CHANGED
@@ -8,7 +8,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
8
8
|
|
9
9
|
Note: [Next major version (X-mas 2021?)](https://github.com/marcandre/backports/issues/139) may drop support for Ruby < 2.2, please comment.
|
10
10
|
|
11
|
-
## [3.
|
11
|
+
## [3.20.0](https://github.com/marcandre/backports/compare/v3.19.0...v3.20.0) - 2020-12-30
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
Ractor backport (all methods)
|
16
|
+
|
17
|
+
## [3.19.0](https://github.com/marcandre/backports/compare/v3.18.2...v3.19.0) - 2020-12-28
|
12
18
|
|
13
19
|
### Added
|
14
20
|
|
@@ -35,7 +41,7 @@ Note: [Next major version (X-mas 2021?)](https://github.com/marcandre/backports/
|
|
35
41
|
|
36
42
|
Require per ruby version now properly requiring 2.3.0 backports for `String` [#152]
|
37
43
|
|
38
|
-
## [3.18.1](https://github.com/marcandre/backports/compare/v3.18.1...v3.18.2) - 2020-
|
44
|
+
## [3.18.1](https://github.com/marcandre/backports/compare/v3.18.1...v3.18.2) - 2020-06-22
|
39
45
|
|
40
46
|
### Fixed
|
41
47
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Backports Library [<img src="https://travis-ci.org/marcandre/backports.svg?branch=master">](https://travis-ci.org/marcandre/backports) [<img src="https://badge.fury.io/rb/backports.svg" alt="Gem Version" />](http://badge.fury.io/rb/backports) [](https://tidelift.com/subscription/pkg/rubygems-backports?utm_source=rubygems-backports&utm_medium=referral&utm_campaign=readme)
|
2
2
|
|
3
|
-
Yearning to use
|
3
|
+
Yearning to use write a gem using some new cool features in Ruby 3.0 while
|
4
|
+
still supporting Ruby 2.5.x?
|
4
5
|
Have some legacy code in Ruby 1.8 but can't live without `flat_map`?
|
5
6
|
|
6
7
|
This gem is for you!
|
@@ -16,13 +17,13 @@ for Ruby < 2.2.
|
|
16
17
|
|
17
18
|
### Explicitly (recommended)
|
18
19
|
|
19
|
-
For example, if you want to use transform_values and transform_keys
|
20
|
+
For example, if you want to use `transform_values` and `transform_keys`, even in
|
20
21
|
Ruby implementations that don't include it:
|
21
22
|
|
22
23
|
require 'backports/2.4.0/hash/transform_values'
|
23
24
|
require 'backports/2.5.0/hash/transform_keys'
|
24
25
|
|
25
|
-
This will enable Hash#transform_values and Hash#transform_keys
|
26
|
+
This will enable `Hash#transform_values` and `Hash#transform_keys`, using the
|
26
27
|
native versions if available or otherwise provide a pure Ruby version.
|
27
28
|
|
28
29
|
### By Module
|
@@ -32,19 +33,17 @@ Class:
|
|
32
33
|
|
33
34
|
require 'backports/2.3.0/hash'
|
34
35
|
|
35
|
-
This will make sure that Hash responds to dig
|
36
|
-
to_proc
|
36
|
+
This will make sure that Hash responds to `dig`, `fetch_values`, `to_proc` and comparisons.
|
37
37
|
|
38
38
|
### Up to a specific Ruby version (for quick coding)
|
39
39
|
|
40
40
|
You can load all backports up to a specific version.
|
41
|
-
For example, to bring any
|
42
|
-
version of Ruby mostly up to Ruby 2.7.0's standards:
|
41
|
+
For example, to bring any version of Ruby mostly up to Ruby 3.0.0's standards:
|
43
42
|
|
44
|
-
require 'backports/
|
43
|
+
require 'backports/3.0.0'
|
45
44
|
|
46
45
|
This will bring in all the features of 1.8.7 and many features of Ruby 1.9.x
|
47
|
-
all the way up to Ruby
|
46
|
+
all the way up to Ruby 3.0.0 (for all versions of Ruby)!
|
48
47
|
|
49
48
|
You may `require 'backports/latest'` as a
|
50
49
|
shortcut to the latest Ruby version supported.
|
@@ -118,6 +117,12 @@ itself, JRuby and Rubinius.
|
|
118
117
|
- `except`
|
119
118
|
- `transform_keys`, `transform_keys!` (with hash argument)
|
120
119
|
|
120
|
+
#### Ractor
|
121
|
+
- All methods, with the caveats:
|
122
|
+
- uses Ruby's `Thread` internally
|
123
|
+
- will not raise some errors when `Ractor` would (in particular `Ractor::IsolationError`)
|
124
|
+
- supported in Ruby 2.0+ only
|
125
|
+
|
121
126
|
#### Symbol
|
122
127
|
- `name`
|
123
128
|
|
@@ -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,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
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class Ractor
|
2
|
+
class << self
|
3
|
+
# @api private
|
4
|
+
def ractor_isolate(val, move = false)
|
5
|
+
return val if move
|
6
|
+
|
7
|
+
Cloner.deep_clone(val)
|
8
|
+
end
|
9
|
+
|
10
|
+
private def ractor_check_shareability?(obj, freeze_all)
|
11
|
+
ractor_shareable_self?(obj, freeze_all) do
|
12
|
+
visited = {}
|
13
|
+
|
14
|
+
return false unless ractor_shareable_parts?(obj, freeze_all, visited)
|
15
|
+
|
16
|
+
ractor_mark_set_shareable(visited)
|
17
|
+
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# yield if shareability can't be determined without looking at its parts
|
23
|
+
def ractor_shareable_self?(obj, freeze_all)
|
24
|
+
return true if @ractor_shareable.key?(obj)
|
25
|
+
return true if ractor_shareable_by_nature?(obj, freeze_all)
|
26
|
+
if obj.frozen? || (freeze_all && obj.freeze)
|
27
|
+
yield
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private def ractor_shareable_parts?(obj, freeze_all, visited)
|
34
|
+
return true if visited.key?(obj)
|
35
|
+
visited[obj] = true
|
36
|
+
|
37
|
+
ractor_traverse(obj) do |part|
|
38
|
+
return false unless ractor_shareable_self?(part, freeze_all) do
|
39
|
+
ractor_shareable_parts?(part, freeze_all, visited)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def ractor_mark_set_shareable(visited)
|
47
|
+
visited.each do |key|
|
48
|
+
@ractor_shareable[key] = Ractor
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private def ractor_traverse(obj, &block)
|
53
|
+
case obj
|
54
|
+
when ::Hash
|
55
|
+
Hash obj.default
|
56
|
+
yield obj.default_proc
|
57
|
+
obj.each do |key, value|
|
58
|
+
yield key
|
59
|
+
yield value
|
60
|
+
end
|
61
|
+
when ::Range
|
62
|
+
yield obj.begin
|
63
|
+
yield obj.end
|
64
|
+
when ::Array, ::Struct
|
65
|
+
obj.each(&block)
|
66
|
+
when ::Complex
|
67
|
+
yield obj.real
|
68
|
+
yield obj.imaginary
|
69
|
+
when ::Rational
|
70
|
+
yield obj.numerator
|
71
|
+
yield obj.denominator
|
72
|
+
end
|
73
|
+
obj.instance_variables.each do |var|
|
74
|
+
yield obj.instance_variable_get(var)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private def ractor_shareable_by_nature?(obj, freeze_all)
|
79
|
+
case obj
|
80
|
+
when ::Module, ::Ractor
|
81
|
+
true
|
82
|
+
when ::Regexp, ::Range, ::Numeric
|
83
|
+
!freeze_all # Assume that these are literals that would have been frozen in 3.0
|
84
|
+
# unless we're making them shareable, in which case we might as well
|
85
|
+
# freeze them for real.
|
86
|
+
when ::Symbol, false, true, nil # Were only frozen in Ruby 2.3+
|
87
|
+
true
|
88
|
+
else
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module Backports
|
2
|
+
class FilteredQueue
|
3
|
+
require 'backports/2.3.0/queue/close'
|
4
|
+
CONSUME_ON_ESCAPE = true
|
5
|
+
|
6
|
+
class ClosedQueueError < ::ClosedQueueError
|
7
|
+
end
|
8
|
+
class TimeoutError < ::ThreadError
|
9
|
+
end
|
10
|
+
|
11
|
+
class Message
|
12
|
+
# Not using Struct as we want comparision by identity
|
13
|
+
attr_reader :value
|
14
|
+
attr_accessor :reserved
|
15
|
+
|
16
|
+
def initialize(value)
|
17
|
+
@value = value
|
18
|
+
@reserved = false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
private_constant :Message
|
22
|
+
|
23
|
+
# Like ::Queue, but with
|
24
|
+
# - filtering
|
25
|
+
# - timeout
|
26
|
+
# - raises on closed queues
|
27
|
+
|
28
|
+
attr_reader :num_waiting
|
29
|
+
|
30
|
+
# Timeout processing based on https://spin.atomicobject.com/2017/06/28/queue-pop-with-timeout-fixed/
|
31
|
+
def initialize
|
32
|
+
@mutex = ::Mutex.new
|
33
|
+
@queue = []
|
34
|
+
@closed = false
|
35
|
+
@received = ::ConditionVariable.new
|
36
|
+
@num_waiting = 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
@mutex.synchronize do
|
41
|
+
@closed = true
|
42
|
+
@received.broadcast
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def closed?
|
48
|
+
@closed
|
49
|
+
end
|
50
|
+
|
51
|
+
def <<(x)
|
52
|
+
@mutex.synchronize do
|
53
|
+
ensure_open
|
54
|
+
@queue << Message.new(x)
|
55
|
+
@received.signal
|
56
|
+
end
|
57
|
+
self
|
58
|
+
end
|
59
|
+
alias_method :push, :<<
|
60
|
+
|
61
|
+
def clear
|
62
|
+
@mutex.synchronize do
|
63
|
+
@queue.clear
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def pop(timeout: nil, &block)
|
69
|
+
msg = nil
|
70
|
+
exclude = [] if block # exclusion list of messages rejected by this call
|
71
|
+
timeout_time = timeout + Time.now.to_f if timeout
|
72
|
+
while true do
|
73
|
+
@mutex.synchronize do
|
74
|
+
reenter if reentrant?
|
75
|
+
msg = acquire!(timeout_time, exclude)
|
76
|
+
return consume!(msg).value unless block
|
77
|
+
end
|
78
|
+
return msg.value if filter?(msg, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def empty?
|
83
|
+
avail = @mutex.synchronize do
|
84
|
+
available!
|
85
|
+
end
|
86
|
+
|
87
|
+
!avail
|
88
|
+
end
|
89
|
+
|
90
|
+
protected def timeout_value
|
91
|
+
raise self.class::TimeoutError, "timeout elapsed"
|
92
|
+
end
|
93
|
+
|
94
|
+
protected def closed_queue_value
|
95
|
+
ensure_open
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return if outer message should be consumed or not
|
99
|
+
protected def reenter
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
### private section
|
104
|
+
#
|
105
|
+
# bang methods require synchronization
|
106
|
+
|
107
|
+
# @returns:
|
108
|
+
# * true if message consumed (block result truthy or due to reentrant call)
|
109
|
+
# * false if rejected
|
110
|
+
private def filter?(msg)
|
111
|
+
consume = self.class::CONSUME_ON_ESCAPE
|
112
|
+
begin
|
113
|
+
reentered = consume_on_reentry(msg) do
|
114
|
+
consume = !!(yield msg.value)
|
115
|
+
end
|
116
|
+
reentered ? reenter : consume
|
117
|
+
ensure
|
118
|
+
commit(msg, consume) unless reentered
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @returns msg
|
123
|
+
private def consume!(msg)
|
124
|
+
@queue.delete(msg)
|
125
|
+
end
|
126
|
+
|
127
|
+
private def reject!(msg)
|
128
|
+
msg.reserved = false
|
129
|
+
@received.broadcast
|
130
|
+
end
|
131
|
+
|
132
|
+
private def commit(msg, consume)
|
133
|
+
@mutex.synchronize do
|
134
|
+
if consume
|
135
|
+
consume!(msg)
|
136
|
+
else
|
137
|
+
reject!(msg)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private def consume_on_reentry(msg)
|
143
|
+
q_map = current_filtered_queues
|
144
|
+
if (outer_msg = q_map[self])
|
145
|
+
commit(outer_msg, reenter)
|
146
|
+
end
|
147
|
+
q_map[self] = msg
|
148
|
+
begin
|
149
|
+
yield
|
150
|
+
ensure
|
151
|
+
reentered = !q_map.delete(self)
|
152
|
+
end
|
153
|
+
reentered
|
154
|
+
end
|
155
|
+
|
156
|
+
private def reentrant?
|
157
|
+
!!current_filtered_queues[self]
|
158
|
+
end
|
159
|
+
|
160
|
+
# @returns Hash { FilteredQueue => Message }
|
161
|
+
private def current_filtered_queues
|
162
|
+
t = Thread.current
|
163
|
+
t.thread_variable_get(:backports_currently_filtered_queues) or
|
164
|
+
t.thread_variable_set(:backports_currently_filtered_queues, {}.compare_by_identity)
|
165
|
+
end
|
166
|
+
|
167
|
+
# private methods assume @mutex synchonized
|
168
|
+
# adds to exclude list
|
169
|
+
private def acquire!(timeout_time, exclude = nil)
|
170
|
+
while true do
|
171
|
+
if (msg = available!(exclude))
|
172
|
+
msg.reserved = true
|
173
|
+
exclude << msg if exclude
|
174
|
+
return msg
|
175
|
+
end
|
176
|
+
return closed_queue_value if @closed
|
177
|
+
# wait for element or timeout
|
178
|
+
if timeout_time
|
179
|
+
remaining_time = timeout_time - ::Time.now.to_f
|
180
|
+
return timeout_value if remaining_time <= 0
|
181
|
+
end
|
182
|
+
begin
|
183
|
+
@num_waiting += 1
|
184
|
+
@received.wait(@mutex, remaining_time)
|
185
|
+
ensure
|
186
|
+
@num_waiting -= 1
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private def available!(exclude = nil)
|
192
|
+
@queue.find do |msg|
|
193
|
+
next if exclude && exclude.include?(msg)
|
194
|
+
!msg.reserved
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
private def ensure_open
|
199
|
+
raise self.class::ClosedQueueError, 'queue closed' if @closed
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
data/lib/backports/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Backports
|
2
|
-
VERSION = "3.
|
2
|
+
VERSION = "3.20.0" unless Backports.constants.include? :VERSION # the guard is against a redefinition warning that happens on Travis
|
3
3
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: backports
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.20.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc-André Lafortune
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Essential backports that enable many of the nice features of Ruby for
|
14
14
|
earlier versions.
|
@@ -538,6 +538,7 @@ files:
|
|
538
538
|
- lib/backports/3.0.0/hash.rb
|
539
539
|
- lib/backports/3.0.0/hash/except.rb
|
540
540
|
- lib/backports/3.0.0/hash/transform_keys.rb
|
541
|
+
- lib/backports/3.0.0/ractor.rb
|
541
542
|
- lib/backports/3.0.0/symbol.rb
|
542
543
|
- lib/backports/3.0.0/symbol/name.rb
|
543
544
|
- lib/backports/3.0.rb
|
@@ -548,6 +549,11 @@ files:
|
|
548
549
|
- lib/backports/force/string_length.rb
|
549
550
|
- lib/backports/force/string_size.rb
|
550
551
|
- lib/backports/latest.rb
|
552
|
+
- lib/backports/ractor/cloner.rb
|
553
|
+
- lib/backports/ractor/errors.rb
|
554
|
+
- lib/backports/ractor/queues.rb
|
555
|
+
- lib/backports/ractor/ractor.rb
|
556
|
+
- lib/backports/ractor/sharing.rb
|
551
557
|
- lib/backports/rails.rb
|
552
558
|
- lib/backports/rails/array.rb
|
553
559
|
- lib/backports/rails/enumerable.rb
|
@@ -566,6 +572,7 @@ files:
|
|
566
572
|
- lib/backports/tools/arguments.rb
|
567
573
|
- lib/backports/tools/deprecation.rb
|
568
574
|
- lib/backports/tools/extreme_object.rb
|
575
|
+
- lib/backports/tools/filtered_queue.rb
|
569
576
|
- lib/backports/tools/float_integer_conversion.rb
|
570
577
|
- lib/backports/tools/io.rb
|
571
578
|
- lib/backports/tools/make_block_optional.rb
|