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,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
|