polyphony 0.13
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 +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
|