polyphony 0.99.4 → 0.99.6

Sign up to get free protection for your applications and to get access to all the features.
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