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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -0
- data/.gitmodules +0 -0
- data/CHANGELOG.md +35 -0
- data/Gemfile.lock +3 -3
- data/README.md +3 -3
- data/Rakefile +1 -1
- data/TODO.md +20 -14
- data/bin/test +4 -0
- data/examples/io/raw.rb +14 -0
- data/examples/io/reline.rb +18 -0
- data/examples/performance/fiber_transfer.rb +13 -4
- data/examples/performance/multi_snooze.rb +0 -1
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +8 -20
- data/ext/liburing/liburing.h +585 -0
- data/ext/liburing/liburing/README.md +4 -0
- data/ext/liburing/liburing/barrier.h +73 -0
- data/ext/liburing/liburing/compat.h +15 -0
- data/ext/liburing/liburing/io_uring.h +343 -0
- data/ext/liburing/queue.c +333 -0
- data/ext/liburing/register.c +187 -0
- data/ext/liburing/setup.c +210 -0
- data/ext/liburing/syscall.c +54 -0
- data/ext/liburing/syscall.h +18 -0
- data/ext/polyphony/backend.h +1 -15
- data/ext/polyphony/backend_common.h +120 -0
- data/ext/polyphony/backend_io_uring.c +919 -0
- data/ext/polyphony/backend_io_uring_context.c +73 -0
- data/ext/polyphony/backend_io_uring_context.h +52 -0
- data/ext/polyphony/{libev_backend.c → backend_libev.c} +241 -297
- data/ext/polyphony/event.c +1 -1
- data/ext/polyphony/extconf.rb +31 -13
- data/ext/polyphony/fiber.c +107 -28
- data/ext/polyphony/libev.c +4 -0
- data/ext/polyphony/libev.h +8 -2
- data/ext/polyphony/liburing.c +8 -0
- data/ext/polyphony/playground.c +51 -0
- data/ext/polyphony/polyphony.c +6 -6
- data/ext/polyphony/polyphony.h +34 -14
- data/ext/polyphony/polyphony_ext.c +12 -4
- data/ext/polyphony/queue.c +1 -1
- data/ext/polyphony/runqueue.c +102 -0
- data/ext/polyphony/runqueue_ring_buffer.c +85 -0
- data/ext/polyphony/runqueue_ring_buffer.h +31 -0
- data/ext/polyphony/thread.c +42 -90
- data/lib/polyphony.rb +2 -2
- data/lib/polyphony/adapters/process.rb +0 -3
- data/lib/polyphony/adapters/trace.rb +2 -2
- data/lib/polyphony/core/exceptions.rb +0 -4
- data/lib/polyphony/core/global_api.rb +13 -11
- data/lib/polyphony/core/sync.rb +7 -5
- data/lib/polyphony/extensions/core.rb +14 -33
- data/lib/polyphony/extensions/debug.rb +13 -0
- data/lib/polyphony/extensions/fiber.rb +21 -44
- data/lib/polyphony/extensions/io.rb +15 -4
- data/lib/polyphony/extensions/openssl.rb +6 -0
- data/lib/polyphony/extensions/socket.rb +63 -10
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +1 -1
- data/test/helper.rb +36 -4
- data/test/io_uring_test.rb +55 -0
- data/test/stress.rb +4 -1
- data/test/test_backend.rb +15 -6
- data/test/test_ext.rb +1 -2
- data/test/test_fiber.rb +31 -24
- data/test/test_global_api.rb +71 -31
- data/test/test_io.rb +42 -0
- data/test/test_queue.rb +1 -1
- data/test/test_signal.rb +11 -8
- data/test/test_socket.rb +2 -2
- data/test/test_sync.rb +21 -0
- data/test/test_throttler.rb +3 -7
- data/test/test_trace.rb +7 -5
- metadata +31 -6
data/lib/polyphony.rb
CHANGED
@@ -76,10 +76,10 @@ module Polyphony
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def install_terminating_signal_handlers
|
79
|
-
trap('SIGTERM'
|
79
|
+
trap('SIGTERM') { raise SystemExit }
|
80
80
|
orig_trap('SIGINT') do
|
81
81
|
orig_trap('SIGINT') { exit! }
|
82
|
-
|
82
|
+
Fiber.schedule_priority_oob_fiber { raise Interrupt }
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
@@ -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
|
-
|
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
|
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
|
@@ -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
|
-
|
30
|
-
|
31
|
-
|
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
|
96
|
-
Fiber.current.
|
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.
|
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)
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -16,11 +16,13 @@ module Polyphony
|
|
16
16
|
|
17
17
|
def synchronize_not_holding
|
18
18
|
@token = @store.shift
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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 @
|
26
|
-
@
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
157
|
-
#
|
158
|
-
#
|
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
|
-
|
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 =
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
212
|
+
results = @children.dup
|
229
213
|
@on_child_done = proc do |c, r|
|
230
|
-
|
214
|
+
results[c] = r
|
231
215
|
schedule if @children.empty?
|
232
216
|
end
|
233
217
|
suspend
|
234
218
|
@on_child_done = nil
|
235
|
-
|
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 =
|
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
|
328
|
-
|
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&.
|
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
|
-
|
94
|
-
|
93
|
+
alias_method :orig_getbyte, :getbyte
|
94
|
+
def getbyte
|
95
|
+
char = getc
|
96
|
+
char ? char.getbyte(0) : nil
|
97
|
+
end
|
95
98
|
|
96
|
-
|
97
|
-
|
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)
|
@@ -19,16 +19,21 @@ class ::Socket
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def recv(maxlen, flags = 0, outbuf = nil)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|