polyphony 0.99.4 → 0.99.6

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -0
  3. data/.yardopts +0 -2
  4. data/README.md +1 -1
  5. data/docs/readme.md +1 -1
  6. data/docs/tutorial.md +2 -2
  7. data/examples/pipes/gzip_http_server.rb +2 -2
  8. data/examples/pipes/http_server.rb +1 -1
  9. data/examples/pipes/tcp_proxy.rb +1 -1
  10. data/ext/polyphony/backend_common.c +4 -4
  11. data/ext/polyphony/backend_io_uring.c +8 -8
  12. data/ext/polyphony/backend_libev.c +5 -5
  13. data/ext/polyphony/fiber.c +33 -42
  14. data/ext/polyphony/io_extensions.c +50 -37
  15. data/ext/polyphony/pipe.c +6 -20
  16. data/ext/polyphony/polyphony.c +72 -144
  17. data/ext/polyphony/queue.c +23 -63
  18. data/ext/polyphony/thread.c +4 -13
  19. data/lib/polyphony/adapters/process.rb +2 -5
  20. data/lib/polyphony/adapters/sequel.rb +2 -2
  21. data/lib/polyphony/core/debug.rb +1 -4
  22. data/lib/polyphony/core/exceptions.rb +1 -5
  23. data/lib/polyphony/core/resource_pool.rb +7 -8
  24. data/lib/polyphony/core/sync.rb +5 -8
  25. data/lib/polyphony/core/thread_pool.rb +3 -10
  26. data/lib/polyphony/core/throttler.rb +1 -5
  27. data/lib/polyphony/core/timer.rb +23 -30
  28. data/lib/polyphony/extensions/fiber.rb +513 -543
  29. data/lib/polyphony/extensions/io.rb +5 -14
  30. data/lib/polyphony/extensions/object.rb +283 -2
  31. data/lib/polyphony/extensions/openssl.rb +5 -26
  32. data/lib/polyphony/extensions/pipe.rb +6 -17
  33. data/lib/polyphony/extensions/socket.rb +24 -118
  34. data/lib/polyphony/extensions/thread.rb +3 -18
  35. data/lib/polyphony/extensions/timeout.rb +0 -1
  36. data/lib/polyphony/net.rb +5 -9
  37. data/lib/polyphony/version.rb +1 -1
  38. data/lib/polyphony.rb +2 -6
  39. data/test/test_io.rb +221 -221
  40. data/test/test_socket.rb +3 -3
  41. data/test/test_trace.rb +2 -2
  42. metadata +5 -9
  43. data/docs/index.md +0 -94
  44. data/docs/link_rewriter.rb +0 -17
  45. data/docs/main-concepts/index.md +0 -9
  46. data/lib/polyphony/core/global_api.rb +0 -309
  47. /data/{assets → docs/assets}/echo-fibers.svg +0 -0
  48. /data/{assets → docs/assets}/polyphony-logo.png +0 -0
  49. /data/{assets → docs/assets}/sleeping-fiber.svg +0 -0
@@ -108,7 +108,7 @@ class ::IO
108
108
  def double_splice(src, dest)
109
109
  Polyphony.backend_double_splice(src, dest)
110
110
  end
111
-
111
+
112
112
  # Tees data from the source to the desination.
113
113
  #
114
114
  # @param src [IO, Polyphony::Pipe] source to tee from
@@ -232,7 +232,7 @@ class ::IO
232
232
  Polyphony.backend_write(self, str)
233
233
  self
234
234
  end
235
-
235
+
236
236
  # @!visibility private
237
237
  alias_method :orig_gets, :gets
238
238
 
@@ -350,24 +350,16 @@ class ::IO
350
350
  buf ? readpartial(maxlen, buf) : readpartial(maxlen)
351
351
  end
352
352
 
353
- # call-seq:
354
- # io.read_loop { |data| ... }
355
- # io.read_loop(maxlen) { |data| ... }
356
- #
357
353
  # Reads up to `maxlen` bytes at a time in an infinite loop. Read data
358
354
  # will be passed to the given block.
359
355
  #
360
356
  # @param maxlen [Integer] maximum bytes to receive
361
- # @yield [String] handler block
362
- # @return [void]
357
+ # @yield [String] read data
358
+ # @return [IO] self
363
359
  def read_loop(maxlen = 8192, &block)
364
360
  Polyphony.backend_read_loop(self, maxlen, &block)
365
361
  end
366
362
 
367
- # call-seq:
368
- # io.feed_loop(receiver, method)
369
- # io.feed_loop(receiver, method) { |result| ... }
370
- #
371
363
  # Receives data from the io in an infinite loop, passing the data to the given
