polyphony 0.17 → 0.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile.lock +11 -3
  4. data/README.md +18 -18
  5. data/TODO.md +5 -21
  6. data/examples/core/channel_echo.rb +3 -3
  7. data/examples/core/enumerator.rb +1 -1
  8. data/examples/core/fork.rb +1 -1
  9. data/examples/core/genserver.rb +1 -1
  10. data/examples/core/lock.rb +3 -3
  11. data/examples/core/multiple_spawn.rb +2 -2
  12. data/examples/core/nested_async.rb +1 -1
  13. data/examples/core/nested_multiple_spawn.rb +3 -3
  14. data/examples/core/resource_cancel.rb +1 -1
  15. data/examples/core/sleep_spawn.rb +2 -2
  16. data/examples/core/spawn.rb +1 -1
  17. data/examples/core/spawn_cancel.rb +1 -1
  18. data/examples/core/spawn_error.rb +4 -4
  19. data/examples/core/supervisor.rb +1 -1
  20. data/examples/core/supervisor_with_error.rb +1 -1
  21. data/examples/core/supervisor_with_manual_move_on.rb +1 -1
  22. data/examples/core/thread.rb +2 -2
  23. data/examples/core/thread_cancel.rb +2 -2
  24. data/examples/core/thread_pool.rb +1 -1
  25. data/examples/core/throttle.rb +3 -3
  26. data/examples/core/timeout.rb +10 -0
  27. data/examples/fs/read.rb +1 -1
  28. data/examples/http/http_client.rb +1 -1
  29. data/examples/http/http_get.rb +7 -0
  30. data/examples/http/http_parse_experiment.rb +118 -0
  31. data/examples/http/http_proxy.rb +81 -0
  32. data/examples/http/http_server.rb +15 -4
  33. data/examples/http/http_server_forked.rb +2 -2
  34. data/examples/http/http_server_throttled.rb +1 -1
  35. data/examples/http/http_ws_server.rb +2 -2
  36. data/examples/http/https_server.rb +5 -1
  37. data/examples/http/https_wss_server.rb +1 -1
  38. data/examples/http/rack_server_https_forked.rb +1 -1
  39. data/examples/interfaces/pg_client.rb +1 -1
  40. data/examples/interfaces/pg_pool.rb +1 -1
  41. data/examples/interfaces/redis_channels.rb +5 -5
  42. data/examples/interfaces/redis_pubsub.rb +2 -2
  43. data/examples/interfaces/redis_pubsub_perf.rb +3 -3
  44. data/examples/io/echo_client.rb +2 -2
  45. data/examples/io/echo_pipe.rb +17 -0
  46. data/examples/io/echo_server.rb +1 -1
  47. data/examples/io/echo_server_with_timeout.rb +1 -1
  48. data/examples/io/httparty.rb +10 -0
  49. data/examples/io/httparty_multi.rb +29 -0
  50. data/examples/io/httparty_threaded.rb +25 -0
  51. data/examples/io/irb.rb +15 -0
  52. data/examples/io/net-http.rb +15 -0
  53. data/examples/io/system.rb +1 -1
  54. data/examples/io/tcpsocket.rb +18 -0
  55. data/examples/performance/perf_multi_snooze.rb +2 -2
  56. data/examples/performance/perf_snooze.rb +17 -20
  57. data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -1
  58. data/ext/ev/ev.h +9 -1
  59. data/ext/ev/ev_ext.c +4 -1
  60. data/ext/ev/ev_module.c +36 -22
  61. data/ext/ev/extconf.rb +1 -1
  62. data/ext/ev/io.c +23 -23
  63. data/ext/ev/signal.c +1 -1
  64. data/ext/ev/socket.c +161 -0
  65. data/lib/polyphony/core/coprocess.rb +1 -1
  66. data/lib/polyphony/core/fiber_pool.rb +2 -2
  67. data/lib/polyphony/core/supervisor.rb +2 -18
  68. data/lib/polyphony/extensions/io.rb +19 -6
  69. data/lib/polyphony/extensions/kernel.rb +17 -5
  70. data/lib/polyphony/extensions/socket.rb +40 -1
  71. data/lib/polyphony/http/agent.rb +56 -25
  72. data/lib/polyphony/http/http1_adapter.rb +254 -0
  73. data/lib/polyphony/http/http2_adapter.rb +157 -0
  74. data/lib/polyphony/http/{http2_request.rb → request.rb} +25 -22
  75. data/lib/polyphony/http/server.rb +19 -11
  76. data/lib/polyphony/net.rb +10 -6
  77. data/lib/polyphony/version.rb +1 -1
  78. data/polyphony.gemspec +6 -5
  79. data/test/test_coprocess.rb +9 -9
  80. data/test/test_core.rb +14 -14
  81. data/test/test_io.rb +4 -4
  82. data/test/test_kernel.rb +1 -1
  83. metadata +48 -23
  84. data/lib/polyphony/http/http1.rb +0 -124
  85. data/lib/polyphony/http/http1_request.rb +0 -83
  86. data/lib/polyphony/http/http2.rb +0 -65
