polyphony 0.47.4 → 0.49.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/CHANGELOG.md +321 -296
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/TODO.md +38 -29
- 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/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 -22
- data/ext/polyphony/backend_io_uring.c +38 -78
- data/ext/polyphony/backend_libev.c +22 -67
- 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.rb +2 -1
- data/lib/polyphony/adapters/postgres.rb +3 -3
- data/lib/polyphony/adapters/process.rb +2 -0
- data/lib/polyphony/core/global_api.rb +14 -2
- data/lib/polyphony/core/thread_pool.rb +3 -1
- data/lib/polyphony/core/throttler.rb +1 -1
- data/lib/polyphony/core/timer.rb +72 -0
- data/lib/polyphony/extensions/fiber.rb +32 -8
- data/lib/polyphony/extensions/io.rb +8 -14
- data/lib/polyphony/extensions/openssl.rb +4 -4
- data/lib/polyphony/extensions/socket.rb +13 -10
- data/lib/polyphony/net.rb +3 -6
- 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 -1
- data/test/test_global_api.rb +30 -0
- data/test/test_io.rb +26 -0
- data/test/test_socket.rb +32 -6
- data/test/test_supervise.rb +2 -1
- data/test/test_timer.rb +124 -0
- metadata +8 -4
- data/ext/polyphony/backend.h +0 -26
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Implements a common timer for running multiple timeouts
|
5
|
+
class Timer
|
6
|
+
def initialize(resolution:)
|
7
|
+
@fiber = spin_loop(interval: resolution) { update }
|
8
|
+
@timeouts = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop
|
12
|
+
@fiber.stop
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel_after(duration, with_exception: Polyphony::Cancel)
|
16
|
+
fiber = Fiber.current
|
17
|
+
@timeouts[fiber] = {
|
18
|
+
duration: duration,
|
19
|
+
target_stamp: Time.now + duration,
|
20
|
+
exception: with_exception
|
21
|
+
}
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
@timeouts.delete(fiber)
|
25
|
+
end
|
26
|
+
|
27
|
+
def move_on_after(duration, with_value: nil)
|
28
|
+
fiber = Fiber.current
|
29
|
+
@timeouts[fiber] = {
|
30
|
+
duration: duration,
|
31
|
+
target_stamp: Time.now + duration,
|
32
|
+
value: with_value
|
33
|
+
}
|
34
|
+
yield
|
35
|
+
rescue Polyphony::MoveOn => e
|
36
|
+
e.value
|
37
|
+
ensure
|
38
|
+
@timeouts.delete(fiber)
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset
|
42
|
+
record = @timeouts[Fiber.current]
|
43
|
+
return unless record
|
44
|
+
|
45
|
+
record[:target_stamp] = Time.now + record[:duration]
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def timeout_exception(record)
|
51
|
+
case (exception = record[:exception])
|
52
|
+
when Class then exception.new
|
53
|
+
when Array then exception[0].new(exception[1])
|
54
|
+
when nil then Polyphony::MoveOn.new(record[:value])
|
55
|
+
else RuntimeError.new(exception)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def update
|
60
|
+
now = Time.now
|
61
|
+
# elapsed = nil
|
62
|
+
@timeouts.each do |fiber, record|
|
63
|
+
next if record[:target_stamp] > now
|
64
|
+
|
65
|
+
exception = timeout_exception(record)
|
66
|
+
# (elapsed ||= []) << fiber
|
67
|
+
fiber.schedule exception
|
68
|
+
end
|
69
|
+
# elapsed&.each { |f| @timeouts.delete(f) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -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)
|
@@ -200,11 +221,14 @@ module Polyphony
|
|
200
221
|
@on_child_done&.(child_fiber, result)
|
201
222
|
end
|
202
223
|
|
203
|
-
def terminate_all_children
|
224
|
+
def terminate_all_children(graceful = false)
|
204
225
|
return unless @children
|
205
226
|
|
206
227
|
e = Polyphony::Terminate.new
|
207
|
-
@children.each_key
|
228
|
+
@children.each_key do |c|
|
229
|
+
c.graceful_shutdown = true if graceful
|
230
|
+
c.raise e
|
231
|
+
end
|
208
232
|
end
|
209
233
|
|
210
234
|
def await_all_children
|
@@ -220,8 +244,8 @@ module Polyphony
|
|
220
244
|
results.values
|
221
245
|
end
|
222
246
|
|
223
|
-
def shutdown_all_children
|
224
|
-
terminate_all_children
|
247
|
+
def shutdown_all_children(graceful = false)
|
248
|
+
terminate_all_children(graceful)
|
225
249
|
await_all_children
|
226
250
|
end
|
227
251
|
end
|
@@ -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
|
@@ -29,7 +33,7 @@ class ::Socket
|
|
29
33
|
|
30
34
|
def recvfrom(maxlen, flags = 0)
|
31
35
|
@read_buffer ||= +''
|
32
|
-
|
36
|
+
while true
|
33
37
|
result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
|
34
38
|
case result
|
35
39
|
when nil then raise IOError
|
@@ -134,7 +138,7 @@ class ::TCPSocket
|
|
134
138
|
end
|
135
139
|
|
136
140
|
def recv(maxlen, flags = 0, outbuf = nil)
|
137
|
-
Thread.current.backend.recv(self,
|
141
|
+
Thread.current.backend.recv(self, outbuf || +'', maxlen)
|
138
142
|
end
|
139
143
|
|
140
144
|
def recv_loop(&block)
|
@@ -188,11 +192,12 @@ class ::TCPServer
|
|
188
192
|
|
189
193
|
alias_method :orig_accept, :accept
|
190
194
|
def accept
|
191
|
-
@io
|
195
|
+
Thread.current.backend.accept(@io, TCPSocket)
|
196
|
+
# @io.accept
|
192
197
|
end
|
193
198
|
|
194
199
|
def accept_loop(&block)
|
195
|
-
Thread.current.backend.accept_loop(@io, &block)
|
200
|
+
Thread.current.backend.accept_loop(@io, TCPSocket, &block)
|
196
201
|
end
|
197
202
|
|
198
203
|
alias_method :orig_close, :close
|
@@ -204,19 +209,17 @@ end
|
|
204
209
|
class ::UNIXServer
|
205
210
|
alias_method :orig_accept, :accept
|
206
211
|
def accept
|
207
|
-
Thread.current.backend.accept(self)
|
212
|
+
Thread.current.backend.accept(self, UNIXSocket)
|
208
213
|
end
|
209
214
|
|
210
215
|
def accept_loop(&block)
|
211
|
-
Thread.current.backend.accept_loop(self, &block)
|
216
|
+
Thread.current.backend.accept_loop(self, UNIXSocket, &block)
|
212
217
|
end
|
213
|
-
|
214
|
-
|
215
218
|
end
|
216
219
|
|
217
220
|
class ::UNIXSocket
|
218
221
|
def recv(maxlen, flags = 0, outbuf = nil)
|
219
|
-
Thread.current.backend.recv(self,
|
222
|
+
Thread.current.backend.recv(self, outbuf || +'', maxlen)
|
220
223
|
end
|
221
224
|
|
222
225
|
def recv_loop(&block)
|
data/lib/polyphony/net.rb
CHANGED
@@ -8,10 +8,7 @@ module Polyphony
|
|
8
8
|
module Net
|
9
9
|
class << self
|
10
10
|
def tcp_connect(host, port, opts = {})
|
11
|
-
socket =
|
12
|
-
addr = ::Socket.sockaddr_in(port, host)
|
13
|
-
s.connect(addr)
|
14
|
-
end
|
11
|
+
socket = TCPSocket.new(host, port)
|
15
12
|
if opts[:secure_context] || opts[:secure]
|
16
13
|
secure_socket(socket, opts[:secure_context], opts.merge(host: host))
|
17
14
|
else
|
@@ -23,7 +20,7 @@ module Polyphony
|
|
23
20
|
host ||= '0.0.0.0'
|
24
21
|
raise 'Port number not specified' unless port
|
25
22
|
|
26
|
-
socket =
|
23
|
+
socket = listening_socket_from_options(host, port, opts)
|
27
24
|
if opts[:secure_context] || opts[:secure]
|
28
25
|
secure_server(socket, opts[:secure_context], opts)
|
29
26
|
else
|
@@ -31,7 +28,7 @@ module Polyphony
|
|
31
28
|
end
|
32
29
|
end
|
33
30
|
|
34
|
-
def
|
31
|
+
def listening_socket_from_options(host, port, opts)
|
35
32
|
::Socket.new(:INET, :STREAM).tap do |s|
|
36
33
|
s.reuse_addr if opts[:reuse_addr]
|
37
34
|
s.dont_linger if opts[:dont_linger]
|
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
@@ -639,7 +639,6 @@ class FiberTest < MiniTest::Test
|
|
639
639
|
i.close
|
640
640
|
f.await
|
641
641
|
rescue Exception => e
|
642
|
-
trace e
|
643
642
|
o << e.class.name
|
644
643
|
o.close
|
645
644
|
end
|
@@ -1038,3 +1037,67 @@ class RestartTest < MiniTest::Test
|
|
1038
1037
|
assert_equal [f, 'foo', 'bar', :done, f2, 'baz', 42, :done], buffer
|
1039
1038
|
end
|
1040
1039
|
end
|
1040
|
+
|
1041
|
+
class GracefulTerminationTest < MiniTest::Test
|
1042
|
+
def test_graceful_termination
|
1043
|
+
buffer = []
|
1044
|
+
f = spin do
|
1045
|
+
buffer << 1
|
1046
|
+
snooze
|
1047
|
+
buffer << 2
|
1048
|
+
sleep 3
|
1049
|
+
buffer << 3
|
1050
|
+
ensure
|
1051
|
+
buffer << 4 if Fiber.current.graceful_shutdown?
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
3.times { snooze }
|
1055
|
+
f.terminate(false)
|
1056
|
+
f.await
|
1057
|
+
assert_equal [1, 2], buffer
|
1058
|
+
|
1059
|
+
buffer = []
|
1060
|
+
f = spin do
|
1061
|
+
buffer << 1
|
1062
|
+
snooze
|
1063
|
+
buffer << 2
|
1064
|
+
sleep 3
|
1065
|
+
buffer << 3
|
1066
|
+
ensure
|
1067
|
+
buffer << 4 if Fiber.current.graceful_shutdown?
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
3.times { snooze }
|
1071
|
+
f.terminate(true)
|
1072
|
+
f.await
|
1073
|
+
assert_equal [1, 2, 4], buffer
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
def test_graceful_child_shutdown
|
1077
|
+
buffer = []
|
1078
|
+
f0 = spin do
|
1079
|
+
f1 = spin do
|
1080
|
+
sleep
|
1081
|
+
ensure
|
1082
|
+
buffer << 1 if Fiber.current.graceful_shutdown?
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
f2 = spin do
|
1086
|
+
sleep
|
1087
|
+
ensure
|
1088
|
+
buffer << 2 if Fiber.current.graceful_shutdown?
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
sleep
|
1092
|
+
ensure
|
1093
|
+
Fiber.current.terminate_all_children(true) if Fiber.current.graceful_shutdown?
|
1094
|
+
Fiber.current.await_all_children
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
3.times { snooze }
|
1098
|
+
f0.terminate(true)
|
1099
|
+
f0.await
|
1100
|
+
|
1101
|
+
assert_equal [1, 2], buffer
|
1102
|
+
end
|
1103
|
+
end
|
data/test/test_global_api.rb
CHANGED
@@ -307,6 +307,36 @@ class SpinLoopTest < MiniTest::Test
|
|
307
307
|
f.stop
|
308
308
|
assert_in_range 1..3, counter
|
309
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
|
310
340
|
end
|
311
341
|
|
312
342
|
class SpinScopeTest < MiniTest::Test
|