372
364
  # receiver using the given method. If a block is given, the result of the
373
365
  # method call to the receiver is passed to the block.
@@ -384,8 +376,7 @@ class ::IO
384
376
  #
385
377
  # @param receiver [any] receiver object
386
378
  # @param method [Symbol] method to call
387
- # @yield [any] block to handle result of method call to receiver
388
- # @return [void]
379
+ # @return [IO] self
389
380
  def feed_loop(receiver, method = :call, &block)
390
381
  Polyphony.backend_feed_loop(self, receiver, method, &block)
391
382
  end
@@ -1,8 +1,289 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../core/global_api'
3
+ require_relative '../core/throttler'
4
4
 
5
5
  # Object extensions (methods available to all objects / call sites)
6
6
  class ::Object
7
- include Polyphony::GlobalAPI
7
+ # Spins up a fiber that will run the given block after sleeping for the
8
+ # given delay.
9
+ #
10
+ # @param interval [Number] delay in seconds before running the given block
11
+ # @return [Fiber] spun fiber
12
+ def after(interval, &block)
13
+ spin do
14
+ sleep interval
15
+ block.()
16
+ end
17
+ end
18
+
19
+ # Runs the given block after setting up a cancellation timer for
20
+ # cancellation. If the cancellation timer elapses, the execution will be
21
+ # interrupted with an exception defaulting to `Polyphony::Cancel`.
22
+ #
23
+ # This method should be used when a timeout should cause an exception to be
24
+ # propagated down the call stack or up the fiber tree.
25
+ #
26
+ # Example of normal use:
27
+ #
28
+ # def read_from_io_with_timeout(io)
29
+ # cancel_after(10) { io.read }
30
+ # rescue Polyphony::Cancel
31
+ # nil
32
+ # end
33
+ #
34
+ # The timeout period can be reset by passing a block that takes a single
35
+ # argument. The block will be provided with the canceller fiber. To reset
36
+ # the timeout, use `Fiber#reset`, as shown in the following example:
37
+ #
38
+ # cancel_after(10) do |timeout|
39
+ # loop do
40
+ # msg = socket.gets
41
+ # timeout.reset
42
+ # handle_msg(msg)
43
+ # end
44
+ # end
45
+ #
46
+ # @overload cancel_after(interval)
47
+ # @param interval [Number] timout in seconds
48
+ # @yield [Fiber] timeout fiber
49
+ # @return [any] block's return value
50
+ # @overload cancel_after(interval, with_exception: exception)
51
+ # @param interval [Number] timout in seconds
52
+ # @param with_exception [Class, Exception] exception or exception class
53
+ # @yield [Fiber] timeout fiber
54
+ # @return [any] block's return value
55
+ # @overload cancel_after(interval, with_exception: [klass, message])
56
+ # @param interval [Number] timout in seconds
57
+ # @param with_exception [Array] array containing class and message to use as exception
58
+ # @yield [Fiber] timeout fiber
59
+ # @return [any] block's return value
60
+ def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
61
+ if block.arity > 0
62
+ cancel_after_with_optional_reset(interval, with_exception, &block)
63
+ else
64
+ Polyphony.backend_timeout(interval, with_exception, &block)
65
+ end
66
+ end
67
+
68
+ # Spins up a new fiber.
69
+ #
70
+ # @param tag [any] optional tag for the new fiber
71
+ # @return [Fiber] new fiber
72
+ def spin(tag = nil, &block)
73
+ Fiber.current.spin(tag, caller, &block)
74
+ end
75
+
76
+ # Spins up a new fiber, running the given block inside an infinite loop. If
77
+ # `rate:` or `interval:` parameters are given, the loop is throttled
78
+ # accordingly.
79
+ #
80
+ # @param tag [any] optional tag for the new fiber
81
+ # @param rate [Number, nil] loop rate (times per second)
82
+ # @param interval [Number, nil] interval between consecutive iterations in seconds
83
+ # @return [Fiber] new fiber
84
+ def spin_loop(tag = nil, rate: nil, interval: nil, &block)
85
+ if rate || interval
86
+ Fiber.current.spin(tag, caller) do
87
+ throttled_loop(rate: rate, interval: interval, &block)
88
+ end
89
+ else
90
+ spin_loop_without_throttling(tag, caller, block)
91
+ end
92
+ end
93
+
94
+ # Runs the given code, then waits for any child fibers of the current fibers
95
+ # to terminate.
96
+ #
97
+ # @return [any] given block's return value
98
+ def spin_scope(&block)
99
+ raise unless block
100
+
101
+ spin do
102
+ result = yield
103
+ Fiber.current.await_all_children
104
+ result
105
+ end.await
106
+ end
107
+
108
+ # Runs the given block in an infinite loop with a regular interval between
109
+ # consecutive iterations.
110
+ #
111
+ # @param interval [Number] interval between consecutive iterations in seconds
112
+ def every(interval, &block)
113
+ Polyphony.backend_timer_loop(interval, &block)
114
+ end
115
+
116
+ # Runs the given block after setting up a cancellation timer for
117
+ # cancellation. If the cancellation timer elapses, the execution will be
118
+ # interrupted with a `Polyphony::MoveOn` exception, which will be rescued,
119
+ # and with cause the operation to return the given value.
120
+ #
121
+ # This method should be used when a timeout is to be handled locally,
122
+ # without generating an exception that is to propagated down the call stack
123
+ # or up the fiber tree.
124
+ #
125
+ # Example of normal use:
126
+ #
127
+ # move_on_after(10) {
128
+ # sleep 60
129
+ # 42
130
+ # } #=> nil
131
+ #
132
+ # move_on_after(10, with_value: :oops) {
133
+ # sleep 60
134
+ # 42
135
+ # } #=> :oops
136
+ #
137
+ # The timeout period can be reset by passing a block that takes a single
138
+ # argument. The block will be provided with the canceller fiber. To reset
139
+ # the timeout, use `Fiber#reset`, as shown in the following example:
140
+ #
141
+ # move_on_after(10) do |timeout|
142
+ # loop do
143
+ # msg = socket.gets
144
+ # timeout.reset
145
+ # handle_msg(msg)
146
+ # end
147
+ # end
148
+ #
149
+ # @overload move_on_after(interval) { ... }
150
+ # @param interval [Number] timout in seconds
151
+ # @yield [Fiber] timeout fiber
152
+ # @return [any] block's return value
153
+ # @overload move_on_after(interval, with_value: value) { ... }
154
+ # @param interval [Number] timout in seconds
155
+ # @param with_value [any] return value in case of timeout
156
+ # @yield [Fiber] timeout fiber
157
+ # @return [any] block's return value
158
+ def move_on_after(interval, with_value: nil, &block)
159
+ if block.arity > 0
160
+ move_on_after_with_optional_reset(interval, with_value, &block)
161
+ else
162
+ Polyphony.backend_timeout(interval, nil, with_value, &block)
163
+ end
164
+ end
165
+
166
+ # Returns the first message from the current fiber's mailbox. If the mailbox
167
+ # is empty, blocks until a message is available.
168
+ #
169
+ # @return [any] received message
170
+ def receive
171
+ Fiber.current.receive
172
+ end
173
+
174
+ # Returns all messages currently pending on the current fiber's mailbox.
175
+ #
176
+ # @return [Array] array of received messages
177
+ def receive_all_pending
178
+ Fiber.current.receive_all_pending
179
+ end
180
+
181
+ # Supervises the current fiber's children. See `Fiber#supervise` for
182
+ # options.
183
+ #
184
+ # @param args [Array] positional parameters
185
+ # @param opts [Hash] named parameters
186
+ # @return [any]
187
+ def supervise(*args, **opts, &block)
188
+ Fiber.current.supervise(*args, **opts, &block)
189
+ end
190
+
191
+ # Sleeps for the given duration. If the duration is `nil`, sleeps
192
+ # indefinitely.
193
+ #
194
+ # @param duration [Number, nil] duration
195
+ # @return [any]
196
+ def sleep(duration = nil)
197
+ duration ?
198
+ Polyphony.backend_sleep(duration) : Polyphony.backend_wait_event(true)
199
+ end
200
+
201
+ # Starts a throttled loop with the given rate. If `count:` is given, the
202
+ # loop is run for the given number of times. Otherwise, the loop is
203
+ # infinite. The loop rate (times per second) can be given as the rate
204
+ # parameter. The throttling can also be controlled by providing an
205
+ # `interval:` or `rate:` named parameter.
206
+ #
207
+ # @param rate [Number, nil] loop rate (times per second)
208
+ # @option opts [Number] :rate loop rate (times per second)
209
+ # @option opts [Number] :interval loop interval in seconds
210
+ # @option opts [Number] :count number of iterations (nil for infinite)
211
+ # @return [any]
212
+ def throttled_loop(rate = nil, **opts, &block)
213
+ throttler = Polyphony::Throttler.new(rate || opts)
214
+ if opts[:count]
215
+ opts[:count].times { |_i| throttler.(&block) }
216
+ else
217
+ while true
218
+ throttler.(&block)
219
+ end
220
+ end
221
+ rescue LocalJumpError, StopIteration
222
+ # break called or StopIteration raised
223
+ end
224
+
225
+ private
226
+
227
+ # Helper method for performing a `cancel_after` with optional reset.
228
+ #
229
+ # @param interval [Number] timeout interval in seconds
230
+ # @param exception [Exception, Class, Array<class, message>] exception spec
231
+ # @return [any] block's return value
232
+ def cancel_after_with_optional_reset(interval, exception, &block)
233
+ fiber = Fiber.current
234
+ canceller = spin do
235
+ Polyphony.backend_sleep(interval)
236
+ exception = cancel_exception(exception)
237
+ exception.raising_fiber = Fiber.current
238
+ fiber.cancel(exception)
239
+ end
240
+ block.call(canceller)
241
+ ensure
242
+ canceller.stop
243
+ end
244
+
245
+ # Converts the given exception spec to an exception instance.
246
+ #
247
+ # @param exception [Exception, Class, Array<class, message>] exception spec
248
+ # @return [Exception] exception instance
249
+ def cancel_exception(exception)
250
+ case exception
251
+ when Class then exception.new
252
+ when Array then exception[0].new(exception[1])
253
+ else RuntimeError.new(exception)
254
+ end
255
+ end
256
+
257
+ # Helper method for performing `#spin_loop` without throttling. Spins up a
258
+ # new fiber in which to run the loop.
259
+ #
260
+ # @param tag [any] new fiber's tag
261
+ # @param caller [Array<String>] caller info
262
+ # @param block [Proc] code to run
263
+ # @return [any]
264
+ def spin_loop_without_throttling(tag, caller, block)
265
+ Fiber.current.spin(tag, caller) do
266
+ block.call while true
267
+ rescue LocalJumpError, StopIteration
268
+ # break called or StopIteration raised
269
+ end
270
+ end
271
+
272
+ # Helper method for performing `#move_on_after` with optional reset.
273
+ #
274
+ # @param interval [Number] timeout interval in seconds
275
+ # @param value [any] return value in case of timeout
276
+ # @return [any] return value of given block or timeout value
277
+ def move_on_after_with_optional_reset(interval, value, &block)
278
+ fiber = Fiber.current
279
+ canceller = spin do
280
+ sleep interval
281
+ fiber.move_on(value)
282
+ end
283
+ block.call(canceller)
284
+ rescue Polyphony::MoveOn => e
285
+ e.value
286
+ ensure
287
+ canceller.stop
288
+ end
8
289
  end
