polyphony 0.45.1 → 0.46.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +35 -0
  5. data/Gemfile.lock +3 -3
  6. data/README.md +3 -3
  7. data/Rakefile +1 -1
  8. data/TODO.md +20 -14
  9. data/bin/test +4 -0
  10. data/examples/io/raw.rb +14 -0
  11. data/examples/io/reline.rb +18 -0
  12. data/examples/performance/fiber_transfer.rb +13 -4
  13. data/examples/performance/multi_snooze.rb +0 -1
  14. data/examples/performance/thread-vs-fiber/polyphony_server.rb +8 -20
  15. data/ext/liburing/liburing.h +585 -0
  16. data/ext/liburing/liburing/README.md +4 -0
  17. data/ext/liburing/liburing/barrier.h +73 -0
  18. data/ext/liburing/liburing/compat.h +15 -0
  19. data/ext/liburing/liburing/io_uring.h +343 -0
  20. data/ext/liburing/queue.c +333 -0
  21. data/ext/liburing/register.c +187 -0
  22. data/ext/liburing/setup.c +210 -0
  23. data/ext/liburing/syscall.c +54 -0
  24. data/ext/liburing/syscall.h +18 -0
  25. data/ext/polyphony/backend.h +1 -15
  26. data/ext/polyphony/backend_common.h +120 -0
  27. data/ext/polyphony/backend_io_uring.c +919 -0
  28. data/ext/polyphony/backend_io_uring_context.c +73 -0
  29. data/ext/polyphony/backend_io_uring_context.h +52 -0
  30. data/ext/polyphony/{libev_backend.c → backend_libev.c} +241 -297
  31. data/ext/polyphony/event.c +1 -1
  32. data/ext/polyphony/extconf.rb +31 -13
  33. data/ext/polyphony/fiber.c +107 -28
  34. data/ext/polyphony/libev.c +4 -0
  35. data/ext/polyphony/libev.h +8 -2
  36. data/ext/polyphony/liburing.c +8 -0
  37. data/ext/polyphony/playground.c +51 -0
  38. data/ext/polyphony/polyphony.c +6 -6
  39. data/ext/polyphony/polyphony.h +34 -14
  40. data/ext/polyphony/polyphony_ext.c +12 -4
  41. data/ext/polyphony/queue.c +1 -1
  42. data/ext/polyphony/runqueue.c +102 -0
  43. data/ext/polyphony/runqueue_ring_buffer.c +85 -0
  44. data/ext/polyphony/runqueue_ring_buffer.h +31 -0
  45. data/ext/polyphony/thread.c +42 -90
  46. data/lib/polyphony.rb +2 -2
  47. data/lib/polyphony/adapters/process.rb +0 -3
  48. data/lib/polyphony/adapters/trace.rb +2 -2
  49. data/lib/polyphony/core/exceptions.rb +0 -4
  50. data/lib/polyphony/core/global_api.rb +13 -11
  51. data/lib/polyphony/core/sync.rb +7 -5
  52. data/lib/polyphony/extensions/core.rb +14 -33
  53. data/lib/polyphony/extensions/debug.rb +13 -0
  54. data/lib/polyphony/extensions/fiber.rb +21 -44
  55. data/lib/polyphony/extensions/io.rb +15 -4
  56. data/lib/polyphony/extensions/openssl.rb +6 -0
  57. data/lib/polyphony/extensions/socket.rb +63 -10
  58. data/lib/polyphony/version.rb +1 -1
  59. data/polyphony.gemspec +1 -1
  60. data/test/helper.rb +36 -4
  61. data/test/io_uring_test.rb +55 -0
  62. data/test/stress.rb +4 -1
  63. data/test/test_backend.rb +15 -6
  64. data/test/test_ext.rb +1 -2
  65. data/test/test_fiber.rb +31 -24
  66. data/test/test_global_api.rb +71 -31
  67. data/test/test_io.rb +42 -0
  68. data/test/test_queue.rb +1 -1
  69. data/test/test_signal.rb +11 -8
  70. data/test/test_socket.rb +2 -2
  71. data/test/test_sync.rb +21 -0
  72. data/test/test_throttler.rb +3 -7
  73. data/test/test_trace.rb +7 -5
  74. metadata +31 -6
@@ -76,10 +76,10 @@ module Polyphony
76
76
  end
77
77
 
78
78
  def install_terminating_signal_handlers
79
- trap('SIGTERM', SystemExit)
79
+ trap('SIGTERM') { raise SystemExit }
80
80
  orig_trap('SIGINT') do
