polyphony 0.77 → 0.80

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile.lock +2 -1
  5. data/examples/core/pingpong.rb +7 -4
  6. data/examples/core/zlib_stream.rb +15 -0
  7. data/ext/polyphony/backend_common.c +16 -8
  8. data/ext/polyphony/backend_common.h +8 -3
  9. data/ext/polyphony/backend_io_uring.c +19 -3
  10. data/ext/polyphony/backend_libev.c +33 -17
  11. data/ext/polyphony/fiber.c +28 -28
  12. data/ext/polyphony/polyphony.c +1 -8
  13. data/ext/polyphony/polyphony.h +11 -8
  14. data/ext/polyphony/queue.c +82 -6
  15. data/ext/polyphony/thread.c +6 -2
  16. data/lib/polyphony/adapters/fs.rb +4 -0
  17. data/lib/polyphony/adapters/process.rb +14 -1
  18. data/lib/polyphony/adapters/redis.rb +28 -0
  19. data/lib/polyphony/adapters/sequel.rb +19 -1
  20. data/lib/polyphony/core/debug.rb +203 -0
  21. data/lib/polyphony/core/exceptions.rb +21 -6
  22. data/lib/polyphony/core/global_api.rb +228 -73
  23. data/lib/polyphony/core/resource_pool.rb +65 -20
  24. data/lib/polyphony/core/sync.rb +57 -12
  25. data/lib/polyphony/core/thread_pool.rb +42 -5
  26. data/lib/polyphony/core/throttler.rb +21 -5
  27. data/lib/polyphony/core/timer.rb +125 -1
  28. data/lib/polyphony/extensions/exception.rb +36 -6
  29. data/lib/polyphony/extensions/fiber.rb +244 -61
  30. data/lib/polyphony/extensions/io.rb +4 -2
  31. data/lib/polyphony/extensions/kernel.rb +9 -4
  32. data/lib/polyphony/extensions/object.rb +8 -0
  33. data/lib/polyphony/extensions/openssl.rb +3 -1
  34. data/lib/polyphony/extensions/socket.rb +458 -39
  35. data/lib/polyphony/extensions/thread.rb +108 -43
  36. data/lib/polyphony/extensions/timeout.rb +12 -1
  37. data/lib/polyphony/extensions.rb +1 -0
  38. data/lib/polyphony/net.rb +59 -0
  39. data/lib/polyphony/version.rb +1 -1
  40. data/lib/polyphony.rb +0 -2
  41. data/test/test_backend.rb +6 -2
  42. data/test/test_global_api.rb +0 -23
  43. data/test/test_io.rb +7 -7
  44. data/test/test_queue.rb +103 -1
  45. data/test/test_resource_pool.rb +1 -1
  46. data/test/test_signal.rb +15 -15
  47. data/test/test_supervise.rb +27 -0
  48. data/test/test_thread.rb +1 -1
  49. data/test/test_throttler.rb +0 -6
  50. data/test/test_trace.rb +189 -24
  51. metadata +9 -8
  52. data/lib/polyphony/core/channel.rb +0 -15
@@ -3,8 +3,16 @@
3
3
  require_relative './throttler'
4
4
 
5
5
  module Polyphony
6
- # Global API methods to be included in ::Object
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 !block
17
- cancel_after_blockless_canceller(Fiber.current, interval, with_exception)
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
- def cancel_after_blockless_canceller(fiber, interval, with_exception)
26
- spin do
27
- sleep interval
28
- exception = cancel_exception(with_exception)
29
- exception.raising_fiber = nil
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
- spin_looped_block(tag, caller, block)
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
- def spin_scope
72
- raise unless block_given?
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 !block
87
- move_on_blockless_canceller(Fiber.current, interval, with_value)
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
- def move_on_blockless_canceller(fiber, interval, with_value)
96
- spin do
97
- sleep interval
98
- fiber.schedule with_value
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
- return sleep_forever unless duration
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
- throttler&.stop
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
- def method_missing(sym, *args, &block)
43
- acquire { |r| r.send(sym, *args, &block) }
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
@@ -1,56 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- # Implements mutex lock for synchronizing access to a shared resource
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
- def synchronize_not_holding
18
- @token = @store.shift
19
- begin
20
- @holding_fiber = Fiber.current
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