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,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :run
|
4
|
+
|
5
|
+
require 'http/parser'
|
6
|
+
|
7
|
+
Request = import('./http1_request')
|
8
|
+
HTTP2 = import('./http2')
|
9
|
+
|
10
|
+
class Http::Parser
|
11
|
+
def async!
|
12
|
+
self.on_message_complete = proc { @request_complete = true }
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(data)
|
17
|
+
self << data
|
18
|
+
return nil unless @request_complete
|
19
|
+
|
20
|
+
@request_complete = nil
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets up parsing and handling of request/response cycle
|
26
|
+
# @param socket [Net::Socket] socket
|
27
|
+
# @param handler [Proc] request handler
|
28
|
+
# @return [void]
|
29
|
+
def run(socket, handler)
|
30
|
+
ctx = connection_context(socket, handler)
|
31
|
+
ctx[:parser].on_body = proc { |chunk| handle_body_chunk(ctx, chunk) }
|
32
|
+
|
33
|
+
loop do
|
34
|
+
data = socket.read
|
35
|
+
if ctx[:parser].parse(data)
|
36
|
+
break unless handle_request(ctx)
|
37
|
+
EV.snooze
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue IOError, SystemCallError => e
|
41
|
+
# do nothing
|
42
|
+
ensure
|
43
|
+
socket.close
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a context hash for the given socket. This hash contains references
|
47
|
+
# related to the connection and its current state
|
48
|
+
# @param socket [Net::Socket] socket
|
49
|
+
# @param handler [Proc] request handler
|
50
|
+
# @return [Hash]
|
51
|
+
def connection_context(socket, handler)
|
52
|
+
{
|
53
|
+
can_upgrade: true,
|
54
|
+
count: 0,
|
55
|
+
socket: socket,
|
56
|
+
handler: handler,
|
57
|
+
parser: Http::Parser.new.async!,
|
58
|
+
body: nil,
|
59
|
+
request: Request.new
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Adds given chunk to request body
|
64
|
+
# @param ctx [Hash] connection context
|
65
|
+
# @return [void]
|
66
|
+
def handle_body_chunk(context, chunk)
|
67
|
+
|
68
|
+
context[:body] ||= +''
|
69
|
+
context[:body] << chunk
|
70
|
+
end
|
71
|
+
|
72
|
+
# Handles request, upgrading the connection if possible
|
73
|
+
# @param ctx [Hash] connection context
|
74
|
+
# @return [boolean] true if HTTP 1 loop should continue handling socket
|
75
|
+
def handle_request(ctx)
|
76
|
+
return nil if ctx[:can_upgrade] && upgrade_connection(ctx)
|
77
|
+
|
78
|
+
# allow upgrading the connection only on first request
|
79
|
+
ctx[:can_upgrade] = false
|
80
|
+
ctx[:request].setup(ctx[:socket], ctx[:parser], ctx[:body])
|
81
|
+
ctx[:handler].(ctx[:request])
|
82
|
+
|
83
|
+
if ctx[:parser].keep_alive?
|
84
|
+
ctx[:body] = nil
|
85
|
+
true
|
86
|
+
else
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
S_EMPTY = ''
|
92
|
+
S_UPGRADE = 'Upgrade'
|
93
|
+
S_H2C = 'h2c'
|
94
|
+
S_SCHEME = ':scheme'
|
95
|
+
S_METHOD = ':method'
|
96
|
+
S_AUTHORITY = ':authority'
|
97
|
+
S_PATH = ':path'
|
98
|
+
S_HTTP = 'http'
|
99
|
+
S_HOST = 'Host'
|
100
|
+
|
101
|
+
# Upgrades an HTTP 1 connection to HTTP 2 on client request
|
102
|
+
# @param ctx [Hash] connection context
|
103
|
+
# @return [Boolean] true if connection was upgraded
|
104
|
+
def upgrade_connection(ctx)
|
105
|
+
return false unless ctx[:parser].headers[S_UPGRADE] == S_H2C
|
106
|
+
|
107
|
+
request = http2_upgraded_request(ctx)
|
108
|
+
body = ctx[:body] || S_EMPTY
|
109
|
+
HTTP2.upgrade(ctx[:socket], ctx[:handler], request, body)
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a request hash for handling by upgraded HTTP 2 connection
|
114
|
+
# @param ctx [Hash] connection context
|
115
|
+
# @return [Hash]
|
116
|
+
def http2_upgraded_request(ctx)
|
117
|
+
headers = ctx[:parser].headers
|
118
|
+
headers.merge(
|
119
|
+
S_SCHEME => S_HTTP,
|
120
|
+
S_METHOD => ctx[:parser].http_method,
|
121
|
+
S_AUTHORITY => headers[S_HOST],
|
122
|
+
S_PATH => ctx[:parser].request_url
|
123
|
+
)
|
124
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Request
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Request
|
8
|
+
def setup(conn, parser, body)
|
9
|
+
@conn = conn
|
10
|
+
@parser = parser
|
11
|
+
@method = parser.http_method
|
12
|
+
@request_url = parser.request_url
|
13
|
+
@body = body
|
14
|
+
end
|
15
|
+
|
16
|
+
def method
|
17
|
+
@method ||= @parser.http_method
|
18
|
+
end
|
19
|
+
|
20
|
+
S_EMPTY = ''
|
21
|
+
|
22
|
+
def path
|
23
|
+
@uri ||= URI.parse(@parser.request_url || S_EMPTY)
|
24
|
+
@path ||= @uri.path
|
25
|
+
end
|
26
|
+
|
27
|
+
S_AMPERSAND = '&'
|
28
|
+
S_EQUAL = '='
|
29
|
+
|
30
|
+
def query
|
31
|
+
@uri ||= URI.parse(@parser.request_url || S_EMPTY)
|
32
|
+
return @query if @query
|
33
|
+
|
34
|
+
if (q = u.query)
|
35
|
+
@query = q.split(S_AMPERSAND).each_with_object({}) do |kv, h|
|
36
|
+
k, v = kv.split(S_EQUAL)
|
37
|
+
h[k.to_sym] = URI.decode_www_form_component(v)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
@query = {}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def headers
|
45
|
+
@headers ||= @parser.headers
|
46
|
+
end
|
47
|
+
|
48
|
+
S_CONTENT_LENGTH = 'Content-Length'
|
49
|
+
S_STATUS = ':status'
|
50
|
+
EMPTY_LINE = "\r\n"
|
51
|
+
|
52
|
+
def respond(body, headers = {})
|
53
|
+
status = headers.delete(S_STATUS) || 200
|
54
|
+
data = +"HTTP/1.1 #{status}\r\n"
|
55
|
+
headers[S_CONTENT_LENGTH] = body.bytesize if body
|
56
|
+
headers.each do |k, v|
|
57
|
+
if v.is_a?(Array)
|
58
|
+
v.each { |vv| data << "#{k}: #{vv}\r\n" }
|
59
|
+
else
|
60
|
+
data << "#{k}: #{v}\r\n"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
if body
|
64
|
+
data << "\r\n#{body}"
|
65
|
+
else
|
66
|
+
data << EMPTY_LINE
|
67
|
+
end
|
68
|
+
|
69
|
+
@conn << data
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :run, :upgrade
|
4
|
+
|
5
|
+
require 'http/2'
|
6
|
+
|
7
|
+
Request = import('./http2_request')
|
8
|
+
|
9
|
+
S_HTTP2_SETTINGS = 'HTTP2-Settings'
|
10
|
+
|
11
|
+
UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
|
12
|
+
HTTP/1.1 101 Switching Protocols
|
13
|
+
Connection: Upgrade
|
14
|
+
Upgrade: h2c
|
15
|
+
|
16
|
+
HTTP
|
17
|
+
|
18
|
+
def upgrade(socket, handler, request, body)
|
19
|
+
interface = prepare(socket, handler)
|
20
|
+
settings = request[S_HTTP2_SETTINGS]
|
21
|
+
socket.write(UPGRADE_MESSAGE)
|
22
|
+
interface.upgrade(settings, request, body)
|
23
|
+
client_loop(socket, interface)
|
24
|
+
end
|
25
|
+
|
26
|
+
def prepare(socket, handler)
|
27
|
+
::HTTP2::Server.new.tap do |interface|
|
28
|
+
interface.on(:frame) { |bytes| socket << bytes }
|
29
|
+
interface.on(:stream) { |stream| start_stream(stream, handler) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def run(socket, handler)
|
34
|
+
interface = prepare(socket, handler)
|
35
|
+
client_loop(socket, interface)
|
36
|
+
end
|
37
|
+
|
38
|
+
def client_loop(socket, interface)
|
39
|
+
loop do
|
40
|
+
data = socket.read
|
41
|
+
interface << data
|
42
|
+
EV.snooze
|
43
|
+
end
|
44
|
+
rescue IOError, SystemCallError => e
|
45
|
+
# do nothing
|
46
|
+
rescue StandardError => e
|
47
|
+
puts "error in HTTP2 parse_incoming_data: #{e.inspect}"
|
48
|
+
puts e.backtrace.join("\n")
|
49
|
+
ensure
|
50
|
+
socket.close
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handles HTTP 2 stream
|
54
|
+
# @param stream [HTTP2::Stream] HTTP 2 stream
|
55
|
+
# @param handler [Proc] request handler
|
56
|
+
# @return [void]
|
57
|
+
def start_stream(stream, handler)
|
58
|
+
request = Request.new(stream)
|
59
|
+
|
60
|
+
# stream.on(:active) { puts 'client opened new stream' }
|
61
|
+
# stream.on(:close) { puts 'stream closed' }
|
62
|
+
|
63
|
+
stream.on(:headers) { |h| request.set_headers(h) }
|
64
|
+
stream.on(:data) { |data| request.add_body_chunk(chunk) }
|
65
|
+
stream.on(:half_close) { handler.(request) }
|
66
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Request
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Request
|
8
|
+
def initialize(stream)
|
9
|
+
@stream = stream
|
10
|
+
end
|
11
|
+
|
12
|
+
def set_headers(headers)
|
13
|
+
@headers = Hash[*headers.flatten]
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_body_chunk(chunk)
|
17
|
+
if @body
|
18
|
+
@body << chunk
|
19
|
+
else
|
20
|
+
@body = +chunk
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
S_METHOD = ':method'
|
25
|
+
|
26
|
+
def method
|
27
|
+
@method ||= @headers[S_METHOD]
|
28
|
+
end
|
29
|
+
|
30
|
+
def scheme
|
31
|
+
@scheme ||= @headers[':scheme']
|
32
|
+
end
|
33
|
+
|
34
|
+
S_EMPTY = ''
|
35
|
+
|
36
|
+
def path
|
37
|
+
@uri ||= URI.parse(@headers[':path'] || S_EMPTY)
|
38
|
+
@path ||= @uri.path
|
39
|
+
end
|
40
|
+
|
41
|
+
S_AMPERSAND = '&'
|
42
|
+
S_EQUAL = '='
|
43
|
+
|
44
|
+
def query
|
45
|
+
@uri ||= URI.parse(@headers[':path'] || S_EMPTY)
|
46
|
+
return @query if @query
|
47
|
+
|
48
|
+
if (q = u.query)
|
49
|
+
@query = q.split(S_AMPERSAND).each_with_object({}) do |kv, h|
|
50
|
+
k, v = kv.split(S_EQUAL)
|
51
|
+
h[k.to_sym] = URI.decode_www_form_component(v)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
@query = {}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
S_CONTENT_LENGTH = 'Content-Length'
|
59
|
+
S_STATUS = ':status'
|
60
|
+
S_STATUS_200 = '200'
|
61
|
+
EMPTY_LINE = "\r\n"
|
62
|
+
|
63
|
+
def respond(body, headers = {})
|
64
|
+
headers[S_STATUS] ||= S_STATUS_200
|
65
|
+
|
66
|
+
@stream.headers(headers, end_stream: false)
|
67
|
+
@stream.data(body, end_stream: true)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :load
|
4
|
+
|
5
|
+
def run(app)
|
6
|
+
->(req) {
|
7
|
+
response = app.(env(req))
|
8
|
+
respond(req, response)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def load(path)
|
13
|
+
src = IO.read(path)
|
14
|
+
instance_eval(src)
|
15
|
+
end
|
16
|
+
|
17
|
+
def env(request)
|
18
|
+
{ }
|
19
|
+
end
|
20
|
+
|
21
|
+
S_STATUS = ':status'
|
22
|
+
|
23
|
+
def respond(request, (status_code, headers, body))
|
24
|
+
headers[S_STATUS] = status_code.to_s
|
25
|
+
body = body.first
|
26
|
+
request.respond(body, headers)
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :serve, :listen
|
4
|
+
|
5
|
+
Net = import('../net')
|
6
|
+
HTTP1 = import('./http1')
|
7
|
+
HTTP2 = import('./http2')
|
8
|
+
|
9
|
+
ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
|
10
|
+
H2_PROTOCOL = 'h2'
|
11
|
+
|
12
|
+
async def serve(host, port, opts = {}, &handler)
|
13
|
+
opts[:alpn_protocols] = ALPN_PROTOCOLS
|
14
|
+
server = Net.tcp_listen(host, port, opts)
|
15
|
+
|
16
|
+
accept_loop(server, handler)
|
17
|
+
end
|
18
|
+
|
19
|
+
def listen(host, port, opts = {}, &handler)
|
20
|
+
opts[:alpn_protocols] = ALPN_PROTOCOLS
|
21
|
+
server = Net.tcp_listen(host, port, opts)
|
22
|
+
proc { accept_loop(server, handler) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def accept_loop(server, handler)
|
26
|
+
while true
|
27
|
+
client = server.accept
|
28
|
+
spawn client_task(client, handler)
|
29
|
+
end
|
30
|
+
rescue OpenSSL::SSL::SSLError
|
31
|
+
retry # disregard
|
32
|
+
end
|
33
|
+
|
34
|
+
async def client_task(client, handler)
|
35
|
+
client.no_delay
|
36
|
+
protocol_module(client).run(client, handler)
|
37
|
+
end
|
38
|
+
|
39
|
+
def protocol_module(socket)
|
40
|
+
use_http2 = socket.respond_to?(:alpn_protocol) &&
|
41
|
+
socket.alpn_protocol == H2_PROTOCOL
|
42
|
+
use_http2 ? HTTP2 : HTTP1
|
43
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :LineReader
|
4
|
+
|
5
|
+
Core = import('./core')
|
6
|
+
|
7
|
+
# a stream that can read single lines from another stream
|
8
|
+
class LineReader
|
9
|
+
# Initializes the line reader with a source and optional line separator
|
10
|
+
# @param source [Stream] source stream
|
11
|
+
# @param sep [String] line separator
|
12
|
+
def initialize(source = nil, sep = $/)
|
13
|
+
@source = source
|
14
|
+
if source
|
15
|
+
source.on(:data) { |data| push(data) }
|
16
|
+
source.on(:close) { close }
|
17
|
+
source.on(:error) { |err| error(err) }
|
18
|
+
end
|
19
|
+
@read_buffer = +''
|
20
|
+
@separator = sep
|
21
|
+
@separator_size = sep.bytesize
|
22
|
+
end
|
23
|
+
|
24
|
+
# Pushes data into the read buffer and emits lines
|
25
|
+
# @param data [String] data to be read
|
26
|
+
# @return [void]
|
27
|
+
def push(data)
|
28
|
+
@read_buffer << data
|
29
|
+
emit_lines
|
30
|
+
end
|
31
|
+
|
32
|
+
# Emits lines from the read buffer
|
33
|
+
# @return [void]
|
34
|
+
def emit_lines
|
35
|
+
while (line = _gets)
|
36
|
+
@lines_promise.resolve(line)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a line sliced from the read buffer
|
41
|
+
# @return [String] line
|
42
|
+
def _gets
|
43
|
+
idx = @read_buffer.index(@separator)
|
44
|
+
idx && @read_buffer.slice!(0, idx + @separator_size)
|
45
|
+
end
|
46
|
+
|
47
|
+
def gets
|
48
|
+
Core.promise do |p|
|
49
|
+
@lines_promise = p
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a async generator of lines
|
54
|
+
# @return [Promise] line generator
|
55
|
+
def lines
|
56
|
+
Core.generator do |p|
|
57
|
+
@lines_promise = p
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Iterates asynchronously over lines received
|
62
|
+
# @return [void]
|
63
|
+
def each_line(&block)
|
64
|
+
lines.each(&block)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Closes the stream and cancels any pending reads
|
68
|
+
# @return [void]
|
69
|
+
def close
|
70
|
+
@lines_promise&.stop
|
71
|
+
end
|
72
|
+
|
73
|
+
# handles error generated by source
|
74
|
+
# @param err [Exception] raised error
|
75
|
+
# @return [void]
|
76
|
+
def error(err)
|
77
|
+
return unless @lines_promise
|
78
|
+
|
79
|
+
@lines_promise.stop
|
80
|
+
@lines_promise.reject(err)
|
81
|
+
end
|
82
|
+
end
|