polyphony 0.47.2 → 0.48.0
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/CHANGELOG.md +317 -293
- data/Gemfile.lock +1 -1
- data/TODO.md +46 -3
- data/examples/core/supervisor.rb +3 -3
- data/examples/core/worker-thread.rb +3 -4
- data/examples/io/tcp_proxy.rb +32 -0
- data/examples/io/unix_socket.rb +26 -0
- data/examples/performance/line_splitting.rb +34 -0
- data/examples/performance/loop.rb +32 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -0
- data/ext/polyphony/backend_common.h +2 -2
- data/ext/polyphony/backend_io_uring.c +42 -83
- data/ext/polyphony/backend_libev.c +32 -77
- data/ext/polyphony/event.c +1 -1
- data/ext/polyphony/polyphony.c +0 -2
- data/ext/polyphony/polyphony.h +5 -4
- data/ext/polyphony/queue.c +2 -2
- data/ext/polyphony/thread.c +9 -28
- data/lib/polyphony/adapters/postgres.rb +3 -3
- data/lib/polyphony/core/global_api.rb +19 -16
- data/lib/polyphony/core/resource_pool.rb +1 -12
- data/lib/polyphony/core/thread_pool.rb +3 -1
- data/lib/polyphony/core/throttler.rb +1 -1
- data/lib/polyphony/extensions/fiber.rb +34 -8
- data/lib/polyphony/extensions/io.rb +8 -14
- data/lib/polyphony/extensions/openssl.rb +4 -4
- data/lib/polyphony/extensions/socket.rb +68 -16
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +1 -1
- data/test/helper.rb +1 -0
- data/test/test_backend.rb +1 -1
- data/test/test_fiber.rb +64 -0
- data/test/test_global_api.rb +41 -1
- data/test/test_io.rb +26 -0
- data/test/test_resource_pool.rb +0 -21
- data/test/test_signal.rb +18 -0
- data/test/test_socket.rb +27 -2
- data/test/test_supervise.rb +2 -1
- metadata +7 -4
- data/ext/polyphony/backend.h +0 -26
@@ -58,18 +58,7 @@ module Polyphony
|
|
58
58
|
# Discards the currently-acquired resource
|
59
59
|
# instead of returning it to the pool when done.
|
60
60
|
def discard!
|
61
|
-
if
|
62
|
-
@size.times do
|
63
|
-
acquire do |r|
|
64
|
-
next if yield(r)
|
65
|
-
|
66
|
-
@size -= 1
|
67
|
-
@acquired_resources.delete(Fiber.current)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
else
|
71
|
-
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
72
|
-
end
|
61
|
+
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
73
62
|
end
|
74
63
|
|
75
64
|
def preheat!
|
@@ -34,9 +34,18 @@ module Polyphony
|
|
34
34
|
schedule Polyphony::Cancel.new
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
37
|
+
def graceful_shutdown=(graceful)
|
38
|
+
@graceful_shutdown = graceful
|
39
|
+
end
|
40
|
+
|
41
|
+
def graceful_shutdown?
|
42
|
+
@graceful_shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
def terminate(graceful = false)
|
38
46
|
return if @running == false
|
39
47
|
|
48
|
+
@graceful_shutdown = graceful
|
40
49
|
schedule Polyphony::Terminate.new
|
41
50
|
end
|
42
51
|
|
@@ -66,20 +75,32 @@ module Polyphony
|
|
66
75
|
@on_child_done = proc do |fiber, result|
|
67
76
|
self << fiber unless result.is_a?(Exception)
|
68
77
|
end
|
69
|
-
|
78
|
+
while true
|
79
|
+
supervise_perform(opts)
|
80
|
+
end
|
81
|
+
rescue Polyphony::MoveOn
|
82
|
+
# generated in #supervise_perform to stop supervisor
|
70
83
|
ensure
|
71
84
|
@on_child_done = nil
|
72
85
|
end
|
73
86
|
|
74
87
|
def supervise_perform(opts)
|
75
88
|
fiber = receive
|
76
|
-
|
89
|
+
if fiber && opts[:restart]
|
90
|
+
restart_fiber(fiber, opts)
|
91
|
+
elsif Fiber.current.children.empty?
|
92
|
+
Fiber.current.stop
|
93
|
+
end
|
77
94
|
rescue Polyphony::Restart
|
78
95
|
restart_all_children
|
79
96
|
rescue Exception => e
|
80
97
|
Kernel.raise e if e.source_fiber.nil? || e.source_fiber == self
|
81
98
|
|
82
|
-
|
99
|
+
if opts[:restart]
|
100
|
+
restart_fiber(e.source_fiber, opts)
|
101
|
+
elsif Fiber.current.children.empty?
|
102
|
+
Fiber.current.stop
|
103
|
+
end
|
83
104
|
end
|
84
105
|
|
85
106
|
def restart_fiber(fiber, opts)
|
@@ -173,6 +194,7 @@ module Polyphony
|
|
173
194
|
# signals (see also the patched `Kernel#trap`)
|
174
195
|
def schedule_priority_oob_fiber(&block)
|
175
196
|
f = Fiber.new do
|
197
|
+
Fiber.current.setup_raw
|
176
198
|
block.call
|
177
199
|
rescue Exception => e
|
178
200
|
Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
|
@@ -199,11 +221,14 @@ module Polyphony
|
|
199
221
|
@on_child_done&.(child_fiber, result)
|
200
222
|
end
|
201
223
|
|
202
|
-
def terminate_all_children
|
224
|
+
def terminate_all_children(graceful = false)
|
203
225
|
return unless @children
|
204
226
|
|
205
227
|
e = Polyphony::Terminate.new
|
206
|
-
@children.each_key
|
228
|
+
@children.each_key do |c|
|
229
|
+
c.graceful_shutdown = true if graceful
|
230
|
+
c.raise e
|
231
|
+
end
|
207
232
|
end
|
208
233
|
|
209
234
|
def await_all_children
|
@@ -219,8 +244,8 @@ module Polyphony
|
|
219
244
|
results.values
|
220
245
|
end
|
221
246
|
|
222
|
-
def shutdown_all_children
|
223
|
-
terminate_all_children
|
247
|
+
def shutdown_all_children(graceful = false)
|
248
|
+
terminate_all_children(graceful)
|
224
249
|
await_all_children
|
225
250
|
end
|
226
251
|
end
|
@@ -262,6 +287,7 @@ module Polyphony
|
|
262
287
|
# allows the fiber to be scheduled and to receive messages.
|
263
288
|
def setup_raw
|
264
289
|
@thread = Thread.current
|
290
|
+
@running = true
|
265
291
|
end
|
266
292
|
|
267
293
|
def setup_main_fiber
|
@@ -119,18 +119,11 @@ class ::IO
|
|
119
119
|
end
|
120
120
|
|
121
121
|
alias_method :orig_readpartial, :read
|
122
|
-
def readpartial(len, str =
|
123
|
-
|
124
|
-
result = Thread.current.backend.read(self, @read_buffer, len, false)
|
122
|
+
def readpartial(len, str = +'')
|
123
|
+
result = Thread.current.backend.read(self, str, len, false)
|
125
124
|
raise EOFError unless result
|
126
125
|
|
127
|
-
|
128
|
-
str << @read_buffer
|
129
|
-
else
|
130
|
-
str = @read_buffer
|
131
|
-
end
|
132
|
-
@read_buffer = +''
|
133
|
-
str
|
126
|
+
result
|
134
127
|
end
|
135
128
|
|
136
129
|
alias_method :orig_write, :write
|
@@ -154,15 +147,16 @@ class ::IO
|
|
154
147
|
|
155
148
|
@read_buffer ||= +''
|
156
149
|
|
157
|
-
|
150
|
+
while true
|
158
151
|
idx = @read_buffer.index(sep)
|
159
152
|
return @read_buffer.slice!(0, idx + sep_size) if idx
|
160
153
|
|
161
|
-
data = readpartial(8192)
|
154
|
+
data = readpartial(8192, +'')
|
155
|
+
return nil unless data
|
162
156
|
@read_buffer << data
|
163
|
-
rescue EOFError
|
164
|
-
return nil
|
165
157
|
end
|
158
|
+
rescue EOFError
|
159
|
+
return nil
|
166
160
|
end
|
167
161
|
|
168
162
|
# def print(*args)
|
@@ -25,7 +25,7 @@ class ::OpenSSL::SSL::SSLSocket
|
|
25
25
|
|
26
26
|
alias_method :orig_accept, :accept
|
27
27
|
def accept
|
28
|
-
|
28
|
+
while true
|
29
29
|
result = accept_nonblock(exception: false)
|
30
30
|
case result
|
31
31
|
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
@@ -37,14 +37,14 @@ class ::OpenSSL::SSL::SSLSocket
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def accept_loop
|
40
|
-
|
40
|
+
while true
|
41
41
|
yield accept
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
45
|
alias_method :orig_sysread, :sysread
|
46
46
|
def sysread(maxlen, buf = +'')
|
47
|
-
|
47
|
+
while true
|
48
48
|
case (result = read_nonblock(maxlen, buf, exception: false))
|
49
49
|
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
50
50
|
when :wait_writable then Thread.current.backend.wait_io(io, true)
|
@@ -55,7 +55,7 @@ class ::OpenSSL::SSL::SSLSocket
|
|
55
55
|
|
56
56
|
alias_method :orig_syswrite, :syswrite
|
57
57
|
def syswrite(buf)
|
58
|
-
|
58
|
+
while true
|
59
59
|
case (result = write_nonblock(buf, exception: false))
|
60
60
|
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
61
61
|
when :wait_writable then Thread.current.backend.wait_io(io, true)
|
@@ -8,7 +8,11 @@ require_relative '../core/thread_pool'
|
|
8
8
|
# Socket overrides (eventually rewritten in C)
|
9
9
|
class ::Socket
|
10
10
|
def accept
|
11
|
-
Thread.current.backend.accept(self)
|
11
|
+
Thread.current.backend.accept(self, TCPSocket)
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept_loop(&block)
|
15
|
+
Thread.current.backend.accept_loop(self, TCPSocket, &block)
|
12
16
|
end
|
13
17
|
|
14
18
|
NO_EXCEPTION = { exception: false }.freeze
|
@@ -19,17 +23,7 @@ class ::Socket
|
|
19
23
|
end
|
20
24
|
|
21
25
|
def recv(maxlen, flags = 0, outbuf = nil)
|
22
|
-
Thread.current.backend.recv(self,
|
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
|
26
|
+
Thread.current.backend.recv(self, outbuf || +'', maxlen)
|
33
27
|
end
|
34
28
|
|
35
29
|
def recv_loop(&block)
|
@@ -39,7 +33,7 @@ class ::Socket
|
|
39
33
|
|
40
34
|
def recvfrom(maxlen, flags = 0)
|
41
35
|
@read_buffer ||= +''
|
42
|
-
|
36
|
+
while true
|
43
37
|
result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
|
44
38
|
case result
|
45
39
|
when nil then raise IOError
|
@@ -144,7 +138,7 @@ class ::TCPSocket
|
|
144
138
|
end
|
145
139
|
|
146
140
|
def recv(maxlen, flags = 0, outbuf = nil)
|
147
|
-
Thread.current.backend.recv(self,
|
141
|
+
Thread.current.backend.recv(self, outbuf || +'', maxlen)
|
148
142
|
end
|
149
143
|
|
150
144
|
def recv_loop(&block)
|
@@ -198,11 +192,12 @@ class ::TCPServer
|
|
198
192
|
|
199
193
|
alias_method :orig_accept, :accept
|
200
194
|
def accept
|
201
|
-
@io
|
195
|
+
Thread.current.backend.accept(@io, TCPSocket)
|
196
|
+
# @io.accept
|
202
197
|
end
|
203
198
|
|
204
199
|
def accept_loop(&block)
|
205
|
-
Thread.current.backend.accept_loop(@io, &block)
|
200
|
+
Thread.current.backend.accept_loop(@io, TCPSocket, &block)
|
206
201
|
end
|
207
202
|
|
208
203
|
alias_method :orig_close, :close
|
@@ -210,3 +205,60 @@ class ::TCPServer
|
|
210
205
|
@io.close
|
211
206
|
end
|
212
207
|
end
|
208
|
+
|
209
|
+
class ::UNIXServer
|
210
|
+
alias_method :orig_accept, :accept
|
211
|
+
def accept
|
212
|
+
Thread.current.backend.accept(self, UNIXSocket)
|
213
|
+
end
|
214
|
+
|
215
|
+
def accept_loop(&block)
|
216
|
+
Thread.current.backend.accept_loop(self, UNIXSocket, &block)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class ::UNIXSocket
|
221
|
+
def recv(maxlen, flags = 0, outbuf = nil)
|
222
|
+
Thread.current.backend.recv(self, outbuf || +'', maxlen)
|
223
|
+
end
|
224
|
+
|
225
|
+
def recv_loop(&block)
|
226
|
+
Thread.current.backend.recv_loop(self, &block)
|
227
|
+
end
|
228
|
+
alias_method :read_loop, :recv_loop
|
229
|
+
|
230
|
+
def send(mesg, flags = 0)
|
231
|
+
Thread.current.backend.send(self, mesg)
|
232
|
+
end
|
233
|
+
|
234
|
+
def write(str, *args)
|
235
|
+
if args.empty?
|
236
|
+
Thread.current.backend.send(self, str)
|
237
|
+
else
|
238
|
+
Thread.current.backend.send(self, str + args.join)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
alias_method :<<, :write
|
242
|
+
|
243
|
+
def readpartial(maxlen, str = nil)
|
244
|
+
@read_buffer ||= +''
|
245
|
+
result = Thread.current.backend.recv(self, @read_buffer, maxlen)
|
246
|
+
raise EOFError unless result
|
247
|
+
|
248
|
+
if str
|
249
|
+
str << @read_buffer
|
250
|
+
else
|
251
|
+
str = @read_buffer
|
252
|
+
end
|
253
|
+
@read_buffer = +''
|
254
|
+
str
|
255
|
+
end
|
256
|
+
|
257
|
+
def read_nonblock(len, str = nil, exception: true)
|
258
|
+
@io.read_nonblock(len, str, exception: exception)
|
259
|
+
end
|
260
|
+
|
261
|
+
def write_nonblock(buf, exception: true)
|
262
|
+
@io.write_nonblock(buf, exception: exception)
|
263
|
+
end
|
264
|
+
end
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.licenses = ['MIT']
|
7
7
|
s.summary = 'Fine grained concurrency for Ruby'
|
8
8
|
s.author = 'Sharon Rosner'
|
9
|
-
s.email = '
|
9
|
+
s.email = 'sharon@noteflakes.com'
|
10
10
|
s.files = `git ls-files`.split
|
11
11
|
s.homepage = 'https://digital-fabric.github.io/polyphony'
|
12
12
|
s.metadata = {
|
data/test/helper.rb
CHANGED
data/test/test_backend.rb
CHANGED
@@ -105,7 +105,7 @@ class BackendTest < MiniTest::Test
|
|
105
105
|
|
106
106
|
clients = []
|
107
107
|
server_fiber = spin do
|
108
|
-
@backend.accept_loop(server) { |c| clients << c }
|
108
|
+
@backend.accept_loop(server, TCPSocket) { |c| clients << c }
|
109
109
|
end
|
110
110
|
|
111
111
|
c1 = TCPSocket.new('127.0.0.1', 1234)
|
data/test/test_fiber.rb
CHANGED
@@ -1038,3 +1038,67 @@ class RestartTest < MiniTest::Test
|
|
1038
1038
|
assert_equal [f, 'foo', 'bar', :done, f2, 'baz', 42, :done], buffer
|
1039
1039
|
end
|
1040
1040
|
end
|
1041
|
+
|
1042
|
+
class GracefulTerminationTest < MiniTest::Test
|
1043
|
+
def test_graceful_termination
|
1044
|
+
buffer = []
|
1045
|
+
f = spin do
|
1046
|
+
buffer << 1
|
1047
|
+
snooze
|
1048
|
+
buffer << 2
|
1049
|
+
sleep 3
|
1050
|
+
buffer << 3
|
1051
|
+
ensure
|
1052
|
+
buffer << 4 if Fiber.current.graceful_shutdown?
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
3.times { snooze }
|
1056
|
+
f.terminate(false)
|
1057
|
+
f.await
|
1058
|
+
assert_equal [1, 2], buffer
|
1059
|
+
|
1060
|
+
buffer = []
|
1061
|
+
f = spin do
|
1062
|
+
buffer << 1
|
1063
|
+
snooze
|
1064
|
+
buffer << 2
|
1065
|
+
sleep 3
|
1066
|
+
buffer << 3
|
1067
|
+
ensure
|
1068
|
+
buffer << 4 if Fiber.current.graceful_shutdown?
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
3.times { snooze }
|
1072
|
+
f.terminate(true)
|
1073
|
+
f.await
|
1074
|
+
assert_equal [1, 2, 4], buffer
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def test_graceful_child_shutdown
|
1078
|
+
buffer = []
|
1079
|
+
f0 = spin do
|
1080
|
+
f1 = spin do
|
1081
|
+
sleep
|
1082
|
+
ensure
|
1083
|
+
buffer << 1 if Fiber.current.graceful_shutdown?
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
f2 = spin do
|
1087
|
+
sleep
|
1088
|
+
ensure
|
1089
|
+
buffer << 2 if Fiber.current.graceful_shutdown?
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
sleep
|
1093
|
+
ensure
|
1094
|
+
Fiber.current.terminate_all_children(true) if Fiber.current.graceful_shutdown?
|
1095
|
+
Fiber.current.await_all_children
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
3.times { snooze }
|
1099
|
+
f0.terminate(true)
|
1100
|
+
f0.await
|
1101
|
+
|
1102
|
+
assert_equal [1, 2], buffer
|
1103
|
+
end
|
1104
|
+
end
|
data/test/test_global_api.rb
CHANGED
@@ -210,7 +210,7 @@ class CancelAfterTest < MiniTest::Test
|
|
210
210
|
sleep 0.007
|
211
211
|
end
|
212
212
|
t1 = Time.now
|
213
|
-
assert_in_range 0.014..0.
|
213
|
+
assert_in_range 0.014..0.024, t1 - t0
|
214
214
|
end
|
215
215
|
|
216
216
|
class CustomException < Exception
|
@@ -297,6 +297,46 @@ class SpinLoopTest < MiniTest::Test
|
|
297
297
|
f.stop
|
298
298
|
assert_in_range 1..3, counter
|
299
299
|
end
|
300
|
+
|
301
|
+
def test_spin_loop_with_interval
|
302
|
+
buffer = []
|
303
|
+
counter = 0
|
304
|
+
t0 = Time.now
|
305
|
+
f = spin_loop(interval: 0.01) { buffer << (counter += 1) }
|
306
|
+
sleep 0.02
|
307
|
+
f.stop
|
308
|
+
assert_in_range 1..3, counter
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_spin_loop_break
|
312
|
+
i = 0
|
313
|
+
f = spin_loop do
|
314
|
+
i += 1
|
315
|
+
snooze
|
316
|
+
break if i >= 5
|
317
|
+
end
|
318
|
+
f.await
|
319
|
+
assert_equal 5, i
|
320
|
+
|
321
|
+
i = 0
|
322
|
+
f = spin_loop do
|
323
|
+
i += 1
|
324
|
+
snooze
|
325
|
+
raise StopIteration if i >= 5
|
326
|
+
end
|
327
|
+
f.await
|
328
|
+
assert_equal 5, i
|
329
|
+
end
|
330
|
+
|
331
|
+
def test_throttled_spin_loop_break
|
332
|
+
i = 0
|
333
|
+
f = spin_loop(rate: 100) do
|
334
|
+
i += 1
|
335
|
+
break if i >= 5
|
336
|
+
end
|
337
|
+
f.await
|
338
|
+
assert_equal 5, i
|
339
|
+
end
|
300
340
|
end
|
301
341
|
|
302
342
|
class SpinScopeTest < MiniTest::Test
|