@@ -17,7 +17,6 @@ class ::OpenSSL::SSL::SSLSocket
17
17
  #
18
18
  # @param socket [TCPSocket] socket to wrap
19
19
  # @param context [OpenSSL::SSL::SSLContext] optional SSL context
20
- # @return [void]
21
20
  def initialize(socket, context = nil)
22
21
  socket = socket.respond_to?(:io) ? socket.io || socket : socket
23
22
  context ? orig_initialize(socket, context) : orig_initialize(socket)
@@ -89,12 +88,6 @@ class ::OpenSSL::SSL::SSLSocket
89
88
  # @!visibility private
90
89
  alias_method :orig_read, :read
91
90
 
92
- # call-seq:
93
- # socket.read -> string
94
- # socket.read(maxlen) -> string
95
- # socket.read(maxlen, buf) -> buf
96
- # socket.read(maxlen, buf, buf_pos) -> buf
97
- #
98
91
  # Reads from the socket. If `maxlen` is given, reads up to `maxlen` bytes from
99
92
  # the socket, otherwise reads to `EOF`. If `buf` is given, it is used as the
100
93
  # buffer to read into, otherwise a new string is allocated. If `buf_pos` is
@@ -123,12 +116,6 @@ class ::OpenSSL::SSL::SSLSocket
123
116
  buf
