polyphony 0.40 → 0.41
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +11 -2
- data/.gitignore +2 -2
- data/.rubocop.yml +30 -0
- data/CHANGELOG.md +6 -2
- data/Gemfile.lock +9 -6
- data/Rakefile +2 -2
- data/TODO.md +18 -97
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/nav.html +5 -5
- data/docs/api-reference/fiber.md +2 -2
- data/docs/main-concepts/design-principles.md +67 -9
- data/docs/main-concepts/extending.md +1 -1
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-sleeping.rb +14 -6
- data/examples/io/xx-irb.rb +1 -1
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +14 -25
- data/ext/{gyro → polyphony}/extconf.rb +2 -2
- data/ext/{gyro → polyphony}/fiber.c +15 -19
- data/ext/{gyro → polyphony}/libev.c +0 -0
- data/ext/{gyro → polyphony}/libev.h +0 -0
- data/ext/polyphony/libev_agent.c +503 -0
- data/ext/polyphony/libev_queue.c +214 -0
- data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -25
- data/ext/polyphony/polyphony.h +90 -0
- data/ext/polyphony/polyphony_ext.c +23 -0
- data/ext/{gyro → polyphony}/socket.c +14 -14
- data/ext/{gyro → polyphony}/thread.c +32 -115
- data/ext/{gyro → polyphony}/tracing.c +1 -1
- data/lib/polyphony.rb +16 -12
- data/lib/polyphony/adapters/irb.rb +1 -1
- data/lib/polyphony/adapters/postgres.rb +6 -5
- data/lib/polyphony/adapters/process.rb +5 -5
- data/lib/polyphony/adapters/trace.rb +28 -28
- data/lib/polyphony/core/channel.rb +3 -3
- data/lib/polyphony/core/exceptions.rb +1 -1
- data/lib/polyphony/core/global_api.rb +11 -9
- data/lib/polyphony/core/resource_pool.rb +3 -3
- data/lib/polyphony/core/sync.rb +2 -2
- data/lib/polyphony/core/thread_pool.rb +6 -6
- data/lib/polyphony/core/throttler.rb +13 -6
- data/lib/polyphony/event.rb +27 -0
- data/lib/polyphony/extensions/core.rb +20 -11
- data/lib/polyphony/extensions/fiber.rb +4 -4
- data/lib/polyphony/extensions/io.rb +56 -26
- data/lib/polyphony/extensions/openssl.rb +4 -8
- data/lib/polyphony/extensions/socket.rb +27 -9
- data/lib/polyphony/extensions/thread.rb +16 -9
- data/lib/polyphony/net.rb +9 -9
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +2 -2
- data/test/helper.rb +12 -1
- data/test/test_agent.rb +77 -0
- data/test/{test_async.rb → test_event.rb} +13 -7
- data/test/test_ext.rb +25 -4
- data/test/test_fiber.rb +19 -10
- data/test/test_global_api.rb +4 -4
- data/test/test_io.rb +46 -24
- data/test/test_queue.rb +74 -0
- data/test/test_signal.rb +3 -40
- data/test/test_socket.rb +33 -0
- data/test/test_thread.rb +37 -16
- data/test/test_trace.rb +6 -5
- metadata +24 -24
- data/ext/gyro/async.c +0 -132
- data/ext/gyro/child.c +0 -108
- data/ext/gyro/gyro.h +0 -158
- data/ext/gyro/gyro_ext.c +0 -33
- data/ext/gyro/io.c +0 -457
- data/ext/gyro/queue.c +0 -146
- data/ext/gyro/selector.c +0 -205
- data/ext/gyro/signal.c +0 -99
- data/ext/gyro/timer.c +0 -115
- data/test/test_timer.rb +0 -56
@@ -7,14 +7,21 @@ require_relative '../core/thread_pool'
|
|
7
7
|
|
8
8
|
# Socket overrides (eventually rewritten in C)
|
9
9
|
class ::Socket
|
10
|
+
def accept
|
11
|
+
Thread.current.agent.accept(self)
|
12
|
+
end
|
13
|
+
|
10
14
|
NO_EXCEPTION = { exception: false }.freeze
|
11
15
|
|
12
16
|
def connect(remotesockaddr)
|
13
17
|
loop do
|
14
18
|
result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
case result
|
20
|
+
when 0 then return
|
21
|
+
when :wait_writable then Thread.current.agent.wait_io(self, true)
|
22
|
+
else
|
23
|
+
raise IOError
|
24
|
+
end
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
@@ -22,9 +29,12 @@ class ::Socket
|
|
22
29
|
outbuf ||= +''
|
23
30
|
loop do
|
24
31
|
result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
|
25
|
-
|
26
|
-
|
27
|
-
|
32
|
+
case result
|
33
|
+
when nil then raise IOError
|
34
|
+
when :wait_readable then Thread.current.agent.wait_io(self, false)
|
35
|
+
else
|
36
|
+
return result
|
37
|
+
end
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
@@ -32,9 +42,12 @@ class ::Socket
|
|
32
42
|
@read_buffer ||= +''
|
33
43
|
loop do
|
34
44
|
result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
|
35
|
-
|
36
|
-
|
37
|
-
|
45
|
+
case result
|
46
|
+
when nil then raise IOError
|
47
|
+
when :wait_readable then Thread.current.agent.wait_io(self, false)
|
48
|
+
else
|
49
|
+
return result
|
50
|
+
end
|
38
51
|
end
|
39
52
|
end
|
40
53
|
|
@@ -117,4 +130,9 @@ class ::TCPServer
|
|
117
130
|
def accept
|
118
131
|
@io ? @io.accept : orig_accept
|
119
132
|
end
|
133
|
+
|
134
|
+
alias_method :orig_close, :close
|
135
|
+
def close
|
136
|
+
@io ? @io.close : orig_close
|
137
|
+
end
|
120
138
|
end
|
@@ -8,26 +8,30 @@ class ::Thread
|
|
8
8
|
|
9
9
|
alias_method :orig_initialize, :initialize
|
10
10
|
def initialize(*args, &block)
|
11
|
-
@join_wait_queue =
|
11
|
+
@join_wait_queue = []
|
12
|
+
@finalization_mutex = Mutex.new
|
12
13
|
@args = args
|
13
14
|
@block = block
|
14
|
-
@finalization_mutex = Mutex.new
|
15
15
|
orig_initialize { execute }
|
16
16
|
end
|
17
17
|
|
18
18
|
def execute
|
19
|
+
# agent must be created in the context of the new thread, therefore it
|
20
|
+
# cannot be created in Thread#initialize
|
21
|
+
@agent = Polyphony::LibevAgent.new
|
19
22
|
setup
|
20
23
|
@ready = true
|
21
24
|
result = @block.(*@args)
|
22
25
|
rescue Polyphony::MoveOn, Polyphony::Terminate => e
|
23
26
|
result = e.value
|
24
|
-
rescue Exception =>
|
25
|
-
result = e
|
27
|
+
rescue Exception => result
|
26
28
|
ensure
|
27
29
|
@ready = true
|
28
30
|
finalize(result)
|
29
31
|
end
|
30
32
|
|
33
|
+
attr_accessor :agent
|
34
|
+
|
31
35
|
def setup
|
32
36
|
@main_fiber = Fiber.current
|
33
37
|
@main_fiber.setup_main_fiber
|
@@ -44,24 +48,25 @@ class ::Thread
|
|
44
48
|
@result = result
|
45
49
|
signal_waiters(result)
|
46
50
|
end
|
47
|
-
|
51
|
+
@agent.finalize
|
48
52
|
end
|
49
53
|
|
50
54
|
def signal_waiters(result)
|
51
|
-
@join_wait_queue.
|
55
|
+
@join_wait_queue.each { |w| w.signal(result) }
|
52
56
|
end
|
53
57
|
|
54
58
|
alias_method :orig_join, :join
|
55
59
|
def join(timeout = nil)
|
56
|
-
|
60
|
+
watcher = Fiber.current.auto_watcher
|
61
|
+
|
57
62
|
@finalization_mutex.synchronize do
|
58
63
|
if @terminated
|
59
64
|
@result.is_a?(Exception) ? (raise @result) : (return @result)
|
60
65
|
else
|
61
|
-
@join_wait_queue
|
66
|
+
@join_wait_queue << watcher
|
62
67
|
end
|
63
68
|
end
|
64
|
-
timeout ? move_on_after(timeout) {
|
69
|
+
timeout ? move_on_after(timeout) { watcher.await } : watcher.await
|
65
70
|
end
|
66
71
|
alias_method :await, :join
|
67
72
|
|
@@ -78,6 +83,8 @@ class ::Thread
|
|
78
83
|
|
79
84
|
alias_method :orig_kill, :kill
|
80
85
|
def kill
|
86
|
+
return if @terminated
|
87
|
+
|
81
88
|
raise Polyphony::Terminate
|
82
89
|
end
|
83
90
|
|
data/lib/polyphony/net.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative './extensions/socket'
|
|
4
4
|
require_relative './extensions/openssl'
|
5
5
|
|
6
6
|
module Polyphony
|
7
|
+
# A more elegant networking API
|
7
8
|
module Net
|
8
9
|
class << self
|
9
10
|
def tcp_connect(host, port, opts = {})
|
@@ -17,11 +18,11 @@ module Polyphony
|
|
17
18
|
socket
|
18
19
|
end
|
19
20
|
end
|
20
|
-
|
21
|
+
|
21
22
|
def tcp_listen(host = nil, port = nil, opts = {})
|
22
23
|
host ||= '0.0.0.0'
|
23
24
|
raise 'Port number not specified' unless port
|
24
|
-
|
25
|
+
|
25
26
|
socket = socket_from_options(host, port, opts)
|
26
27
|
if opts[:secure_context] || opts[:secure]
|
27
28
|
secure_server(socket, opts[:secure_context], opts)
|
@@ -29,7 +30,7 @@ module Polyphony
|
|
29
30
|
socket
|
30
31
|
end
|
31
32
|
end
|
32
|
-
|
33
|
+
|
33
34
|
def socket_from_options(host, port, opts)
|
34
35
|
::Socket.new(:INET, :STREAM).tap do |s|
|
35
36
|
s.reuse_addr if opts[:reuse_addr]
|
@@ -39,19 +40,19 @@ module Polyphony
|
|
39
40
|
s.listen(0)
|
40
41
|
end
|
41
42
|
end
|
42
|
-
|
43
|
+
|
43
44
|
def secure_socket(socket, context, opts)
|
44
45
|
context ||= OpenSSL::SSL::SSLContext.new
|
45
46
|
setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
|
46
47
|
socket = secure_socket_wrapper(socket, context)
|
47
|
-
|
48
|
+
|
48
49
|
socket.tap do |s|
|
49
50
|
s.hostname = opts[:host] if opts[:host]
|
50
51
|
s.connect
|
51
52
|
s.post_connection_check(opts[:host]) if opts[:host]
|
52
53
|
end
|
53
54
|
end
|
54
|
-
|
55
|
+
|
55
56
|
def secure_socket_wrapper(socket, context)
|
56
57
|
if context
|
57
58
|
OpenSSL::SSL::SSLSocket.new(socket, context)
|
@@ -59,12 +60,12 @@ module Polyphony
|
|
59
60
|
OpenSSL::SSL::SSLSocket.new(socket)
|
60
61
|
end
|
61
62
|
end
|
62
|
-
|
63
|
+
|
63
64
|
def secure_server(socket, context, opts)
|
64
65
|
setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
|
65
66
|
OpenSSL::SSL::SSLServer.new(socket, context)
|
66
67
|
end
|
67
|
-
|
68
|
+
|
68
69
|
def setup_alpn(context, protocols)
|
69
70
|
context.alpn_protocols = protocols
|
70
71
|
context.alpn_select_cb = lambda do |peer_protocols|
|
@@ -74,4 +75,3 @@ module Polyphony
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
end
|
77
|
-
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
|
|
17
17
|
}
|
18
18
|
s.rdoc_options = ["--title", "polyphony", "--main", "README.md"]
|
19
19
|
s.extra_rdoc_files = ["README.md"]
|
20
|
-
s.extensions = ["ext/
|
20
|
+
s.extensions = ["ext/polyphony/extconf.rb"]
|
21
21
|
s.require_paths = ["lib"]
|
22
22
|
s.required_ruby_version = '>= 2.6'
|
23
23
|
|
@@ -26,7 +26,7 @@ Gem::Specification.new do |s|
|
|
26
26
|
s.add_development_dependency 'minitest', '5.13.0'
|
27
27
|
s.add_development_dependency 'minitest-reporters', '1.4.2'
|
28
28
|
s.add_development_dependency 'simplecov', '0.17.1'
|
29
|
-
s.add_development_dependency 'rubocop', '0.
|
29
|
+
s.add_development_dependency 'rubocop', '0.85.1'
|
30
30
|
s.add_development_dependency 'pg', '1.1.4'
|
31
31
|
s.add_development_dependency 'rake-compiler', '1.0.5'
|
32
32
|
s.add_development_dependency 'redis', '4.1.0'
|
data/test/helper.rb
CHANGED
@@ -18,6 +18,10 @@ Minitest::Reporters.use! [
|
|
18
18
|
Minitest::Reporters::SpecReporter.new
|
19
19
|
]
|
20
20
|
|
21
|
+
class ::Fiber
|
22
|
+
attr_writer :auto_watcher
|
23
|
+
end
|
24
|
+
|
21
25
|
class MiniTest::Test
|
22
26
|
def setup
|
23
27
|
# puts "* setup #{self.name}"
|
@@ -26,13 +30,20 @@ class MiniTest::Test
|
|
26
30
|
exit!
|
27
31
|
end
|
28
32
|
Fiber.current.setup_main_fiber
|
33
|
+
Fiber.current.instance_variable_set(:@auto_watcher, nil)
|
34
|
+
Thread.current.agent = Polyphony::LibevAgent.new
|
29
35
|
sleep 0
|
30
36
|
end
|
31
37
|
|
32
38
|
def teardown
|
33
|
-
#puts "* teardown #{self.name}"
|
39
|
+
# puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
|
34
40
|
Fiber.current.terminate_all_children
|
35
41
|
Fiber.current.await_all_children
|
42
|
+
Fiber.current.auto_watcher = nil
|
43
|
+
rescue => e
|
44
|
+
puts e
|
45
|
+
puts e.backtrace.join("\n")
|
46
|
+
exit!
|
36
47
|
end
|
37
48
|
end
|
38
49
|
|
data/test/test_agent.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class AgentTest < MiniTest::Test
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
@agent = Polyphony::LibevAgent.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
@agent.finalize
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_sleep
|
16
|
+
count = 0
|
17
|
+
t0 = Time.now
|
18
|
+
spin {
|
19
|
+
@agent.sleep 0.01
|
20
|
+
count += 1
|
21
|
+
}
|
22
|
+
suspend
|
23
|
+
assert Time.now - t0 >= 0.01
|
24
|
+
assert_equal 1, count
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_write_read_partial
|
28
|
+
i, o = IO.pipe
|
29
|
+
buf = +''
|
30
|
+
f = spin { @agent.read(i, buf, 5, false) }
|
31
|
+
@agent.write(o, 'Hello world')
|
32
|
+
return_value = f.await
|
33
|
+
|
34
|
+
assert_equal 'Hello', buf
|
35
|
+
assert_equal return_value, buf
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_write_read_to_eof_limited_buffer
|
39
|
+
i, o = IO.pipe
|
40
|
+
buf = +''
|
41
|
+
f = spin { @agent.read(i, buf, 5, true) }
|
42
|
+
@agent.write(o, 'Hello')
|
43
|
+
snooze
|
44
|
+
@agent.write(o, ' world')
|
45
|
+
snooze
|
46
|
+
o.close
|
47
|
+
return_value = f.await
|
48
|
+
|
49
|
+
assert_equal 'Hello', buf
|
50
|
+
assert_equal return_value, buf
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_write_read_to_eof
|
54
|
+
i, o = IO.pipe
|
55
|
+
buf = +''
|
56
|
+
f = spin { @agent.read(i, buf, 10**6, true) }
|
57
|
+
@agent.write(o, 'Hello')
|
58
|
+
snooze
|
59
|
+
@agent.write(o, ' world')
|
60
|
+
snooze
|
61
|
+
o.close
|
62
|
+
return_value = f.await
|
63
|
+
|
64
|
+
assert_equal 'Hello world', buf
|
65
|
+
assert_equal return_value, buf
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_waitpid
|
69
|
+
pid = fork do
|
70
|
+
Thread.current.agent.post_fork
|
71
|
+
exit(42)
|
72
|
+
end
|
73
|
+
|
74
|
+
result = Thread.current.agent.waitpid(pid)
|
75
|
+
assert_equal [pid, 42], result
|
76
|
+
end
|
77
|
+
end
|
@@ -2,26 +2,29 @@
|
|
2
2
|
|
3
3
|
require_relative 'helper'
|
4
4
|
|
5
|
-
class
|
6
|
-
def
|
5
|
+
class EventTest < MiniTest::Test
|
6
|
+
def test_that_event_receives_signal_across_threads
|
7
7
|
count = 0
|
8
|
-
a =
|
8
|
+
a = Polyphony::Event.new
|
9
9
|
spin {
|
10
10
|
a.await
|
11
11
|
count += 1
|
12
12
|
}
|
13
13
|
snooze
|
14
|
-
Thread.new do
|
14
|
+
t = Thread.new do
|
15
15
|
orig_sleep 0.001
|
16
16
|
a.signal
|
17
17
|
end
|
18
18
|
suspend
|
19
19
|
assert_equal 1, count
|
20
|
+
ensure
|
21
|
+
t&.kill
|
22
|
+
t&.join
|
20
23
|
end
|
21
24
|
|
22
|
-
def
|
25
|
+
def test_that_event_coalesces_signals
|
23
26
|
count = 0
|
24
|
-
a =
|
27
|
+
a = Polyphony::Event.new
|
25
28
|
|
26
29
|
coproc = spin {
|
27
30
|
loop {
|
@@ -31,12 +34,15 @@ class AsyncTest < MiniTest::Test
|
|
31
34
|
}
|
32
35
|
}
|
33
36
|
snooze
|
34
|
-
Thread.new do
|
37
|
+
t = Thread.new do
|
35
38
|
orig_sleep 0.001
|
36
39
|
3.times { a.signal }
|
37
40
|
end
|
38
41
|
|
39
42
|
coproc.await
|
40
43
|
assert_equal 1, count
|
44
|
+
ensure
|
45
|
+
t&.kill
|
46
|
+
t&.join
|
41
47
|
end
|
42
48
|
end
|
data/test/test_ext.rb
CHANGED
@@ -74,7 +74,7 @@ class KernelTest < MiniTest::Test
|
|
74
74
|
$stderr.rewind
|
75
75
|
$stderr = prev_stderr
|
76
76
|
|
77
|
-
|
77
|
+
assert_equal '', data
|
78
78
|
assert_equal "error\n", err_io.read
|
79
79
|
ensure
|
80
80
|
$stderr = prev_stderr
|
@@ -93,6 +93,28 @@ class KernelTest < MiniTest::Test
|
|
93
93
|
$stdin = prev_stdin
|
94
94
|
end
|
95
95
|
|
96
|
+
def test_multiline_gets
|
97
|
+
prev_stdin = $stdin
|
98
|
+
i, o = IO.pipe
|
99
|
+
$stdin = i
|
100
|
+
|
101
|
+
spin do
|
102
|
+
o << "hello\n"
|
103
|
+
o << "world\n"
|
104
|
+
o << "nice\n"
|
105
|
+
o << "to\n"
|
106
|
+
o << "meet\n"
|
107
|
+
o << "you\n"
|
108
|
+
end
|
109
|
+
|
110
|
+
s = +''
|
111
|
+
6.times { s << gets }
|
112
|
+
|
113
|
+
assert_equal "hello\nworld\nnice\nto\nmeet\nyou\n", s
|
114
|
+
ensure
|
115
|
+
$stdin = prev_stdin
|
116
|
+
end
|
117
|
+
|
96
118
|
def test_gets_from_argv
|
97
119
|
prev_stdin = $stdin
|
98
120
|
|
@@ -103,8 +125,7 @@ class KernelTest < MiniTest::Test
|
|
103
125
|
count = contents.size
|
104
126
|
|
105
127
|
buffer = []
|
106
|
-
|
107
|
-
(count * 2).times { buffer << gets }
|
128
|
+
(count * 2).times { |i| s = gets; buffer << s }
|
108
129
|
assert_equal contents * 2, buffer
|
109
130
|
|
110
131
|
i, o = IO.pipe
|
@@ -172,4 +193,4 @@ class TimeoutTest < MiniTest::Test
|
|
172
193
|
assert_kind_of MyTimeout, e
|
173
194
|
assert_equal 'foo', e.message
|
174
195
|
end
|
175
|
-
end
|
196
|
+
end
|