@@ -34,6 +34,19 @@ class ::Socket
34
34
  @write_watcher&.stop
35
35
  end
36
36
 
37
+ def recv(maxlen, flags = 0, outbuf = nil)
38
+ outbuf ||= +''
39
+ loop do
40
+ result = recv_nonblock(maxlen, flags, outbuf, NO_EXCEPTION)
41
+ case result
42
+ when :wait_readable
43
+ read_watcher.await
44
+ else
45
+ return result
46
+ end
47
+ end
48
+ end
49
+
37
50
  def recvfrom(maxlen, flags = 0)
38
51
  @read_buffer ||= +''
39
52
  loop do
@@ -70,6 +83,32 @@ class ::Socket
70
83
  end
71
84
  end
72
85
 
86
+ class ::TCPSocket
87
+ NO_EXCEPTION = { exception: false }.freeze
88
+
89
+ def foo; :bar; end
90
+
91
+ def initialize(remote_host, remote_port, local_host=nil, local_port=nil)
92
+ @io = Socket.new Socket::AF_INET, Socket::SOCK_STREAM
93
+ if local_host && local_port
94
+ @io.bind(Addrinfo.tcp(local_host, local_port))
95
+ end
96
+ @io.connect(Addrinfo.tcp(remote_host, remote_port))
97
+ end
98
+
99
+ def close
100
+ @io.close
101
+ end
102
+
103
+ def setsockopt(*args)
104
+ @io.setsockopt(*args)
105
+ end
106
+
107
+ def closed?
108
+ @io.closed?
109
+ end
110
+ end
111
+
73
112
  class ::TCPServer
74
113
  NO_EXCEPTION = { exception: false }.freeze
75
114
 
@@ -86,4 +125,4 @@ class ::TCPServer
86
125
  ensure
87
126
  @read_watcher&.stop
88
127
  end
89
- end
128
+ end
@@ -21,12 +21,12 @@ end
21
21
 
22
22
  # Implements an HTTP agent
23
23
  class Agent
24
- def self.get(url, query = nil)
25
- default.get(url, query)
24
+ def self.get(*args)
25
+ default.get(*args)
26
26
  end
27
27
 
28
- def self.post(url, query = nil)
29
- default.post(url, query)
28
+ def self.post(*args)
29
+ default.post(*args)
30
30
  end
31
31
 
32
32
  def self.default
@@ -39,15 +39,16 @@ class Agent
39
39
  end
40
40
  end
41
41
 
42
- def get(url, query = nil)
43
- request(url, method: :GET, query: query)
42
+ OPTS_DEFAULT = {}.freeze
43
+
44
+ def get(url, opts = OPTS_DEFAULT)
45
+ request(url, opts.merge(method: :GET))
44
46
  end
45
47
 
46
- def post(url, query = nil)
47
- request(url, method: :POST, query: query)
48
+ def post(url, opts = OPTS_DEFAULT)
49
+ request(url, opts.merge(method: :POST))
48
50
  end
49
51
 
50
- OPTS_DEFAULT = {}.freeze
51
52
 
52
53
  def request(url, opts = OPTS_DEFAULT)
53
54
  ctx = request_ctx(url, opts)
@@ -55,7 +56,7 @@ class Agent
55
56
  response = do_request(ctx)
56
57
  case response[:status_code]
57
58
  when 301, 302
58
- request(response[:headers]['Location'])
59
+ redirect(response[:headers]['Location'], ctx, opts)
59
60
  when 200, 204
60
61
  response.extend(ResponseMixin)
61
62
  else
