polyphony 0.47.3 → 0.49.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 +318 -295
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/TODO.md +40 -27
- 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.rb +1 -0
- data/lib/polyphony/adapters/postgres.rb +3 -3
- 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 +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 -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 +122 -0
- metadata +9 -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
|
@@ -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
@@ -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
|