81
81
  orig_trap('SIGINT') { exit! }
82
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, Interrupt.new)
82
+ Fiber.schedule_priority_oob_fiber { raise Interrupt }
83
83
  end
84
84
  end
85
85
 
@@ -24,9 +24,6 @@ module Polyphony
24
24
  def kill_and_await(sig, pid)
25
25
  ::Process.kill(sig, pid)
26
26
  Thread.current.backend.waitpid(pid)
27
- rescue SystemCallError
28
- # ignore
29
- puts 'SystemCallError in kill_and_await'
30
27
  end
31
28
  end
32
29
  end
@@ -118,13 +118,13 @@ module Polyphony
118
118
 
119
119
  ALL_FIBER_EVENTS = %i[
120
120
  fiber_create fiber_terminate fiber_schedule fiber_switchpoint fiber_run
121
- fiber_ev_loop_enter fiber_ev_loop_leave
121
+ fiber_event_poll_enter fiber_event_poll_leave
122
122
  ].freeze
123
123
 
124
124
  def event_masks(events)
125
125
  events.each_with_object([[], []]) do |e, masks|
126
126
  case e
127
- when /fiber_/
127
+ when /^fiber_/
128
128
  masks[1] += e == :fiber_all ? ALL_FIBER_EVENTS : [e]
129
129
  masks[0] << :c_return unless masks[0].include?(:c_return)
130
130
  else
@@ -14,10 +14,6 @@ module Polyphony
14
14
  @caller_backtrace = caller
15
15
  @value = value
16
16
  end
17
-
18
- def backtrace
19
- sanitize(@caller_backtrace)
20
- end
21
17
  end
22
18
 
23
19
  # MoveOn is used to interrupt a long-running blocking operation, while
@@ -20,19 +20,24 @@ module Polyphony
20
20
  canceller = spin do
21
21
  sleep interval
22
22
  exception = cancel_exception(with_exception)
23
+ # we don't want the cancelling fiber caller location as part of the
24
+ # exception backtrace
25
+ exception.__raising_fiber__ = nil
23
26
  fiber.schedule exception
24
27
  end
25
28
  block ? cancel_after_wrap_block(canceller, &block) : canceller
26
29
  end
27
30
 
28
31
  def cancel_exception(exception)
29
- return exception.new if exception.is_a?(Class)
30
-
31
- RuntimeError.new(exception)
32
+ case exception
33
+ when Class then exception.new
34
+ when Array then exception[0].new(exception[1])
35
+ else RuntimeError.new(exception)
36
+ end
32
37
  end
33
38
 
34
39
  def cancel_after_wrap_block(canceller, &block)
35
- block.call
40
+ block.call(canceller)
36
41
  ensure
37
42
  canceller.stop
38
43
  end
@@ -81,7 +86,7 @@ module Polyphony
81
86
  sleep interval
82
87
  fiber.schedule Polyphony::MoveOn.new(with_value)
83
88
  end
84
- block.call
89
+ block.call(canceller)
85
90
  rescue Polyphony::MoveOn => e
86
91
  e.value
87
92
  ensure
@@ -92,8 +97,8 @@ module Polyphony
92
97
  Fiber.current.receive
93
98
  end
94
99
 
95
- def receive_pending
96
- Fiber.current.receive_pending
100
+ def receive_all_pending
101
+ Fiber.current.receive_all_pending
97
102
  end
98
103
 
99
104
  def supervise(*args, &block)
@@ -107,10 +112,7 @@ module Polyphony
107
112
  end
108
113
 
109
114
  def sleep_forever
110
- Thread.current.backend.ref
111
- loop { sleep 60 }
112
- ensure
113
- Thread.current.backend.unref
115
+ Thread.current.backend.wait_event(true)
114
116
  end
115
117
 
116
118
  def throttled_loop(rate = nil, **opts, &block)
@@ -16,11 +16,13 @@ module Polyphony
16
16
 
17
17
  def synchronize_not_holding
18
18
  @token = @store.shift
19
- @holding_fiber = Fiber.current
20
- yield
21
- ensure
22
- @holding_fiber = nil
23
- @store << @token if @token
19
+ begin
20
+ @holding_fiber = Fiber.current
21
+ yield
22
+ ensure
23
+ @holding_fiber = nil
24
+ @store << @token if @token
25
+ end
24
26
  end
25
27
 
26
28
  def conditional_release