@@ -63,11 +64,31 @@ class Agent
63
64
  end
64
65
  end
65
66
 
67
+ def redirect(url, ctx, opts)
68
+ url = case url
69
+ when /^http(?:s)?\:\/\//
70
+ url
71
+ when /^\/\/(.+)$/
72
+ ctx[:uri].scheme + url
73
+ when /^\//
74
+ "%s://%s%s" % [
75
+ ctx[:uri].scheme,
76
+ ctx[:uri].host,
77
+ url
78
+ ]
79
+ else
80
+ ctx[:uri] + url
81
+ end
82
+
83
+ request(url, opts)
84
+ end
85
+
66
86
  def request_ctx(url, opts)
67
87
  {
68
88
  method: opts[:method] || :GET,
69
89
  uri: url_to_uri(url, opts),
70
- opts: opts
90
+ opts: opts,
91
+ retry: 0,
71
92
  }
72
93
  end
73
94
 
@@ -87,13 +108,20 @@ class Agent
87
108
  def do_request(ctx)
88
109
  key = uri_key(ctx[:uri])
89
110
  @pools[key].acquire do |state|
90
- state[:socket] ||= connect(key)
91
- state[:protocol_method] ||= protocol_method(state[:socket], ctx)
92
- send(state[:protocol_method], state, ctx)
93
- rescue => e
94
- state[:socket]&.close rescue nil
95
- state.clear
96
- raise e
111
+ cancel_after(10) do
112
+ state[:socket] ||= connect(key)
113
+ state[:protocol_method] ||= protocol_method(state[:socket], ctx)
114
+ send(state[:protocol_method], state, ctx)
115
+ rescue => e
116
+ state[:socket]&.close rescue nil
117
+ state.clear
118
+ if ctx[:retry] < 3
119
+ ctx[:retry] += 1
120
+ do_request(ctx)
121
+ else
122
+ raise e
123
+ end
124
+ end
97
125
  end
98
126
  end
99
127
 
@@ -136,12 +164,13 @@ class Agent
136
164
  stream = state[:http2_client].new_stream # allocate new stream
137
165
 
138
166
  headers = {
139
- ':scheme' => ctx[:uri].scheme,
140
167
  ':method' => ctx[:method].to_s,
141
- ':path' => ctx[:uri].request_uri,
168
+ ':scheme' => ctx[:uri].scheme,
142
169
  ':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
170
+ ':path' => ctx[:uri].request_uri,
143
171
  }
144
172
  headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
173
+ puts "* proxy request headers: #{headers.inspect}"
145
174
 
146
175
  if ctx[:opts][:payload]
147
176
  stream.headers(headers, end_stream: false)
@@ -173,13 +202,17 @@ class Agent
173
202
  (stream.close rescue nil) unless done
174
203
  end
175
204
 
176
- HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n\r\n"
205
+ HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n%<headers>s\r\n"
177
206
 
178
207
  def format_http1_request(ctx)
208
+ headers = ctx[:opts][:headers] ? ctx[:opts][:headers].map { |k, v| "#{k}: #{v}\r\n"}.join : nil
209
+ puts "* proxy request headers: #{headers.inspect}"
210
+
179
211
  HTTP1_REQUEST % {
180
212
  method: ctx[:method],
181
213
  request: ctx[:uri].request_uri,
182
- host: ctx[:uri].host
214
+ host: ctx[:uri].host,
215
+ headers: headers
183
216
  }
184
217
  end
185
218
 
@@ -198,9 +231,7 @@ class Agent
198
231
  when 'http'
199
232
  Polyphony::Net.tcp_connect(key[:host], key[:port])
200
233
  when 'https'
201
- Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS).tap do |socket|
202
- socket.post_connection_check(key[:host])
203
- end
234
+ Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS)
204
235
  else
205
236
  raise "Invalid scheme #{key[:scheme].inspect}"
206
237
  end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :HTTP1Adapter