124
117
  end
125
118
 
126
- # call-seq:
127
- # socket.readpartial(maxlen) -> string
128
- # socket.readpartial(maxlen, buf) -> buf
129
- # socket.readpartial(maxlen, buf, buf_pos) -> buf
130
- # socket.readpartial(maxlen, buf, buf_pos, raise_on_eof) -> buf
131
- #
132
119
  # Reads up to `maxlen` from the socket. If `buf` is given, it is used as the
133
120
  # buffer to read into, otherwise a new string is allocated. If `buf_pos` is
134
121
  # given, reads into the given offset (in bytes) in the given buffer. If the
@@ -162,18 +149,12 @@ class ::OpenSSL::SSL::SSLSocket
162
149
  result
163
150
  end
164
151
 
165
- # call-seq:
166
- # socket.recv_loop { |data| ... }
167
- # socket.recv_loop(maxlen) { |data| ... }
168
- # socket.read_loop { |data| ... }
169
- # socket.read_loop(maxlen) { |data| ... }
170
- #
171
152
  # Receives up to `maxlen` bytes at a time in an infinite loop. Read buffers
172
153
  # will be passed to the given block.
173
154
  #
174
155
  # @param maxlen [Integer] maximum bytes to receive
175
- # @yield [String] handler block
176
- # @return [void]
156
+ # @yield [String] read data
157
+ # @return [OpenSSL::SSL::SSLSocket] self
177
158
  def read_loop(maxlen = 8192)
