polyphony 0.45.1 → 0.46.1

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