@@ -12,7 +12,7 @@ class ::Exception
12
12
  attr_accessor :__disable_sanitized_backtrace__
13
13
  end
14
14
 
15
- attr_accessor :source_fiber
15
+ attr_accessor :source_fiber, :__raising_fiber__
16
16
 
17
17
  alias_method :orig_initialize, :initialize
18
18
  def initialize(*args)
@@ -22,8 +22,8 @@ class ::Exception
22
22
 
23
23
  alias_method :orig_backtrace, :backtrace
24
24
  def backtrace
25
- unless @first_backtrace_call
26
- @first_backtrace_call = true
25
+ unless @backtrace_called
26
+ @backtrace_called = true
27
27
  return orig_backtrace
28
28
  end
29
29
 
@@ -31,12 +31,10 @@ class ::Exception
31
31
  end
32
32
 
33
33
  def sanitized_backtrace
34
- if @__raising_fiber__
35
- backtrace = orig_backtrace || []
36
- sanitize(backtrace + @__raising_fiber__.caller)
37
- else
38
- sanitize(orig_backtrace)
39
- end
34
+ return sanitize(orig_backtrace) unless @__raising_fiber__
35
+
36
+ backtrace = orig_backtrace || []
37
+ sanitize(backtrace + @__raising_fiber__.caller)
40
38
  end
41
39
 
42
40
  POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
@@ -149,39 +147,22 @@ module ::Kernel
149
147
  return orig_trap(sig, command) if command.is_a? String
150
148
 
151
149
  block = command if !block && command.respond_to?(:call)
152
- exception = signal_exception(block, command)
153
150
 
154
151
  # The signal trap can be invoked at any time, including while the system
155
152
  # backend is blocking while polling for events. In order to deal with this
156
- # correctly, we spin a fiber that will run the signal handler code, then
157
- # call break_out_of_ev_loop, which will put the fiber at the front of the
158
- # run queue, then wake up the backend.
159
- #
160
- # If the command argument is an exception class however, it will be raised
161
- # directly in the context of the main fiber.
153
+ # correctly, we run the signal handler code in an out-of-band, priority
154
+ # scheduled fiber, that will pass any uncaught exception (including
155
+ # SystemExit and Interrupt) to the main thread's main fiber. See also
156
+ # `Fiber#schedule_priority_oob_fiber`.
162
157
  orig_trap(sig) do
163
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
158
+ Fiber.schedule_priority_oob_fiber(&block)
164
159
  end
165
160
  end
166
161
  end
167
162
 
168
- def signal_exception(block, command)
169
- if block
170
- Polyphony::Interjection.new(block)
171
- elsif command.is_a?(Class)
172
- command.new
173
- else
174
- raise ArgumentError, 'Must supply block or exception or callable object'
175
- end
176
- end
177
-
178
163
  # Override Timeout to use cancel scope
179
164
  module ::Timeout
180
- def self.timeout(sec, klass = nil, message = nil, &block)
181
- cancel_after(sec, &block)
182
- rescue Polyphony::Cancel => e
183
- error = klass ? klass.new(message) : ::Timeout::Error.new
184
- error.set_backtrace(e.backtrace)
185
- raise error
165
+ def self.timeout(sec, klass = Timeout::Error, message = 'execution expired', &block)
166
+ cancel_after(sec, with_exception: [klass, message], &block)
186
167
  end
187
168
  end
@@ -0,0 +1,13 @@
1
+ module ::Kernel
2
+ def trace(*args)
3
+ STDOUT.orig_write(format_trace(args))
4
+ end
5
+
6
+ def format_trace(args)
7
+ if args.size > 1 && args.first.is_a?(String)
8
+ format("%s: %p\n", args.shift, args.size == 1 ? args.first : args)
9
+ else
10
+ format("%p\n", args.size == 1 ? args.first : args)
11
+ end
12
+ end
13
+ end
@@ -7,20 +7,6 @@ require_relative '../core/exceptions'
7
7
  module Polyphony
8
8
  # Fiber control API
9
9
  module FiberControl
10
- def await
11
- if @running == false
12
- return @result.is_a?(Exception) ? (Kernel.raise @result) : @result
13
- end
14
-
15
- fiber = Fiber.current
16
- @waiting_fibers ||= {}
17
- @waiting_fibers[fiber] = true
18
- suspend
19
- ensure
20
- @waiting_fibers&.delete(fiber)
21
- end
22
- alias_method :join, :await
23
-
24
10
  def interrupt(value = nil)
