polyphony 0.40 → 0.41
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 +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
|