polyphony 0.44.0 → 0.45.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/.rubocop.yml +7 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +9 -11
- data/Rakefile +1 -1
- data/TODO.md +12 -7
- data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
- data/docs/api-reference/thread.md +1 -1
- data/docs/getting-started/overview.md +14 -14
- data/docs/getting-started/tutorial.md +1 -1
- data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
- data/examples/io/xx-pry.rb +18 -0
- data/examples/io/xx-rack_server.rb +71 -0
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
- data/ext/polyphony/backend.h +41 -0
- data/ext/polyphony/event.c +3 -3
- data/ext/polyphony/extconf.rb +1 -1
- data/ext/polyphony/{libev_agent.c → libev_backend.c} +175 -175
- data/ext/polyphony/polyphony.c +1 -1
- data/ext/polyphony/polyphony.h +4 -4
- data/ext/polyphony/polyphony_ext.c +2 -2
- data/ext/polyphony/queue.c +2 -2
- data/ext/polyphony/thread.c +21 -21
- data/lib/polyphony.rb +13 -12
- data/lib/polyphony/adapters/irb.rb +2 -17
- data/lib/polyphony/adapters/mysql2.rb +1 -1
- data/lib/polyphony/adapters/postgres.rb +5 -5
- data/lib/polyphony/adapters/process.rb +2 -2
- data/lib/polyphony/adapters/readline.rb +17 -0
- data/lib/polyphony/adapters/sequel.rb +1 -1
- data/lib/polyphony/core/global_api.rb +11 -6
- data/lib/polyphony/core/resource_pool.rb +2 -2
- data/lib/polyphony/core/sync.rb +38 -2
- data/lib/polyphony/core/throttler.rb +1 -1
- data/lib/polyphony/extensions/core.rb +31 -20
- data/lib/polyphony/extensions/fiber.rb +1 -1
- data/lib/polyphony/extensions/io.rb +7 -8
- data/lib/polyphony/extensions/openssl.rb +6 -6
- data/lib/polyphony/extensions/socket.rb +4 -14
- data/lib/polyphony/extensions/thread.rb +6 -5
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +4 -3
- data/test/helper.rb +1 -1
- data/test/{test_agent.rb → test_backend.rb} +22 -22
- data/test/test_fiber.rb +4 -4
- data/test/test_io.rb +1 -1
- data/test/test_kernel.rb +5 -0
- data/test/test_signal.rb +3 -3
- data/test/test_sync.rb +52 -0
- metadata +40 -30
- data/.gitbook.yaml +0 -4
- data/ext/polyphony/agent.h +0 -41
data/lib/polyphony/core/sync.rb
CHANGED
@@ -12,12 +12,48 @@ module Polyphony
|
|
12
12
|
return yield if @holding_fiber == Fiber.current
|
13
13
|
|
14
14
|
begin
|
15
|
-
token = @store.shift
|
15
|
+
@token = @store.shift
|
16
16
|
@holding_fiber = Fiber.current
|
17
17
|
yield
|
18
18
|
ensure
|
19
19
|
@holding_fiber = nil
|
20
|
-
@store << token if token
|
20
|
+
@store << @token if @token
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def conditional_release
|
25
|
+
@store << @token
|
26
|
+
@token = nil
|
27
|
+
@holding_fiber = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def conditional_reacquire
|
31
|
+
@token = @store.shift
|
32
|
+
@holding_fiber = Fiber.current
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Implements a fiber-aware ConditionVariable
|
37
|
+
class ConditionVariable
|
38
|
+
def initialize
|
39
|
+
@queue = Polyphony::Queue.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait(mutex, _timeout = nil)
|
43
|
+
mutex.conditional_release
|
44
|
+
@queue << Fiber.current
|
45
|
+
Thread.current.backend.wait_event(true)
|
46
|
+
mutex.conditional_reacquire
|
47
|
+
end
|
48
|
+
|
49
|
+
def signal
|
50
|
+
fiber = @queue.shift
|
51
|
+
fiber.schedule
|
52
|
+
end
|
53
|
+
|
54
|
+
def broadcast
|
55
|
+
while (fiber = @queue.shift)
|
56
|
+
fiber.schedule
|
21
57
|
end
|
22
58
|
end
|
23
59
|
end
|
@@ -57,7 +57,7 @@ module ::Process
|
|
57
57
|
class << self
|
58
58
|
alias_method :orig_detach, :detach
|
59
59
|
def detach(pid)
|
60
|
-
fiber = spin { Thread.current.
|
60
|
+
fiber = spin { Thread.current.backend.waitpid(pid) }
|
61
61
|
fiber.define_singleton_method(:pid) { pid }
|
62
62
|
fiber
|
63
63
|
end
|
@@ -116,19 +116,28 @@ module ::Kernel
|
|
116
116
|
strs = args.inject([]) do |m, a|
|
117
117
|
m << a.inspect << "\n"
|
118
118
|
end
|
119
|
-
STDOUT.write
|
119
|
+
STDOUT.write(*strs)
|
120
120
|
args.size == 1 ? args.first : args
|
121
121
|
end
|
122
122
|
|
123
123
|
alias_method :orig_system, :system
|
124
124
|
def system(*args)
|
125
|
-
|
126
|
-
|
127
|
-
|
125
|
+
Kernel.system(*args)
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
alias_method :orig_system, :system
|
130
|
+
def system(*args)
|
131
|
+
waiter = nil
|
132
|
+
Open3.popen2(*args) do |i, o, t|
|
133
|
+
waiter = t
|
134
|
+
i.close
|
135
|
+
pipe_to_eof(o, $stdout)
|
136
|
+
end
|
137
|
+
waiter.await.last == 0
|
138
|
+
rescue SystemCallError
|
139
|
+
nil
|
128
140
|
end
|
129
|
-
true
|
130
|
-
rescue SystemCallError
|
131
|
-
nil
|
132
141
|
end
|
133
142
|
|
134
143
|
def pipe_to_eof(src, dest)
|
@@ -143,23 +152,15 @@ module ::Kernel
|
|
143
152
|
alias_method :orig_trap, :trap
|
144
153
|
def trap(sig, command = nil, &block)
|
145
154
|
return orig_trap(sig, command) if command.is_a? String
|
146
|
-
|
147
|
-
block = command if !block && command.respond_to?(:call)
|
148
|
-
if block
|
149
|
-
exception = Polyphony::Interjection.new(block)
|
150
|
-
else
|
151
|
-
exception = command.is_a?(Class) && command.new
|
152
|
-
end
|
153
155
|
|
154
|
-
|
155
|
-
|
156
|
-
end
|
156
|
+
block = command if !block && command.respond_to?(:call)
|
157
|
+
exception = signal_exception(block, command)
|
157
158
|
|
158
159
|
# The signal trap can be invoked at any time, including while the system
|
159
|
-
#
|
160
|
+
# backend is blocking while polling for events. In order to deal with this
|
160
161
|
# correctly, we spin a fiber that will run the signal handler code, then
|
161
162
|
# call break_out_of_ev_loop, which will put the fiber at the front of the
|
162
|
-
# run queue, then wake up the
|
163
|
+
# run queue, then wake up the backend.
|
163
164
|
#
|
164
165
|
# If the command argument is an exception class however, it will be raised
|
165
166
|
# directly in the context of the main fiber.
|
@@ -169,6 +170,16 @@ module ::Kernel
|
|
169
170
|
end
|
170
171
|
end
|
171
172
|
|
173
|
+
def signal_exception(block, command)
|
174
|
+
if block
|
175
|
+
Polyphony::Interjection.new(block)
|
176
|
+
elsif command.is_a?(Class)
|
177
|
+
command.new
|
178
|
+
else
|
179
|
+
raise ArgumentError, 'Must supply block or exception or callable object'
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
172
183
|
# Override Timeout to use cancel scope
|
173
184
|
module ::Timeout
|
174
185
|
def self.timeout(sec, klass = nil, message = nil, &block)
|
@@ -99,7 +99,7 @@ class ::IO
|
|
99
99
|
alias_method :orig_read, :read
|
100
100
|
def read(len = nil)
|
101
101
|
@read_buffer ||= +''
|
102
|
-
result = Thread.current.
|
102
|
+
result = Thread.current.backend.read(self, @read_buffer, len, true)
|
103
103
|
return nil unless result
|
104
104
|
|
105
105
|
already_read = @read_buffer
|
@@ -110,7 +110,7 @@ class ::IO
|
|
110
110
|
alias_method :orig_readpartial, :read
|
111
111
|
def readpartial(len, str = nil)
|
112
112
|
@read_buffer ||= +''
|
113
|
-
result = Thread.current.
|
113
|
+
result = Thread.current.backend.read(self, @read_buffer, len, false)
|
114
114
|
raise EOFError unless result
|
115
115
|
|
116
116
|
if str
|
@@ -124,12 +124,12 @@ class ::IO
|
|
124
124
|
|
125
125
|
alias_method :orig_write, :write
|
126
126
|
def write(str, *args)
|
127
|
-
Thread.current.
|
127
|
+
Thread.current.backend.write(self, str, *args)
|
128
128
|
end
|
129
129
|
|
130
130
|
alias_method :orig_write_chevron, :<<
|
131
131
|
def <<(str)
|
132
|
-
Thread.current.
|
132
|
+
Thread.current.backend.write(self, str)
|
133
133
|
self
|
134
134
|
end
|
135
135
|
|
@@ -170,13 +170,12 @@ class ::IO
|
|
170
170
|
return
|
171
171
|
end
|
172
172
|
|
173
|
-
strs = args.
|
173
|
+
strs = args.each_with_object([]) do |a, m|
|
174
174
|
a = a.to_s
|
175
175
|
m << a
|
176
176
|
m << "\n" unless a =~ /\n$/
|
177
|
-
m
|
178
177
|
end
|
179
|
-
write
|
178
|
+
write(*strs)
|
180
179
|
nil
|
181
180
|
end
|
182
181
|
|
@@ -203,7 +202,7 @@ class ::IO
|
|
203
202
|
end
|
204
203
|
|
205
204
|
def read_loop(&block)
|
206
|
-
Thread.current.
|
205
|
+
Thread.current.backend.read_loop(self, &block)
|
207
206
|
end
|
208
207
|
|
209
208
|
# alias_method :orig_read, :read
|
@@ -28,8 +28,8 @@ class ::OpenSSL::SSL::SSLSocket
|
|
28
28
|
loop do
|
29
29
|
result = accept_nonblock(exception: false)
|
30
30
|
case result
|
31
|
-
when :wait_readable then Thread.current.
|
32
|
-
when :wait_writable then Thread.current.
|
31
|
+
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
32
|
+
when :wait_writable then Thread.current.backend.wait_io(io, true)
|
33
33
|
else
|
34
34
|
return result
|
35
35
|
end
|
@@ -40,8 +40,8 @@ class ::OpenSSL::SSL::SSLSocket
|
|
40
40
|
def sysread(maxlen, buf = +'')
|
41
41
|
loop do
|
42
42
|
case (result = read_nonblock(maxlen, buf, exception: false))
|
43
|
-
when :wait_readable then Thread.current.
|
44
|
-
when :wait_writable then Thread.current.
|
43
|
+
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
44
|
+
when :wait_writable then Thread.current.backend.wait_io(io, true)
|
45
45
|
else return result
|
46
46
|
end
|
47
47
|
end
|
@@ -51,8 +51,8 @@ class ::OpenSSL::SSL::SSLSocket
|
|
51
51
|
def syswrite(buf)
|
52
52
|
loop do
|
53
53
|
case (result = write_nonblock(buf, exception: false))
|
54
|
-
when :wait_readable then Thread.current.
|
55
|
-
when :wait_writable then Thread.current.
|
54
|
+
when :wait_readable then Thread.current.backend.wait_io(io, false)
|
55
|
+
when :wait_writable then Thread.current.backend.wait_io(io, true)
|
56
56
|
else
|
57
57
|
return result
|
58
58
|
end
|
@@ -5,26 +5,16 @@ require 'socket'
|
|
5
5
|
require_relative './io'
|
6
6
|
require_relative '../core/thread_pool'
|
7
7
|
|
8
|
-
class ::BasicSocket
|
9
|
-
def write_nonblock(string, _options = {})
|
10
|
-
write(string)
|
11
|
-
end
|
12
|
-
|
13
|
-
def read_nonblock(maxlen, str = nil, _options = {})
|
14
|
-
readpartial(maxlen, str)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
8
|
# Socket overrides (eventually rewritten in C)
|
19
9
|
class ::Socket
|
20
10
|
def accept
|
21
|
-
Thread.current.
|
11
|
+
Thread.current.backend.accept(self)
|
22
12
|
end
|
23
13
|
|
24
14
|
NO_EXCEPTION = { exception: false }.freeze
|
25
15
|
|
26
16
|
def connect(remotesockaddr)
|
27
|
-
Thread.current.
|
17
|
+
Thread.current.backend.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
|
28
18
|
end
|
29
19
|
|
30
20
|
def recv(maxlen, flags = 0, outbuf = nil)
|
@@ -33,7 +23,7 @@ class ::Socket
|
|
33
23
|
result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
|
34
24
|
case result
|
35
25
|
when nil then raise IOError
|
36
|
-
when :wait_readable then Thread.current.
|
26
|
+
when :wait_readable then Thread.current.backend.wait_io(self, false)
|
37
27
|
else
|
38
28
|
return result
|
39
29
|
end
|
@@ -46,7 +36,7 @@ class ::Socket
|
|
46
36
|
result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
|
47
37
|
case result
|
48
38
|
when nil then raise IOError
|
49
|
-
when :wait_readable then Thread.current.
|
39
|
+
when :wait_readable then Thread.current.backend.wait_io(self, false)
|
50
40
|
else
|
51
41
|
return result
|
52
42
|
end
|
@@ -16,21 +16,22 @@ class ::Thread
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def execute
|
19
|
-
#
|
19
|
+
# backend must be created in the context of the new thread, therefore it
|
20
20
|
# cannot be created in Thread#initialize
|
21
|
-
@
|
21
|
+
@backend = Polyphony::Backend.new
|
22
22
|
setup
|
23
23
|
@ready = true
|
24
24
|
result = @block.(*@args)
|
25
25
|
rescue Polyphony::MoveOn, Polyphony::Terminate => e
|
26
26
|
result = e.value
|
27
|
-
rescue Exception =>
|
27
|
+
rescue Exception => e
|
28
|
+
result = e
|
28
29
|
ensure
|
29
30
|
@ready = true
|
30
31
|
finalize(result)
|
31
32
|
end
|
32
33
|
|
33
|
-
attr_accessor :
|
34
|
+
attr_accessor :backend
|
34
35
|
|
35
36
|
def setup
|
36
37
|
@main_fiber = Fiber.current
|
@@ -48,7 +49,7 @@ class ::Thread
|
|
48
49
|
@result = result
|
49
50
|
signal_waiters(result)
|
50
51
|
end
|
51
|
-
@
|
52
|
+
@backend.finalize
|
52
53
|
end
|
53
54
|
|
54
55
|
def signal_waiters(result)
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -21,17 +21,18 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.require_paths = ["lib"]
|
22
22
|
s.required_ruby_version = '>= 2.6'
|
23
23
|
|
24
|
-
s.add_development_dependency '
|
25
|
-
s.add_development_dependency 'localhost', '1.1.4'
|
24
|
+
s.add_development_dependency 'rake-compiler', '1.0.5'
|
26
25
|
s.add_development_dependency 'minitest', '5.13.0'
|
27
26
|
s.add_development_dependency 'minitest-reporters', '1.4.2'
|
28
27
|
s.add_development_dependency 'simplecov', '0.17.1'
|
29
28
|
s.add_development_dependency 'rubocop', '0.85.1'
|
29
|
+
s.add_development_dependency 'pry', '0.13.1'
|
30
|
+
|
30
31
|
s.add_development_dependency 'pg', '1.1.4'
|
31
|
-
s.add_development_dependency 'rake-compiler', '1.0.5'
|
32
32
|
s.add_development_dependency 'redis', '4.1.0'
|
33
33
|
s.add_development_dependency 'hiredis', '0.6.3'
|
34
34
|
s.add_development_dependency 'http_parser.rb', '~>0.6.0'
|
35
|
+
s.add_development_dependency 'rack', '>=2.0.8', '<2.3.0'
|
35
36
|
s.add_development_dependency 'mysql2', '0.5.3'
|
36
37
|
s.add_development_dependency 'sequel', '5.34.0'
|
37
38
|
|
data/test/helper.rb
CHANGED
@@ -31,7 +31,7 @@ class MiniTest::Test
|
|
31
31
|
end
|
32
32
|
Fiber.current.setup_main_fiber
|
33
33
|
Fiber.current.instance_variable_set(:@auto_watcher, nil)
|
34
|
-
Thread.current.
|
34
|
+
Thread.current.backend = Polyphony::Backend.new
|
35
35
|
sleep 0 # apparently this helps with timer accuracy
|
36
36
|
end
|
37
37
|
|
@@ -2,39 +2,39 @@
|
|
2
2
|
|
3
3
|
require_relative 'helper'
|
4
4
|
|
5
|
-
class
|
5
|
+
class BackendTest < MiniTest::Test
|
6
6
|
def setup
|
7
7
|
super
|
8
|
-
@
|
9
|
-
@
|
10
|
-
Thread.current.
|
8
|
+
@prev_backend = Thread.current.backend
|
9
|
+
@backend = Polyphony::Backend.new
|
10
|
+
Thread.current.backend = @backend
|
11
11
|
end
|
12
12
|
|
13
13
|
def teardown
|
14
|
-
@
|
15
|
-
Thread.current.
|
14
|
+
@backend.finalize
|
15
|
+
Thread.current.backend = @prev_backend
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_sleep
|
19
19
|
count = 0
|
20
20
|
t0 = Time.now
|
21
21
|
spin {
|
22
|
-
@
|
22
|
+
@backend.sleep 0.01
|
23
23
|
count += 1
|
24
|
-
@
|
24
|
+
@backend.sleep 0.01
|
25
25
|
count += 1
|
26
|
-
@
|
26
|
+
@backend.sleep 0.01
|
27
27
|
count += 1
|
28
28
|
}.await
|
29
|
-
|
29
|
+
assert_in_delta 0.03, Time.now - t0, 0.005
|
30
30
|
assert_equal 3, count
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_write_read_partial
|
34
34
|
i, o = IO.pipe
|
35
35
|
buf = +''
|
36
|
-
f = spin { @
|
37
|
-
@
|
36
|
+
f = spin { @backend.read(i, buf, 5, false) }
|
37
|
+
@backend.write(o, 'Hello world')
|
38
38
|
return_value = f.await
|
39
39
|
|
40
40
|
assert_equal 'Hello', buf
|
@@ -44,10 +44,10 @@ class AgentTest < MiniTest::Test
|
|
44
44
|
def test_write_read_to_eof_limited_buffer
|
45
45
|
i, o = IO.pipe
|
46
46
|
buf = +''
|
47
|
-
f = spin { @
|
48
|
-
@
|
47
|
+
f = spin { @backend.read(i, buf, 5, true) }
|
48
|
+
@backend.write(o, 'Hello')
|
49
49
|
snooze
|
50
|
-
@
|
50
|
+
@backend.write(o, ' world')
|
51
51
|
snooze
|
52
52
|
o.close
|
53
53
|
return_value = f.await
|
@@ -59,10 +59,10 @@ class AgentTest < MiniTest::Test
|
|
59
59
|
def test_write_read_to_eof
|
60
60
|
i, o = IO.pipe
|
61
61
|
buf = +''
|
62
|
-
f = spin { @
|
63
|
-
@
|
62
|
+
f = spin { @backend.read(i, buf, 10**6, true) }
|
63
|
+
@backend.write(o, 'Hello')
|
64
64
|
snooze
|
65
|
-
@
|
65
|
+
@backend.write(o, ' world')
|
66
66
|
snooze
|
67
67
|
o.close
|
68
68
|
return_value = f.await
|
@@ -73,11 +73,11 @@ class AgentTest < MiniTest::Test
|
|
73
73
|
|
74
74
|
def test_waitpid
|
75
75
|
pid = fork do
|
76
|
-
@
|
76
|
+
@backend.post_fork
|
77
77
|
exit(42)
|
78
78
|
end
|
79
79
|
|
80
|
-
result = @
|
80
|
+
result = @backend.waitpid(pid)
|
81
81
|
assert_equal [pid, 42], result
|
82
82
|
end
|
83
83
|
|
@@ -87,7 +87,7 @@ class AgentTest < MiniTest::Test
|
|
87
87
|
buf = []
|
88
88
|
spin do
|
89
89
|
buf << :ready
|
90
|
-
@
|
90
|
+
@backend.read_loop(i) { |d| buf << d }
|
91
91
|
buf << :done
|
92
92
|
end
|
93
93
|
|
@@ -107,7 +107,7 @@ class AgentTest < MiniTest::Test
|
|
107
107
|
|
108
108
|
clients = []
|
109
109
|
server_fiber = spin do
|
110
|
-
@
|
110
|
+
@backend.accept_loop(server) { |c| clients << c }
|
111
111
|
end
|
112
112
|
|
113
113
|
c1 = TCPSocket.new('127.0.0.1', 1234)
|