25
11
  return if @running == false
26
12
 
@@ -117,7 +103,7 @@ module Polyphony
117
103
  suspend
118
104
  fibers.map(&:result)
119
105
  ensure
120
- await_select_cleanup(state)
106
+ await_select_cleanup(state) if state
121
107
  end
122
108
  alias_method :join, :await
123
109
 
@@ -179,21 +165,19 @@ module Polyphony
179
165
  state[:selected] = true
180
166
  end
181
167
  end
182
- end
183
168
 
184
- # Messaging functionality
185
- module FiberMessaging
186
- def <<(value)
187
- @mailbox << value
188
- end
189
- alias_method :send, :<<
190
-
191
- def receive
192
- @mailbox.shift
193
- end
194
-
195
- def receive_pending
196
- @mailbox.shift_all
169
+ # Creates and schedules with priority an out-of-band fiber that runs the
170
+ # supplied block. If any uncaught exception is raised while the fiber is
171
+ # running, it will bubble up to the main thread's main fiber, which will
172
+ # also be scheduled with priority. This method is mainly used trapping
173
+ # signals (see also the patched `Kernel#trap`)
174
+ def schedule_priority_oob_fiber(&block)
175
+ f = Fiber.new do
176
+ block.call
177
+ rescue Exception => e
178
+ Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
179
+ end
180
+ Thread.current.schedule_and_wakeup(f, nil)
197
181
  end
198
182
  end
199
183
 
@@ -225,14 +209,14 @@ module Polyphony
225
209
  def await_all_children
226
210
  return unless @children && !@children.empty?
227
211
 
228
- @results = @children.dup
212
+ results = @children.dup
229
213
  @on_child_done = proc do |c, r|
230
- @results[c] = r
214
+ results[c] = r
231
215
  schedule if @children.empty?
232
216
  end
233
217
  suspend
234
218
  @on_child_done = nil
235
- @results.values
219
+ results.values
236
220
  end
237
221
 
238
222
  def shutdown_all_children
@@ -249,7 +233,6 @@ module Polyphony
249
233
  @parent = parent
250
234
  @caller = caller
251
235
  @block = block
252
- @mailbox = Polyphony::Queue.new
253
236
  __fiber_trace__(:fiber_create, self)
254
237
  schedule
255
238
  end
@@ -279,7 +262,6 @@ module Polyphony
279
262
  # allows the fiber to be scheduled and to receive messages.
280
263
  def setup_raw
281
264
  @thread = Thread.current
282
- @mailbox = Polyphony::Queue.new
283
265
  end
284
266
 
285
267
  def setup_main_fiber
@@ -288,11 +270,10 @@ module Polyphony
288
270
  @thread = Thread.current
289
271
  @running = true
290
272
  @children&.clear
291
- @mailbox = Polyphony::Queue.new
292
273
  end
293
274
 
294
275
  def restart_self(first_value)
295
- @mailbox = Polyphony::Queue.new
276
+ @mailbox = nil
296
277
  @when_done_procs = nil
297
278
  @waiting_fibers = nil
298
279
  run(first_value)
@@ -324,13 +305,10 @@ module Polyphony
324
305
  def inform_dependants(result, uncaught_exception)
325
306
  @parent&.child_done(self, result)
326
307
  @when_done_procs&.each { |p| p.(result) }
327
- @waiting_fibers&.each_key do |f|
328
- f.schedule(result)
329
- end
330
- return unless uncaught_exception && !@waiting_fibers
331
-
308
+ @waiting_fibers&.each_key { |f| f.schedule(result) }
309
+
332
310
  # propagate unaught exception to parent
333
- @parent&.schedule(result)
311
+ @parent&.schedule_with_priority(result) if uncaught_exception && !@waiting_fibers
334
312
  end
335
313
 
336
314
  def when_done(&block)
@@ -344,14 +322,13 @@ end
344
322
  class ::Fiber
345
323
  prepend Polyphony::FiberControl
346
324
  include Polyphony::FiberSupervision
347
- include Polyphony::FiberMessaging
348
325
  include Polyphony::ChildFiberControl
349
326
  include Polyphony::FiberLifeCycle
350
327
 
351
328
  extend Polyphony::FiberControlClassMethods
352
329
 
353
330
  attr_accessor :tag, :thread, :parent
354
- attr_reader :result
331
+ attr_reader :result, :mailbox
355
332
 
356
333
  def running?
357
334
  @running