178
159
  while (data = sysread(maxlen))
179
160
  yield data
@@ -279,13 +260,11 @@ class ::OpenSSL::SSL::SSLServer
279
260
  orig_close
280
261
  end
281
262
 
282
- # call-seq:
283
- # socket.accept_loop { |conn| ... }
284
- #
285
263
  # Accepts incoming connections in an infinite loop.
286
264
  #
287
- # @yield [OpenSSL::SSL::SSLSocket] block receiving accepted sockets
288
- # @return [void]
265
+ # @param ignore_errors [boolean] whether to ignore IO and SSL errors
266
+ # @yield [OpenSSL::SSL::SSLSocket] accepted socket
267
+ # @return [OpenSSL::SSL::SSLServer] self
289
268
  def accept_loop(ignore_errors = true)
290
269
  loop do
291
270
  yield accept
@@ -72,7 +72,7 @@ class Polyphony::Pipe
72
72
  end
73
73
 
74
74
  # Writes to the pipe.
75
-
75
+
76
76
  # @param buf [String] data to write
77
77
  # @param args [any] further arguments to pass to Polyphony.backend_write
78
78
  # @return [Integer] bytes written
@@ -81,7 +81,7 @@ class Polyphony::Pipe
81
81
  end
82
82
 
83
83
  # Writes to the pipe.
84
-
84
+
85
85
  # @param buf [String] data to write
86
86
  # @return [Integer] bytes written
87
87
  def <<(buf)
@@ -89,12 +89,6 @@ class Polyphony::Pipe
89
89
  self
90
90
  end
91
91
 
92
- # call-seq:
93
- # pipe.gets(limit, chomp)
94
- # pipe.gets(separator, limit, chomp)
95
- #
96
- # Reads a single line from the pipe.
97
- #
98
92
  # @param sep [String] line separator
99
93
  # @param _limit [Integer, nil] line length limit
100
94
  # @param _chomp [boolean, nil] whether to chomp the read line
@@ -134,7 +128,7 @@ class Polyphony::Pipe
134
128
  LINEFEED_RE = /\n$/.freeze
135
129
 
136
130
  # Writes a line with line feed to the pipe.
137
- #
131
+ #
138
132
  # @param args [Array] zero or more lines
139
133
  def puts(*args)
140
134
  if args.empty?
@@ -183,16 +177,12 @@ class Polyphony::Pipe
183
177
  # Runs a read loop.
184
178
  #
185
179
  # @param maxlen [Integer] maximum bytes to read
186
- # @yield [String] read block
187
- # @return [void]
180
+ # @yield [String] read data
181
+ # @return [Polyphony::Pipe] self
188
182
  def read_loop(maxlen = 8192, &block)
189
183
  Polyphony.backend_read_loop(self, maxlen, &block)
190
184
  end
191
185
 
192
- # call-seq:
193
- # pipe.feed_loop(receiver, method)
194
- # pipe.feed_loop(receiver, method) { |result| ... }
195
- #
196
186
  # Receives data from the pipe in an infinite loop, passing the data to the
197
187
  # given receiver using the given method. If a block is given, the result of
198
188
  # the method call to the receiver is passed to the block.
@@ -209,8 +199,7 @@ class Polyphony::Pipe
209
199
  #
210
200
  # @param receiver [any] receiver object
211
201
  # @param method [Symbol] method to call
212
- # @yield [any] block to handle result of method call to receiver
213
- # @return [void]
202
+ # @return [Polyphony::Pipe] self
214
203
  def feed_loop(receiver, method = :call, &block)
215
204
  Polyphony.backend_feed_loop(self, receiver, method, &block)
216
205
  end