polyphony 0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +400 -0
  4. data/ext/ev/extconf.rb +19 -0
  5. data/lib/polyphony.rb +26 -0
  6. data/lib/polyphony/core.rb +45 -0
  7. data/lib/polyphony/core/async.rb +36 -0
  8. data/lib/polyphony/core/cancel_scope.rb +61 -0
  9. data/lib/polyphony/core/channel.rb +39 -0
  10. data/lib/polyphony/core/coroutine.rb +106 -0
  11. data/lib/polyphony/core/exceptions.rb +24 -0
  12. data/lib/polyphony/core/fiber_pool.rb +98 -0
  13. data/lib/polyphony/core/supervisor.rb +75 -0
  14. data/lib/polyphony/core/sync.rb +20 -0
  15. data/lib/polyphony/core/thread.rb +49 -0
  16. data/lib/polyphony/core/thread_pool.rb +58 -0
  17. data/lib/polyphony/core/throttler.rb +38 -0
  18. data/lib/polyphony/extensions/io.rb +62 -0
  19. data/lib/polyphony/extensions/kernel.rb +161 -0
  20. data/lib/polyphony/extensions/postgres.rb +96 -0
  21. data/lib/polyphony/extensions/redis.rb +68 -0
  22. data/lib/polyphony/extensions/socket.rb +85 -0
  23. data/lib/polyphony/extensions/ssl.rb +73 -0
  24. data/lib/polyphony/fs.rb +22 -0
  25. data/lib/polyphony/http/agent.rb +214 -0
  26. data/lib/polyphony/http/http1.rb +124 -0
  27. data/lib/polyphony/http/http1_request.rb +71 -0
  28. data/lib/polyphony/http/http2.rb +66 -0
  29. data/lib/polyphony/http/http2_request.rb +69 -0
  30. data/lib/polyphony/http/rack.rb +27 -0
  31. data/lib/polyphony/http/server.rb +43 -0
  32. data/lib/polyphony/line_reader.rb +82 -0
  33. data/lib/polyphony/net.rb +59 -0
  34. data/lib/polyphony/net_old.rb +299 -0
  35. data/lib/polyphony/resource_pool.rb +56 -0
  36. data/lib/polyphony/server_task.rb +18 -0
  37. data/lib/polyphony/testing.rb +34 -0
  38. data/lib/polyphony/version.rb +5 -0
  39. 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