@@ -90,11 +90,22 @@ class ::IO
90
90
  # def each_codepoint
91
91
  # end
92
92
 
93
- # def getbyte
94
- # end
93
+ alias_method :orig_getbyte, :getbyte
94
+ def getbyte
95
+ char = getc
96
+ char ? char.getbyte(0) : nil
97
+ end
95
98
 
96
- # def getc
97
- # end
99
+ alias_method :orig_getc, :getc
100
+ def getc
101
+ return @read_buffer.slice!(0) if @read_buffer && !@read_buffer.empty?
102
+
103
+ @read_buffer ||= +''
104
+ Thread.current.backend.read(self, @read_buffer, 8192, false)
105
+ return @read_buffer.slice!(0) if !@read_buffer.empty?
106
+
107
+ nil
108
+ end
98
109
 
99
110
  alias_method :orig_read, :read
100
111
  def read(len = nil)
@@ -36,6 +36,12 @@ class ::OpenSSL::SSL::SSLSocket
36
36
  end
37
37
  end
38
38
 
39
+ def accept_loop
40
+ loop do
41
+ yield accept
42
+ end
43
+ end
44
+
39
45
  alias_method :orig_sysread, :sysread
40
46
  def sysread(maxlen, buf = +'')
41
47
  loop do
@@ -19,16 +19,21 @@ class ::Socket
19
19
  end
20
20
 
21
21
  def recv(maxlen, flags = 0, outbuf = nil)
22
- outbuf ||= +''
23
- loop do
24
- result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
25
- case result
26
- when nil then raise IOError
27
- when :wait_readable then Thread.current.backend.wait_io(self, false)
28
- else
29
- return result
30
- end
31
- end
22
+ Thread.current.backend.recv(self, buf || +'', maxlen)
23
+ # outbuf ||= +''
24
+ # loop do
25
+ # result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
26
+ # case result
27
+ # when nil then raise IOError
28
+ # when :wait_readable then Thread.current.backend.wait_io(self, false)
29
+ # else
30
+ # return result
31
+ # end
32
+ # end
33
+ end
34
+
35
+ def recv_loop(&block)
36
+ Thread.current.backend.recv_loop(self, &block)
32
37
  end
33
38
 
34
39
  def recvfrom(maxlen, flags = 0)
@@ -44,6 +49,19 @@ class ::Socket
44
49
  end
45
50
  end
46
51
 
52
+ def send(mesg, flags = 0)
53
+ Thread.current.backend.send(self, mesg)
54
+ end
55
+
56
+ def write(str)
57
+ Thread.current.backend.send(self, str)
58
+ end
59
+ alias_method :<<, :write
60
+
61
+ def readpartial(maxlen, str = +'')
62
+ Thread.current.backend.recv(self, str, maxlen)
63
+ end
64
+
47
65
  ZERO_LINGER = [0, 0].pack('ii').freeze
48
66
 
49
67
  def dont_linger
@@ -120,6 +138,37 @@ class ::TCPSocket
120
138
  setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
121
139
  end
122
140
 
141
+ def recv(maxlen, flags = 0, outbuf = nil)
142
+ Thread.current.backend.recv(self, buf || +'', maxlen)
143
+ end
144
+
145
+ def recv_loop(&block)
146
+ Thread.current.backend.recv_loop(self, &block)
147
+ end
148
+
149
+ def send(mesg, flags = 0)
150
+ Thread.current.backend.send(self, mesg)
151
+ end
152
+
153
+ def write(str)
154
+ Thread.current.backend.send(self, str)
155
+ end
156
+ alias_method :<<, :write
157
+
158
+ def readpartial(maxlen, str = nil)
159
+ @read_buffer ||= +''
160
+ result = Thread.current.backend.recv(self, @read_buffer, maxlen)
161
+ raise EOFError unless result
162
+
163
+ if str
164
+ str << @read_buffer
165
+ else
166
+ str = @read_buffer
167
+ end
168
+ @read_buffer = +''
169
+ str
170
+ end
171
+
123
172
  def read_nonblock(len, str = nil, exception: true)
124
173
  @io.read_nonblock(len, str, exception: exception)
125
174
  end
@@ -142,6 +191,10 @@ class ::TCPServer
142
191
  @io.accept
143
192
  end
144
193
 
194
+ def accept_loop(&block)
195
+ Thread.current.backend.accept_loop(@io, &block)
196
+ end
197
+
145
198
  alias_method :orig_close, :close
146
199
  def close
147
200
  @io.close