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