4
+
5
+ require 'http/parser'
6
+
7
+ Request = import('./request')
8
+ HTTP2 = import('./http2_adapter')
9
+
10
+ # HTTP1 protocol implementation
11
+ class HTTP1Adapter
12
+ # Initializes a protocol adapter instance
13
+ def initialize(conn, opts)
14
+ @conn = conn
15
+ @opts = opts
16
+ @parser = HTTP::Parser.new(self)
17
+ @parse_fiber = Fiber.new { parse_loop }
18
+ end
19
+
20
+ def protocol
21
+ 'http/1.1'
22
+ end
23
+
24
+ # Parses incoming data, potentially firing parser callbacks. This loop runs on
25
+ # a separate fiber and is resumed only when the handler (client) loop asks for
26
+ # headers, or the request body, or waits for the request to be completed. The
27
+ # control flow is as follows (arrows represent control transfer between
28
+ # fibers):
29
+ #
30
+ # handler parse_loop
31
+ # get_headers --> ...
32
+ # @parser << @conn.readpartial(8192)
33
+ # ... <-- on_headers
34
+ #
35
+ # get_body --> ...
36
+ # ... <-- on_body
37
+ #
38
+ # consume_request --> ...
39
+ # @parser << @conn.readpartial(8192)
40
+ # ... <-- on_message_complete
41
+ #
42
+ def parse_loop
43
+ while (data = @conn.readpartial(8192))
44
+ break unless data
45
+ @parser << data
46
+ snooze
47
+ end
48
+ @calling_fiber.transfer nil
49
+ rescue SystemCallError, IOError => error
50
+ # ignore IO/system call errors
51
+ @calling_fiber.transfer nil
52
+ rescue Exception => error
53
+ # an error return value will be raised by the receiving fiber
54
+ @calling_fiber.transfer error
55
+ end
56
+
57
+ # request API
58
+
59
+ # Iterates over incoming requests. Requests are yielded once all headers have
60
+ # been received. It is left to the application to read the request body or
61
+ # diesregard it.
62
+ def each(&block)
63
+ can_upgrade = true
64
+ while @parse_fiber.alive? && (headers = get_headers)
65
+ if can_upgrade
66
+ # The connection can be upgraded only on the first request
67
+ return if upgrade_connection(headers, &block)
68
+ can_upgrade = false
69
+ end
70
+
71
+ @headers_sent = nil
72
+ block.(Request.new(headers, self))
73
+
74
+ if @parser.keep_alive?
75
+ @parsing = false
76
+ else
77
+ break
78
+ end
79
+ end
80
+ ensure
81
+ @conn.close rescue nil
82
+ end
83
+
84
+ # Reads headers for the next request. Transfers control to the parse loop,
85
+ # and resumes once the parse_loop has fired the on_headers callback
86
+ def get_headers
87
+ @parsing = true
88
+ @calling_fiber = Fiber.current
89
+ @parse_fiber.safe_transfer
90
+ end
91
+
92
+ # Reads a body chunk for the current request. Transfers control to the parse
93
+ # loop, and resumes once the parse_loop has fired the on_body callback
94
+ def get_body_chunk
95
+ @calling_fiber = Fiber.current
96
+ @read_body = true
97
+ @parse_fiber.safe_transfer
98
+ end
99
+
100
+ # Waits for the current request to complete. Transfers control to the parse
101
+ # loop, and resumes once the parse_loop has fired the on_message_complete
102
+ # callback
103
+ def consume_request
104
+ return unless @parsing
105
+
106
+ @calling_fiber = Fiber.current
107
+ @read_body = false
108
+ @parse_fiber.safe_transfer while @parsing
109
+ end
110
+
111
+ # Upgrades the connection to a different protocol, if the 'Upgrade' header is
112
+ # given. By default the only supported upgrade protocol is HTTP2. Additional
113
+ # protocols, notably WebSocket, can be specified by passing a hash to the
114
+ # :upgrade option when starting a server:
115
+ #
116
+ # opts = {
117
+ # upgrade: {
118
+ # websocket: Polyphony::Websocket.handler(&method(:ws_handler))
119
+ # }
120
+ # }
121
+ # Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) { |req| ... }
122
+ #
123
+ # @param headers [Hash] request headers
124
+ # @return [boolean] truthy if the connection has been upgraded
125
+ def upgrade_connection(headers, &block)
126
+ upgrade_protocol = headers['Upgrade']
127
+ return nil unless upgrade_protocol
128
+
129
+ if @opts[:upgrade] && @opts[:upgrade][upgrade_protocol.to_sym]
130
+ @opts[:upgrade][upgrade_protocol.to_sym].(@conn, headers)
131
+ return true
132
+ end
133
+
134
+ return nil unless upgrade_protocol == 'h2c'
135
+
136
+ # upgrade to HTTP/2
137
+ HTTP2.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
138
+ true
139
+ end
140
+
141
+ # Returns headers for HTTP2 upgrade
142
+ # @param headers [Hash] request headers
143
+ # @return [Hash] headers for HTTP2 upgrade
144
+ def http2_upgraded_headers(headers)
145
+ headers.merge(
146
+ ':scheme' => 'http',
147
+ ':authority' => headers['Host'],
148
+ )
149
+ end
150
+
151
+ # HTTP parser callbacks, called in the context of @parse_fiber
152
+
153
+ # Resumes client fiber on receipt of all headers
154
+ # @param headers [Hash] request headers
155
+ # @return [void]
156
+ def on_headers_complete(headers)
157
+ headers[':path'] = @parser.request_url
158
+ headers[':method'] = @parser.http_method
159
+ @calling_fiber.transfer(headers)
160
+ end
161
+
162
+ # Resumes client fiber on receipt of body chunk
163
+ # @param chunk [String] body chunk
164
+ # @return [void]
165
+ def on_body(chunk)
166
+ @calling_fiber.transfer(chunk) if @read_body
167
+ end
168
+
169
+ # Resumes client fiber on request completion
170
+ # @return [void]
171
+ def on_message_complete
172
+ @parsing = false
173
+ @calling_fiber.transfer nil
174
+ end
175
+
176
+ # response API
177
+
178
+ # Sends response including headers and body. Waits for the request to complete
179
+ # if not yet completed. The body is sent using chunked transfer encoding.
180
+ # @param chunk [String] response body
181
+ # @param headers
182
+ def respond(chunk, headers)
183
+ consume_request if @parsing
184
+ data = format_headers(headers, empty_response: !chunk)
185
+ if chunk
186
+ data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n0\r\n\r\n"
187
+ end
188
+ @conn << data
189
+ @headers_sent = true
190
+ end
191
+
192
+ DEFAULT_HEADERS_OPTS = {
193
+ empty_response: false,
194
+ consume_request: true
195
+ }
196
+
197
+ # Sends response headers. Waits for the request to complete if not yet
198
+ # completed. If empty_response is true(thy), the response status code will
199
+ # default to 204, otherwise to 200.
200
+ # @param headers [Hash] response headers
201
+ # @param empty_response [boolean] whether a response body will be sent
202
+ # @return [void]
203
+ def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
204
+ return if @headers_sent
205
+
206
+ consume_request if @parsing && opts[:consume_request]
207
+ @conn << format_headers(headers, opts[:empty_response])
208
+ @headers_sent = true
209
+ end
210
+
211
+ # Sends a response body chunk. If no headers were sent, default headers are
212
+ # sent using #send_headers. if the done option is true(thy), an empty chunk
213
+ # will be sent to signal response completion to the client.
214
+ # @param chunk [String] response body chunk
215
+ # @param done [boolean] whether the response is completed
216
+ # @return [void]
217
+ def send_body_chunk(chunk, done: false)
218
+ send_headers({}) unless @headers_sent
219
+
220
+ data = +"#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
221
+ data << "0\r\n\r\n" if done
222
+ @conn << data
223
+ end
224
+
225
+ # Finishes the response to the current request. If no headers were sent,
226
+ # default headers are sent using #send_headers.
227
+ # @return [void]
228
+ def finish
229
+ send_headers({}, true) unless @headers_sent
230
+
231
+ @conn << "0\r\n\r\n" if @body_sent
232
+ end
233
+
234
+ private
235
+
236
+ # Formats response headers. If empty_response is true(thy), the response
237
+ # status code will default to 204, otherwise to 200.
238
+ # @param headers [Hash] response headers
239
+ # @param empty_response [boolean] whether a response body will be sent
240
+ # @return [String] formatted response headers
241
+ def format_headers(headers, empty_response)
242
+ status = headers[':status'] || (empty_response ? 204 : 200)
243
+ data = empty_response ?
244
+ +"HTTP/1.1 #{status}\r\n" :
245
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
246
+
247
+ headers.each do |k, v|
248
+ next if k =~ /^:/
249
+ v.is_a?(Array) ?
250
+ v.each { |o| data << "#{k}: #{o}\r\n" } : data << "#{k}: #{v}\r\n"
251
+ end
252
+ data << "\r\n"
253
+ end
254
+ end