polyphony 0.79 → 0.80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile.lock +2 -1
  4. data/examples/core/zlib_stream.rb +15 -0
  5. data/ext/polyphony/backend_common.c +2 -1
  6. data/ext/polyphony/backend_common.h +7 -2
  7. data/lib/polyphony/adapters/fs.rb +4 -0
  8. data/lib/polyphony/adapters/process.rb +14 -1
  9. data/lib/polyphony/adapters/redis.rb +28 -0
  10. data/lib/polyphony/adapters/sequel.rb +19 -1
  11. data/lib/polyphony/core/debug.rb +129 -72
  12. data/lib/polyphony/core/exceptions.rb +21 -6
  13. data/lib/polyphony/core/global_api.rb +228 -73
  14. data/lib/polyphony/core/resource_pool.rb +65 -20
  15. data/lib/polyphony/core/sync.rb +57 -12
  16. data/lib/polyphony/core/thread_pool.rb +42 -5
  17. data/lib/polyphony/core/throttler.rb +21 -5
  18. data/lib/polyphony/core/timer.rb +125 -1
  19. data/lib/polyphony/extensions/exception.rb +36 -6
  20. data/lib/polyphony/extensions/fiber.rb +238 -57
  21. data/lib/polyphony/extensions/io.rb +4 -2
  22. data/lib/polyphony/extensions/kernel.rb +9 -4
  23. data/lib/polyphony/extensions/object.rb +8 -0
  24. data/lib/polyphony/extensions/openssl.rb +3 -1
  25. data/lib/polyphony/extensions/socket.rb +458 -39
  26. data/lib/polyphony/extensions/thread.rb +108 -43
  27. data/lib/polyphony/extensions/timeout.rb +12 -1
  28. data/lib/polyphony/extensions.rb +1 -0
  29. data/lib/polyphony/net.rb +59 -0
  30. data/lib/polyphony/version.rb +1 -1
  31. data/lib/polyphony.rb +0 -2
  32. data/test/test_backend.rb +6 -2
  33. data/test/test_global_api.rb +0 -23
  34. data/test/test_resource_pool.rb +1 -1
  35. data/test/test_throttler.rb +0 -6
  36. data/test/test_trace.rb +87 -0
  37. metadata +9 -8
  38. 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