polyphony 0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +86 -0
- data/README.md +400 -0
- data/ext/ev/extconf.rb +19 -0
- data/lib/polyphony.rb +26 -0
- data/lib/polyphony/core.rb +45 -0
- data/lib/polyphony/core/async.rb +36 -0
- data/lib/polyphony/core/cancel_scope.rb +61 -0
- data/lib/polyphony/core/channel.rb +39 -0
- data/lib/polyphony/core/coroutine.rb +106 -0
- data/lib/polyphony/core/exceptions.rb +24 -0
- data/lib/polyphony/core/fiber_pool.rb +98 -0
- data/lib/polyphony/core/supervisor.rb +75 -0
- data/lib/polyphony/core/sync.rb +20 -0
- data/lib/polyphony/core/thread.rb +49 -0
- data/lib/polyphony/core/thread_pool.rb +58 -0
- data/lib/polyphony/core/throttler.rb +38 -0
- data/lib/polyphony/extensions/io.rb +62 -0
- data/lib/polyphony/extensions/kernel.rb +161 -0
- data/lib/polyphony/extensions/postgres.rb +96 -0
- data/lib/polyphony/extensions/redis.rb +68 -0
- data/lib/polyphony/extensions/socket.rb +85 -0
- data/lib/polyphony/extensions/ssl.rb +73 -0
- data/lib/polyphony/fs.rb +22 -0
- data/lib/polyphony/http/agent.rb +214 -0
- data/lib/polyphony/http/http1.rb +124 -0
- data/lib/polyphony/http/http1_request.rb +71 -0
- data/lib/polyphony/http/http2.rb +66 -0
- data/lib/polyphony/http/http2_request.rb +69 -0
- data/lib/polyphony/http/rack.rb +27 -0
- data/lib/polyphony/http/server.rb +43 -0
- data/lib/polyphony/line_reader.rb +82 -0
- data/lib/polyphony/net.rb +59 -0
- data/lib/polyphony/net_old.rb +299 -0
- data/lib/polyphony/resource_pool.rb +56 -0
- data/lib/polyphony/server_task.rb +18 -0
- data/lib/polyphony/testing.rb +34 -0
- data/lib/polyphony/version.rb +5 -0
- metadata +170 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Client
|
4
|
+
|
5
|
+
require 'pg'
|
6
|
+
|
7
|
+
Core = import('../core')
|
8
|
+
|
9
|
+
module ::PG
|
10
|
+
def self.connect(*args)
|
11
|
+
Connection.connect_start(*args).tap(&method(:connect_async))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.connect_async(conn)
|
15
|
+
loop do
|
16
|
+
res = conn.connect_poll
|
17
|
+
case res
|
18
|
+
when PGRES_POLLING_FAILED then raise Error.new(conn.error_message)
|
19
|
+
when PGRES_POLLING_READING then conn.socket_io.read_watcher.await
|
20
|
+
when PGRES_POLLING_WRITING then conn.socket_io.write_watcher.await
|
21
|
+
when PGRES_POLLING_OK then
|
22
|
+
conn.setnonblocking(true)
|
23
|
+
return
|
24
|
+
end
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
conn.socket_io.stop_watchers
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.connect_sync(conn)
|
31
|
+
loop do
|
32
|
+
res = conn.connect_poll
|
33
|
+
case res
|
34
|
+
when PGRES_POLLING_FAILED then raise Error.new(conn.error_message)
|
35
|
+
when PGRES_POLLING_OK then
|
36
|
+
conn.setnonblocking(true)
|
37
|
+
return
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ::PG::Connection
|
44
|
+
alias_method :orig_get_result, :get_result
|
45
|
+
|
46
|
+
def get_result(&block)
|
47
|
+
while is_busy
|
48
|
+
socket_io.read_watcher.await
|
49
|
+
consume_input
|
50
|
+
end
|
51
|
+
orig_get_result(&block)
|
52
|
+
ensure
|
53
|
+
socket_io.stop_watchers
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :orig_async_exec, :async_exec
|
57
|
+
def async_exec(*args, &block)
|
58
|
+
send_query(*args)
|
59
|
+
result = get_result(&block)
|
60
|
+
while get_result; end
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def block(timeout = 0)
|
65
|
+
while is_busy
|
66
|
+
socket_io.read_watcher.await
|
67
|
+
consume_input
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
SQL_BEGIN = 'begin'
|
72
|
+
SQL_COMMIT = 'commit'
|
73
|
+
SQL_ROLLBACK = 'rollback'
|
74
|
+
|
75
|
+
# Starts a transaction, runs given block, and commits transaction. If an
|
76
|
+
# error is raised, the transaction is rolled back and the error is raised
|
77
|
+
# again.
|
78
|
+
# @return [void]
|
79
|
+
def transaction
|
80
|
+
began = false
|
81
|
+
return yield if @transaction # allow nesting of calls to #transactions
|
82
|
+
|
83
|
+
query(SQL_BEGIN)
|
84
|
+
began = true
|
85
|
+
@transaction = true
|
86
|
+
yield
|
87
|
+
query(SQL_COMMIT)
|
88
|
+
rescue StandardError => e
|
89
|
+
(query(SQL_ROLLBACK) rescue nil) if began
|
90
|
+
raise e
|
91
|
+
ensure
|
92
|
+
@transaction = false if began
|
93
|
+
end
|
94
|
+
|
95
|
+
self.async_api = true
|
96
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Connection
|
4
|
+
|
5
|
+
require "redis"
|
6
|
+
require "hiredis/reader"
|
7
|
+
|
8
|
+
Net = import('../net')
|
9
|
+
|
10
|
+
class Driver
|
11
|
+
def self.connect(config)
|
12
|
+
if config[:scheme] == "unix"
|
13
|
+
raise "unix sockets not supported"
|
14
|
+
# connection.connect_unix(config[:path], connect_timeout)
|
15
|
+
elsif config[:scheme] == "rediss" || config[:ssl]
|
16
|
+
raise "ssl not supported"
|
17
|
+
# raise NotImplementedError, "SSL not supported by hiredis driver"
|
18
|
+
else
|
19
|
+
new(config[:host], config[:port])
|
20
|
+
# connection.connect(config[:host], config[:port], connect_timeout)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(host, port)
|
25
|
+
@connection = Net.tcp_connect(host, port)
|
26
|
+
@reader = ::Hiredis::Reader.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def connected?
|
30
|
+
@connection && !@connection.closed?
|
31
|
+
end
|
32
|
+
|
33
|
+
def timeout=(timeout)
|
34
|
+
# ignore timeout for now
|
35
|
+
end
|
36
|
+
|
37
|
+
def disconnect
|
38
|
+
@connection.close
|
39
|
+
@connection = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(command)
|
43
|
+
@connection.write(format_command(command))
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_command(args)
|
47
|
+
(+"*#{args.size}\r\n").tap do |s|
|
48
|
+
args.each do |a|
|
49
|
+
a = a.to_s
|
50
|
+
s << "$#{a.bytesize}\r\n#{a}\r\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read
|
56
|
+
reply = @reader.gets
|
57
|
+
return reply if reply
|
58
|
+
|
59
|
+
loop do
|
60
|
+
data = @connection.read
|
61
|
+
@reader.feed(data)
|
62
|
+
reply = @reader.gets
|
63
|
+
return reply if reply
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Redis::Connection.drivers << Driver
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
import('./io')
|
6
|
+
|
7
|
+
class ::Socket
|
8
|
+
def accept
|
9
|
+
loop do
|
10
|
+
result, client_addr = accept_nonblock(::IO::NO_EXCEPTION)
|
11
|
+
case result
|
12
|
+
when Socket then return result
|
13
|
+
when :wait_readable then read_watcher.await
|
14
|
+
else
|
15
|
+
raise "failed to accept (#{result.inspect})"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
ensure
|
19
|
+
@read_watcher&.stop
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect(remotesockaddr)
|
23
|
+
loop do
|
24
|
+
result = connect_nonblock(remotesockaddr, ::IO::NO_EXCEPTION)
|
25
|
+
case result
|
26
|
+
when 0 then return
|
27
|
+
when :wait_writable then write_watcher.await
|
28
|
+
else raise IOError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
ensure
|
32
|
+
@write_watcher&.stop
|
33
|
+
end
|
34
|
+
|
35
|
+
def recvfrom(maxlen, flags = 0)
|
36
|
+
@read_buffer ||= +''
|
37
|
+
loop do
|
38
|
+
result = recvfrom_nonblock(maxlen, flags, @read_buffer, ::IO::NO_EXCEPTION)
|
39
|
+
case result
|
40
|
+
when nil then raise IOError
|
41
|
+
when :wait_readable then read_watcher.await
|
42
|
+
else return result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
ensure
|
46
|
+
@read_watcher&.stop
|
47
|
+
end
|
48
|
+
|
49
|
+
ZERO_LINGER = [0, 0].pack("ii")
|
50
|
+
|
51
|
+
def dont_linger
|
52
|
+
setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, ZERO_LINGER)
|
53
|
+
end
|
54
|
+
|
55
|
+
def no_delay
|
56
|
+
setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
57
|
+
end
|
58
|
+
|
59
|
+
def reuse_addr
|
60
|
+
setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
alias_method :orig_getaddrinfo, :getaddrinfo
|
65
|
+
def getaddrinfo(*args)
|
66
|
+
Polyphony::ThreadPool.process { orig_getaddrinfo(*args) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class ::TCPServer
|
72
|
+
def accept
|
73
|
+
loop do
|
74
|
+
result, client_addr = accept_nonblock(::IO::NO_EXCEPTION)
|
75
|
+
case result
|
76
|
+
when TCPSocket then return result
|
77
|
+
when :wait_readable then read_watcher.await
|
78
|
+
else
|
79
|
+
raise "failed to accept (#{result.inspect})"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
ensure
|
83
|
+
@read_watcher&.stop
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
import('./socket')
|
6
|
+
|
7
|
+
class ::OpenSSL::SSL::SSLSocket
|
8
|
+
def accept
|
9
|
+
loop do
|
10
|
+
result = accept_nonblock(::IO::NO_EXCEPTION)
|
11
|
+
case result
|
12
|
+
when :wait_readable then io.read_watcher.await
|
13
|
+
when :wait_writable then io.write_watcher.await
|
14
|
+
else return true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
ensure
|
18
|
+
io.stop_watchers
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect
|
22
|
+
loop do
|
23
|
+
result = connect_nonblock(::IO::NO_EXCEPTION)
|
24
|
+
case result
|
25
|
+
when :wait_readable then io.read_watcher.await
|
26
|
+
when :wait_writable then io.write_watcher.await
|
27
|
+
else return true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
io.stop_watchers
|
32
|
+
end
|
33
|
+
|
34
|
+
def read(max = 8192)
|
35
|
+
@read_buffer ||= +''
|
36
|
+
loop do
|
37
|
+
result = read_nonblock(max, @read_buffer, ::IO::NO_EXCEPTION)
|
38
|
+
case result
|
39
|
+
when nil then raise ::IOError
|
40
|
+
when :wait_readable then io.read_watcher.await
|
41
|
+
else return result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
io.stop_watchers
|
46
|
+
end
|
47
|
+
|
48
|
+
def write(data)
|
49
|
+
loop do
|
50
|
+
result = write_nonblock(data, ::IO::NO_EXCEPTION)
|
51
|
+
case result
|
52
|
+
when nil then raise ::IOError
|
53
|
+
when :wait_writable then io.write_watcher.await
|
54
|
+
else
|
55
|
+
(result == data.bytesize) ? (return result) : (data = data[result..-1])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
io.stop_watchers
|
60
|
+
end
|
61
|
+
|
62
|
+
def dont_linger
|
63
|
+
io.dont_linger
|
64
|
+
end
|
65
|
+
|
66
|
+
def no_delay
|
67
|
+
io.no_delay
|
68
|
+
end
|
69
|
+
|
70
|
+
def reuse_addr
|
71
|
+
io.reuse_addr
|
72
|
+
end
|
73
|
+
end
|
data/lib/polyphony/fs.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :stat,
|
4
|
+
:read
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
ThreadPool = import('./core/thread_pool')
|
9
|
+
|
10
|
+
::File.singleton_class.instance_eval do
|
11
|
+
alias_method :orig_stat, :stat
|
12
|
+
def stat(path)
|
13
|
+
ThreadPool.process { orig_stat(path) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
::IO.singleton_class.instance_eval do
|
18
|
+
alias_method :orig_read, :read
|
19
|
+
def read(path)
|
20
|
+
ThreadPool.process { orig_read(path) }
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Agent
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
require 'http/parser'
|
7
|
+
require 'http/2'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
ResourcePool = import('../resource_pool')
|
11
|
+
|
12
|
+
module ResponseMixin
|
13
|
+
def body
|
14
|
+
self[:body]
|
15
|
+
end
|
16
|
+
|
17
|
+
def json
|
18
|
+
@json ||= ::JSON.parse(self[:body])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Implements an HTTP agent
|
23
|
+
class Agent
|
24
|
+
def self.get(url, query = nil)
|
25
|
+
default.get(url, query)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.post(url, query = nil)
|
29
|
+
default.post(url, query)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.default
|
33
|
+
@default ||= new
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(max_conns = 6)
|
37
|
+
@pools = Hash.new do |h, k|
|
38
|
+
h[k] = ResourcePool.new(limit: max_conns) { {} }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(url, query = nil)
|
43
|
+
request(url, method: :GET, query: query)
|
44
|
+
end
|
45
|
+
|
46
|
+
def post(url, query = nil)
|
47
|
+
request(url, method: :POST, query: query)
|
48
|
+
end
|
49
|
+
|
50
|
+
S_LOCATION = 'Location'
|
51
|
+
|
52
|
+
OPTS_DEFAULT = {}.freeze
|
53
|
+
|
54
|
+
def request(url, opts = OPTS_DEFAULT)
|
55
|
+
ctx = request_ctx(url, opts)
|
56
|
+
response = do_request(ctx)
|
57
|
+
|
58
|
+
case response[:status_code]
|
59
|
+
when 301, 302
|
60
|
+
request(response[:headers][S_LOCATION])
|
61
|
+
when 200, 204
|
62
|
+
response.extend(ResponseMixin)
|
63
|
+
else
|
64
|
+
raise "Error received from server: #{response[:status_code]}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def request_ctx(url, opts)
|
69
|
+
{
|
70
|
+
method: opts[:method] || :GET,
|
71
|
+
uri: url_to_uri(url, opts),
|
72
|
+
opts: opts
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def url_to_uri(url, opts)
|
77
|
+
uri = URI(url)
|
78
|
+
if opts[:query]
|
79
|
+
query = opts[:query].map { |k, v| "#{k}=#{v}" }.join("&")
|
80
|
+
if uri.query
|
81
|
+
v.query = "#{uri.query}&#{query}"
|
82
|
+
else
|
83
|
+
uri.query = query
|
84
|
+
end
|
85
|
+
end
|
86
|
+
uri
|
87
|
+
end
|
88
|
+
|
89
|
+
def do_request(ctx)
|
90
|
+
key = uri_key(ctx[:uri])
|
91
|
+
@pools[key].acquire do |state|
|
92
|
+
state[:socket] ||= connect(key)
|
93
|
+
state[:protocol_method] ||= protocol_method(state[:socket], ctx)
|
94
|
+
send(state[:protocol_method], state, ctx)
|
95
|
+
rescue => e
|
96
|
+
state[:socket]&.close rescue nil
|
97
|
+
state.clear
|
98
|
+
raise e
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
S_H2 = 'h2'
|
103
|
+
|
104
|
+
def protocol_method(socket, ctx)
|
105
|
+
if socket.is_a?(::OpenSSL::SSL::SSLSocket) && (socket.alpn_protocol == S_H2)
|
106
|
+
:do_http2
|
107
|
+
else
|
108
|
+
:do_http1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def do_http1(state, ctx)
|
113
|
+
done = false
|
114
|
+
body = +''
|
115
|
+
parser = HTTP::Parser.new
|
116
|
+
parser.on_message_complete = proc { done = true }
|
117
|
+
parser.on_body = proc { |data| body << data }
|
118
|
+
request = format_http1_request(ctx)
|
119
|
+
|
120
|
+
state[:socket] << request
|
121
|
+
while !done
|
122
|
+
parser << state[:socket].read
|
123
|
+
end
|
124
|
+
|
125
|
+
{
|
126
|
+
protocol: 'http1.1',
|
127
|
+
status_code: parser.status_code,
|
128
|
+
headers: parser.headers,
|
129
|
+
body: body
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def do_http2(state, ctx)
|
134
|
+
unless state[:http2_client]
|
135
|
+
socket, client = state[:socket], HTTP2::Client.new
|
136
|
+
client.on(:frame) {|bytes| socket << bytes }
|
137
|
+
state[:http2_client] = client
|
138
|
+
end
|
139
|
+
|
140
|
+
stream = state[:http2_client].new_stream # allocate new stream
|
141
|
+
|
142
|
+
headers = {
|
143
|
+
':scheme' => ctx[:uri].scheme,
|
144
|
+
':method' => ctx[:method].to_s,
|
145
|
+
':path' => ctx[:uri].request_uri,
|
146
|
+
':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
|
147
|
+
}
|
148
|
+
headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
|
149
|
+
|
150
|
+
if ctx[:opts][:payload]
|
151
|
+
stream.headers(headers, end_stream: false)
|
152
|
+
stream.data(ctx[:opts][:payload], end_stream: true)
|
153
|
+
else
|
154
|
+
stream.headers(headers, end_stream: true)
|
155
|
+
end
|
156
|
+
|
157
|
+
headers = nil
|
158
|
+
body = +''
|
159
|
+
done = nil
|
160
|
+
|
161
|
+
stream.on(:headers) { |h| headers = h.to_h }
|
162
|
+
stream.on(:data) { |c| body << c }
|
163
|
+
stream.on(:close) {
|
164
|
+
done = true
|
165
|
+
return {
|
166
|
+
protocol: 'http1.1',
|
167
|
+
status_code: headers && headers[':status'].to_i,
|
168
|
+
headers: headers || {},
|
169
|
+
body: body
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
while data = state[:socket].read
|
174
|
+
state[:http2_client] << data
|
175
|
+
end
|
176
|
+
ensure
|
177
|
+
(stream.close rescue nil) unless done
|
178
|
+
end
|
179
|
+
|
180
|
+
HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n\r\n"
|
181
|
+
|
182
|
+
def format_http1_request(ctx)
|
183
|
+
HTTP1_REQUEST % {
|
184
|
+
method: ctx[:method],
|
185
|
+
request: ctx[:uri].request_uri,
|
186
|
+
host: ctx[:uri].host
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
def uri_key(uri)
|
191
|
+
{
|
192
|
+
scheme: uri.scheme,
|
193
|
+
host: uri.host,
|
194
|
+
port: uri.port
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
S_HTTP = 'http'
|
199
|
+
S_HTTPS = 'https'
|
200
|
+
SECURE_OPTS = { secure: true, alpn_protocols: ['h2', 'http/1.1'] }
|
201
|
+
|
202
|
+
def connect(key)
|
203
|
+
case key[:scheme]
|
204
|
+
when S_HTTP
|
205
|
+
Polyphony::Net.tcp_connect(key[:host], key[:port])
|
206
|
+
when S_HTTPS
|
207
|
+
Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS).tap do |socket|
|
208
|
+
socket.post_connection_check(key[:host])
|
209
|
+
end
|
210
|
+
else
|
211
|
+
raise "Invalid scheme #{key[:scheme].inspect}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|