polyphony 0.78 → 0.81
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 +19 -0
- data/Gemfile.lock +2 -1
- data/examples/core/pingpong.rb +7 -4
- data/examples/core/zlib_stream.rb +15 -0
- data/ext/polyphony/backend_common.c +16 -8
- data/ext/polyphony/backend_common.h +9 -3
- data/ext/polyphony/backend_io_uring.c +85 -31
- data/ext/polyphony/backend_libev.c +33 -17
- data/ext/polyphony/fiber.c +27 -27
- data/ext/polyphony/polyphony.c +9 -8
- data/ext/polyphony/polyphony.h +21 -7
- data/ext/polyphony/thread.c +6 -2
- data/lib/polyphony/adapters/fs.rb +4 -0
- data/lib/polyphony/adapters/process.rb +14 -1
- data/lib/polyphony/adapters/redis.rb +28 -0
- data/lib/polyphony/adapters/sequel.rb +19 -1
- data/lib/polyphony/core/debug.rb +201 -0
- data/lib/polyphony/core/exceptions.rb +21 -6
- data/lib/polyphony/core/global_api.rb +228 -73
- data/lib/polyphony/core/resource_pool.rb +65 -20
- data/lib/polyphony/core/sync.rb +57 -12
- data/lib/polyphony/core/thread_pool.rb +42 -5
- data/lib/polyphony/core/throttler.rb +21 -5
- data/lib/polyphony/core/timer.rb +125 -1
- data/lib/polyphony/extensions/exception.rb +36 -6
- data/lib/polyphony/extensions/fiber.rb +244 -61
- data/lib/polyphony/extensions/io.rb +4 -2
- data/lib/polyphony/extensions/kernel.rb +9 -4
- data/lib/polyphony/extensions/object.rb +8 -0
- data/lib/polyphony/extensions/openssl.rb +3 -1
- data/lib/polyphony/extensions/socket.rb +458 -39
- data/lib/polyphony/extensions/thread.rb +108 -43
- data/lib/polyphony/extensions/timeout.rb +12 -1
- data/lib/polyphony/extensions.rb +1 -0
- data/lib/polyphony/net.rb +66 -7
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +0 -2
- data/test/test_backend.rb +6 -2
- data/test/test_global_api.rb +0 -23
- data/test/test_io.rb +7 -7
- data/test/test_resource_pool.rb +1 -1
- data/test/test_signal.rb +15 -15
- data/test/test_thread.rb +1 -1
- data/test/test_throttler.rb +0 -6
- data/test/test_trace.rb +189 -24
- metadata +9 -8
- data/lib/polyphony/core/channel.rb +0 -15
@@ -3,8 +3,16 @@
|
|
3
3
|
require_relative './throttler'
|
4
4
|
|
5
5
|
module Polyphony
|
6
|
-
|
6
|
+
|
7
|
+
# Global API methods to be included in `::Object`
|
7
8
|
module GlobalAPI
|
9
|
+
|
10
|
+
# Spins up a fiber that will run the given block after sleeping for the
|
11
|
+
# given delay.
|
12
|
+
#
|
13
|
+
# @param delay [Number] delay in seconds before running the given block
|
14
|
+
# @param &block [Proc] block to run
|
15
|
+
# @return [Fiber] spun fiber
|
8
16
|
def after(interval, &block)
|
9
17
|
spin do
|
10
18
|
sleep interval
|
@@ -12,64 +20,88 @@ module Polyphony
|
|
12
20
|
end
|
13
21
|
end
|
14
22
|
|
23
|
+
# call-seq:
|
24
|
+
# cancel_after(interval) { ... }
|
25
|
+
# cancel_after(interval, with_exception: exception) { ... }
|
26
|
+
# cancel_after(interval, with_exception: [klass, message]) { ... }
|
27
|
+
# cancel_after(interval) { |timeout| ... }
|
28
|
+
# cancel_after(interval, with_exception: exception) { |timeout| ... }
|
29
|
+
# cancel_after(interval, with_exception: [klass, message]) { |timeout| ... }
|
30
|
+
#
|
31
|
+
# Runs the given block after setting up a cancellation timer for
|
32
|
+
# cancellation. If the cancellation timer elapses, the execution will be
|
33
|
+
# interrupted with an exception defaulting to `Polyphony::Cancel`.
|
34
|
+
#
|
35
|
+
# This method should be used when a timeout should cause an exception to be
|
36
|
+
# propagated down the call stack or up the fiber tree.
|
37
|
+
#
|
38
|
+
# Example of normal use:
|
39
|
+
#
|
40
|
+
# def read_from_io_with_timeout(io)
|
41
|
+
# cancel_after(10) { io.read }
|
42
|
+
# rescue Polyphony::Cancel
|
43
|
+
# nil
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# The timeout period can be reset by passing a block that takes a single
|
47
|
+
# argument. The block will be provided with the canceller fiber. To reset
|
48
|
+
# the timeout, use `Fiber#reset`, as shown in the following example:
|
49
|
+
#
|
50
|
+
# cancel_after(10) do |timeout|
|
51
|
+
# loop do
|
52
|
+
# msg = socket.gets
|
53
|
+
# timeout.reset
|
54
|
+
# handle_msg(msg)
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @param interval [Number] timout in seconds
|
59
|
+
# @param with_exception: [Class, Exception] exception or exception class
|
60
|
+
# @param &block [Proc] block to execute
|
61
|
+
# @return [any] block's return value
|
15
62
|
def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
|
16
|
-
if
|
17
|
-
|
18
|
-
elsif block.arity > 0
|
19
|
-
cancel_after_with_block(Fiber.current, interval, with_exception, &block)
|
63
|
+
if block.arity > 0
|
64
|
+
cancel_after_with_optional_reset(interval, with_exception, &block)
|
20
65
|
else
|
21
66
|
Polyphony.backend_timeout(interval, with_exception, &block)
|
22
67
|
end
|
23
68
|
end
|
24
69
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
fiber.schedule exception
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def cancel_after_with_block(fiber, interval, with_exception, &block)
|
35
|
-
canceller = cancel_after_blockless_canceller(fiber, interval, with_exception)
|
36
|
-
block.call(canceller)
|
37
|
-
ensure
|
38
|
-
canceller.stop
|
39
|
-
end
|
40
|
-
|
41
|
-
def cancel_exception(exception)
|
42
|
-
case exception
|
43
|
-
when Class then exception.new
|
44
|
-
when Array then exception[0].new(exception[1])
|
45
|
-
else RuntimeError.new(exception)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
70
|
+
# Spins up a new fiber.
|
71
|
+
#
|
72
|
+
# @param tag [any] optional tag for the new fiber
|
73
|
+
# @param &block [Proc] fiber block
|
74
|
+
# @return [Fiber] new fiber
|
49
75
|
def spin(tag = nil, &block)
|
50
76
|
Fiber.current.spin(tag, caller, &block)
|
51
77
|
end
|
52
78
|
|
79
|
+
# Spins up a new fiber, running the given block inside an infinite loop. If
|
80
|
+
# `rate:` or `interval:` parameters are given, the loop is throttled
|
81
|
+
# accordingly.
|
82
|
+
#
|
83
|
+
# @param tag [any] optional tag for the new fiber
|
84
|
+
# @param rate: [Number, nil] loop rate (times per second)
|
85
|
+
# @param interval: [Number, nil] interval between consecutive iterations in seconds
|
86
|
+
# @param &block [Proc] code to run
|
87
|
+
# @return [Fiber] new fiber
|
53
88
|
def spin_loop(tag = nil, rate: nil, interval: nil, &block)
|
54
89
|
if rate || interval
|
55
90
|
Fiber.current.spin(tag, caller) do
|
56
91
|
throttled_loop(rate: rate, interval: interval, &block)
|
57
92
|
end
|
58
93
|
else
|
59
|
-
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def spin_looped_block(tag, caller, block)
|
64
|
-
Fiber.current.spin(tag, caller) do
|
65
|
-
block.call while true
|
66
|
-
rescue LocalJumpError, StopIteration
|
67
|
-
# break called or StopIteration raised
|
94
|
+
spin_loop_without_throttling(tag, caller, block)
|
68
95
|
end
|
69
96
|
end
|
70
97
|
|
71
|
-
|
72
|
-
|
98
|
+
# Runs the given code, then waits for any child fibers of the current fibers
|
99
|
+
# to terminate.
|
100
|
+
#
|
101
|
+
# @param &block [Proc] code to run
|
102
|
+
# @return [any] given block's return value
|
103
|
+
def spin_scope(&block)
|
104
|
+
raise unless block
|
73
105
|
|
74
106
|
spin do
|
75
107
|
result = yield
|
@@ -78,61 +110,121 @@ module Polyphony
|
|
78
110
|
end.await
|
79
111
|
end
|
80
112
|
|
113
|
+
# Runs the given block in an infinite loop with a regular interval between
|
114
|
+
# consecutive iterations.
|
115
|
+
#
|
116
|
+
# @param interval [Number] interval between consecutive iterations in seconds
|
117
|
+
# @param &block [Proc] block to run
|
118
|
+
# @return [void]
|
81
119
|
def every(interval, &block)
|
82
120
|
Polyphony.backend_timer_loop(interval, &block)
|
83
121
|
end
|
84
122
|
|
123
|
+
# call-seq:
|
124
|
+
# move_on_after(interval) { ... }
|
125
|
+
# move_on_after(interval, with_value: value) { ... }
|
126
|
+
# move_on_after(interval) { |canceller| ... }
|
127
|
+
# move_on_after(interval, with_value: value) { |canceller| ... }
|
128
|
+
#
|
129
|
+
# Runs the given block after setting up a cancellation timer for
|
130
|
+
# cancellation. If the cancellation timer elapses, the execution will be
|
131
|
+
# interrupted with a `Polyphony::MoveOn` exception, which will be rescued,
|
132
|
+
# and with cause the operation to return the given value.
|
133
|
+
#
|
134
|
+
# This method should be used when a timeout is to be handled locally,
|
135
|
+
# without generating an exception that is to propagated down the call stack
|
136
|
+
# or up the fiber tree.
|
137
|
+
#
|
138
|
+
# Example of normal use:
|
139
|
+
#
|
140
|
+
# move_on_after(10) {
|
141
|
+
# sleep 60
|
142
|
+
# 42
|
143
|
+
# } #=> nil
|
144
|
+
#
|
145
|
+
# move_on_after(10, with_value: :oops) {
|
146
|
+
# sleep 60
|
147
|
+
# 42
|
148
|
+
# } #=> :oops
|
149
|
+
#
|
150
|
+
# The timeout period can be reset by passing a block that takes a single
|
151
|
+
# argument. The block will be provided with the canceller fiber. To reset
|
152
|
+
# the timeout, use `Fiber#reset`, as shown in the following example:
|
153
|
+
#
|
154
|
+
# move_on_after(10) do |timeout|
|
155
|
+
# loop do
|
156
|
+
# msg = socket.gets
|
157
|
+
# timeout.reset
|
158
|
+
# handle_msg(msg)
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# @param interval [Number] timout in seconds
|
163
|
+
# @param with_value: [any] return value in case of timeout
|
164
|
+
# @param &block [Proc] block to execute
|
165
|
+
# @return [any] block's return value
|
85
166
|
def move_on_after(interval, with_value: nil, &block)
|
86
|
-
if
|
87
|
-
|
88
|
-
elsif block.arity > 0
|
89
|
-
move_on_after_with_block(Fiber.current, interval, with_value, &block)
|
167
|
+
if block.arity > 0
|
168
|
+
move_on_after_with_optional_reset(interval, with_value, &block)
|
90
169
|
else
|
91
170
|
Polyphony.backend_timeout(interval, nil, with_value, &block)
|
92
171
|
end
|
93
172
|
end
|
94
173
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def move_on_after_with_block(fiber, interval, with_value, &block)
|
103
|
-
canceller = spin do
|
104
|
-
sleep interval
|
105
|
-
fiber.schedule Polyphony::MoveOn.new(with_value)
|
106
|
-
end
|
107
|
-
block.call(canceller)
|
108
|
-
rescue Polyphony::MoveOn => e
|
109
|
-
e.value
|
110
|
-
ensure
|
111
|
-
canceller.stop
|
112
|
-
end
|
113
|
-
|
174
|
+
# Returns the first message from the current fiber's mailbox. If the mailbox
|
175
|
+
# is empty, blocks until a message is available.
|
176
|
+
#
|
177
|
+
# @return [any] received message
|
114
178
|
def receive
|
115
179
|
Fiber.current.receive
|
116
180
|
end
|
117
181
|
|
182
|
+
# Returns all messages currently pending on the current fiber's mailbox.
|
183
|
+
#
|
184
|
+
# @return [Array] array of received messages
|
118
185
|
def receive_all_pending
|
119
186
|
Fiber.current.receive_all_pending
|
120
187
|
end
|
121
188
|
|
189
|
+
# Supervises the current fiber's children. See `Fiber#supervise` for
|
190
|
+
# options.
|
191
|
+
#
|
192
|
+
# @param *args [Array] positional parameters
|
193
|
+
# @param **opts [Hash] named parameters
|
194
|
+
# @param &block [Proc] given block
|
195
|
+
# @return [void]
|
122
196
|
def supervise(*args, **opts, &block)
|
123
197
|
Fiber.current.supervise(*args, **opts, &block)
|
124
198
|
end
|
125
199
|
|
200
|
+
# Sleeps for the given duration. If the duration is `nil`, sleeps
|
201
|
+
# indefinitely.
|
202
|
+
#
|
203
|
+
# @param duration [Number, nil] duration
|
204
|
+
# @return [void]
|
126
205
|
def sleep(duration = nil)
|
127
|
-
|
128
|
-
|
129
|
-
Polyphony.backend_sleep duration
|
130
|
-
end
|
131
|
-
|
132
|
-
def sleep_forever
|
133
|
-
Polyphony.backend_wait_event(true)
|
206
|
+
duration ?
|
207
|
+
Polyphony.backend_sleep(duration) : Polyphony.backend_wait_event(true)
|
134
208
|
end
|
135
209
|
|
210
|
+
# call-seq:
|
211
|
+
# throttled_loop(rate) { ... }
|
212
|
+
# throttled_loop(interval: value) { ... }
|
213
|
+
# throttled_loop(rate: value) { ... }
|
214
|
+
# throttled_loop(rate, count: value) { ... }
|
215
|
+
#
|
216
|
+
# Starts a throttled loop with the given rate. If `count:` is given, the
|
217
|
+
# loop is run for the given number of times. Otherwise, the loop is
|
218
|
+
# infinite. The loop rate (times per second) can be given as the rate
|
219
|
+
# parameter. The throttling can also be controlled by providing an
|
220
|
+
# `interval:` or `rate:` named parameter.
|
221
|
+
#
|
222
|
+
# @param rate [Number, nil] loop rate (times per second)
|
223
|
+
# @param rate: [Number] loop rate (times per second)
|
224
|
+
# @param interval: [Number] loop interval in seconds
|
225
|
+
# @param count: [Number, nil] number of iterations (nil for infinite)
|
226
|
+
# @param &block [Proc] code to run
|
227
|
+
# @return [void]
|
136
228
|
def throttled_loop(rate = nil, **opts, &block)
|
137
229
|
throttler = Polyphony::Throttler.new(rate || opts)
|
138
230
|
if opts[:count]
|
@@ -144,10 +236,73 @@ module Polyphony
|
|
144
236
|
end
|
145
237
|
rescue LocalJumpError, StopIteration
|
146
238
|
# break called or StopIteration raised
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
# Helper method for performing a `cancel_after` with optional reset.
|
244
|
+
#
|
245
|
+
# @param interval [Number] timeout interval in seconds
|
246
|
+
# @param exception [Exception, Class, Array<class, message>] exception spec
|
247
|
+
# @param &block [Proc] block to run
|
248
|
+
# @return [any] block's return value
|
249
|
+
def cancel_after_with_optional_reset(interval, exception, &block)
|
250
|
+
canceller = spin do
|
251
|
+
sleep interval
|
252
|
+
exception = cancel_exception(exception)
|
253
|
+
exception.raising_fiber = Fiber.current
|
254
|
+
fiber.cancel(exception).await
|
255
|
+
end
|
256
|
+
block.call(canceller)
|
147
257
|
ensure
|
148
|
-
|
258
|
+
canceller.stop
|
259
|
+
end
|
260
|
+
|
261
|
+
# Converts the given exception spec to an exception instance.
|
262
|
+
#
|
263
|
+
# @param exception [Exception, Class, Array<class, message>] exception spec
|
264
|
+
# @return [Exception] exception instance
|
265
|
+
def cancel_exception(exception)
|
266
|
+
case exception
|
267
|
+
when Class then exception.new
|
268
|
+
when Array then exception[0].new(exception[1])
|
269
|
+
else RuntimeError.new(exception)
|
270
|
+
end
|
149
271
|
end
|
272
|
+
|
273
|
+
# Helper method for performing `#spin_loop` without throttling. Spins up a
|
274
|
+
# new fiber in which to run the loop.
|
275
|
+
#
|
276
|
+
# @param tag [any] new fiber's tag
|
277
|
+
# @param caller [Array<String>] caller info
|
278
|
+
# @param block [Proc] code to run
|
279
|
+
# @return [void]
|
280
|
+
def spin_loop_without_throttling(tag, caller, block)
|
281
|
+
Fiber.current.spin(tag, caller) do
|
282
|
+
block.call while true
|
283
|
+
rescue LocalJumpError, StopIteration
|
284
|
+
# break called or StopIteration raised
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Helper method for performing `#move_on_after` with optional reset.
|
289
|
+
#
|
290
|
+
# @param interval [Number] timeout interval in seconds
|
291
|
+
# @param value [any] return value in case of timeout
|
292
|
+
# @param &block [Proc] code to run
|
293
|
+
# @return [any] return value of given block or timeout value
|
294
|
+
def move_on_after_with_optional_reset(interval, value, &block)
|
295
|
+
fiber = Fiber.current
|
296
|
+
canceller = spin do
|
297
|
+
sleep interval
|
298
|
+
fiber.move_on(value).await
|
299
|
+
end
|
300
|
+
block.call(canceller)
|
301
|
+
rescue Polyphony::MoveOn => e
|
302
|
+
e.value
|
303
|
+
ensure
|
304
|
+
canceller.stop
|
305
|
+
end
|
306
|
+
|
150
307
|
end
|
151
308
|
end
|
152
|
-
|
153
|
-
Object.include Polyphony::GlobalAPI
|
@@ -5,7 +5,8 @@ module Polyphony
|
|
5
5
|
class ResourcePool
|
6
6
|
attr_reader :limit, :size
|
7
7
|
|
8
|
-
# Initializes a new resource pool
|
8
|
+
# Initializes a new resource pool.
|
9
|
+
#
|
9
10
|
# @param opts [Hash] options
|
10
11
|
# @param &block [Proc] allocator block
|
11
12
|
def initialize(opts, &block)
|
@@ -16,10 +17,27 @@ module Polyphony
|
|
16
17
|
@acquired_resources = {}
|
17
18
|
end
|
18
19
|
|
20
|
+
# Returns number of currently available resources.
|
21
|
+
#
|
22
|
+
# @return [Integer] size of resource stock
|
19
23
|
def available
|
20
24
|
@stock.size
|
21
25
|
end
|
22
26
|
|
27
|
+
# Acquires a resource, passing it to the given block. If no resource is
|
28
|
+
# available, blocks until a resource becomes available. After the block has
|
29
|
+
# run, the resource is released back to the pool. The resource is passed to
|
30
|
+
# the block as its only argument.
|
31
|
+
#
|
32
|
+
# This method is re-entrant: if called from the same fiber, it will immediately
|
33
|
+
# return the resource currently acquired by the fiber.
|
34
|
+
#
|
35
|
+
# rows = db_pool.acquire do |db|
|
36
|
+
# db.query(sql).to_a
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @param &block [Proc] code to run
|
40
|
+
# @return [any] return value of block
|
23
41
|
def acquire(&block)
|
24
42
|
fiber = Fiber.current
|
25
43
|
return yield @acquired_resources[fiber] if @acquired_resources[fiber]
|
@@ -27,6 +45,50 @@ module Polyphony
|
|
27
45
|
acquire_from_stock(fiber, &block)
|
28
46
|
end
|
29
47
|
|
48
|
+
# Acquires a resource, proxies the method calls to the resource, then
|
49
|
+
# releases it. Methods can also be called with blocks, as in the following
|
50
|
+
# example:
|
51
|
+
#
|
52
|
+
# db_pool.query(sql) { |result|
|
53
|
+
# process_result_rows(result)
|
54
|
+
# }
|
55
|
+
#
|
56
|
+
# @param sym [Symbol] method name
|
57
|
+
# @param *args [Array<any>] method arguments
|
58
|
+
# @param &block [Proc] block passed to method
|
59
|
+
def method_missing(sym, *args, &block)
|
60
|
+
acquire { |r| r.send(sym, *args, &block) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# :no-doc:
|
64
|
+
def respond_to_missing?(*_args)
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Discards the currently-acquired resource
|
69
|
+
# instead of returning it to the pool when done.
|
70
|
+
#
|
71
|
+
# @return [Polyphony::ResourcePool] self
|
72
|
+
def discard!
|
73
|
+
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fills the pool to capacity.
|
78
|
+
#
|
79
|
+
# @return [Polyphony::ResourcePool] self
|
80
|
+
def fill!
|
81
|
+
add_to_stock while @size < @limit
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Acquires a resource from stock, yielding it to the given block.
|
88
|
+
#
|
89
|
+
# @param fiber [Fiber] the fiber the resource will be associated with
|
90
|
+
# @param &block [Proc] given block
|
91
|
+
# @return [any] return value of block
|
30
92
|
def acquire_from_stock(fiber)
|
31
93
|
add_to_stock if (@stock.empty? || @stock.pending?) && @size < @limit
|
32
94
|
resource = @stock.shift
|
@@ -39,30 +101,13 @@ module Polyphony
|
|
39
101
|
end
|
40
102
|
end
|
41
103
|
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
def respond_to_missing?(*_args)
|
47
|
-
true
|
48
|
-
end
|
49
|
-
|
50
|
-
# Allocates a resource
|
104
|
+
# Creates a resource, adding it to the stock.
|
105
|
+
#
|
51
106
|
# @return [any] allocated resource
|
52
107
|
def add_to_stock
|
53
108
|
@size += 1
|
54
109
|
resource = @allocator.call
|
55
110
|
@stock << resource
|
56
111
|
end
|
57
|
-
|
58
|
-
# Discards the currently-acquired resource
|
59
|
-
# instead of returning it to the pool when done.
|
60
|
-
def discard!
|
61
|
-
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
62
|
-
end
|
63
|
-
|
64
|
-
def preheat!
|
65
|
-
add_to_stock while @size < @limit
|
66
|
-
end
|
67
112
|
end
|
68
113
|
end
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -1,56 +1,94 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Polyphony
|
4
|
-
|
4
|
+
|
5
|
+
# Implements mutex lock for synchronizing access to a shared resource. This
|
6
|
+
# class replaces the stock `Thread::Mutex` class.
|
5
7
|
class Mutex
|
8
|
+
|
9
|
+
# Initializes a new mutex.
|
6
10
|
def initialize
|
7
11
|
@store = Queue.new
|
8
12
|
@store << :token
|
9
13
|
end
|
10
14
|
|
15
|
+
# Locks the mutex, runs the block, then unlocks it.
|
16
|
+
#
|
17
|
+
# This method is re-entrant. Recursive calls from the given block will not
|
18
|
+
# block.
|
19
|
+
#
|
20
|
+
# @param &block [Proc] code to run
|
21
|
+
# @return [any] return value of block
|
11
22
|
def synchronize(&block)
|
12
23
|
return yield if @holding_fiber == Fiber.current
|
13
24
|
|
14
25
|
synchronize_not_holding(&block)
|
15
26
|
end
|
16
27
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
yield
|
22
|
-
ensure
|
23
|
-
@holding_fiber = nil
|
24
|
-
@store << @token if @token
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
+
# Conditionally releases the mutex. This method is used by condition
|
29
|
+
# variables.
|
30
|
+
#
|
31
|
+
# @return [void]
|
28
32
|
def conditional_release
|
29
33
|
@store << @token
|
30
34
|
@token = nil
|
31
35
|
@holding_fiber = nil
|
32
36
|
end
|
33
37
|
|
38
|
+
# Conditionally reacquires the mutex. This method is used by condition
|
39
|
+
# variables.
|
40
|
+
#
|
41
|
+
# @return [void]
|
34
42
|
def conditional_reacquire
|
35
43
|
@token = @store.shift
|
36
44
|
@holding_fiber = Fiber.current
|
37
45
|
end
|
38
46
|
|
47
|
+
# Returns the fiber currently owning the mutex.
|
48
|
+
#
|
49
|
+
# @return [Fiber, nil] current owner or nil
|
39
50
|
def owned?
|
40
51
|
@holding_fiber == Fiber.current
|
41
52
|
end
|
42
53
|
|
54
|
+
# Returns a truthy value if the mutex is currently locked.
|
55
|
+
#
|
56
|
+
# @return [any] truthy if fiber is currently locked
|
43
57
|
def locked?
|
44
58
|
@holding_fiber
|
45
59
|
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Helper method for performing a `#synchronize` when not currently holding
|
64
|
+
# the mutex.
|
65
|
+
#
|
66
|
+
# @return [any] return value of given block.
|
67
|
+
def synchronize_not_holding
|
68
|
+
@token = @store.shift
|
69
|
+
begin
|
70
|
+
@holding_fiber = Fiber.current
|
71
|
+
yield
|
72
|
+
ensure
|
73
|
+
@holding_fiber = nil
|
74
|
+
@store << @token if @token
|
75
|
+
end
|
76
|
+
end
|
46
77
|
end
|
47
78
|
|
48
79
|
# Implements a fiber-aware ConditionVariable
|
49
80
|
class ConditionVariable
|
81
|
+
|
82
|
+
# Initializes the condition variable.
|
50
83
|
def initialize
|
51
84
|
@queue = Polyphony::Queue.new
|
52
85
|
end
|
53
86
|
|
87
|
+
# Waits for the condition variable to be signalled.
|
88
|
+
#
|
89
|
+
# @param mutex [Polyphony::Mutex] mutex to release while waiting for signal
|
90
|
+
# @param timeout [Number, nil] timeout in seconds (currently not implemented)
|
91
|
+
# @return [void]
|
54
92
|
def wait(mutex, _timeout = nil)
|
55
93
|
mutex.conditional_release
|
56
94
|
@queue << Fiber.current
|
@@ -58,11 +96,18 @@ module Polyphony
|
|
58
96
|
mutex.conditional_reacquire
|
59
97
|
end
|
60
98
|
|
99
|
+
# Signals the condition variable, causing the first fiber in the waiting
|
100
|
+
# queue to be resumed.
|
101
|
+
#
|
102
|
+
# @return [void]
|
61
103
|
def signal
|
62
104
|
fiber = @queue.shift
|
63
105
|
fiber.schedule
|
64
106
|
end
|
65
107
|
|
108
|
+
# Resumes all waiting fibers.
|
109
|
+
#
|
110
|
+
# @return [void]
|
66
111
|
def broadcast
|
67
112
|
while (fiber = @queue.shift)
|
68
113
|
fiber.schedule
|