polyphony 0.17 → 0.19

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