tipi 0.38 → 0.39

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.
@@ -31,6 +31,7 @@ module DigitalFabric
31
31
  def run
32
32
  @fiber = Fiber.current
33
33
  @service.mount(route, self)
34
+ @mounted = true
34
35
  keep_alive_timer = spin_loop(interval: 5) { keep_alive }
35
36
  process_incoming_messages(false)
36
37
  rescue GracefulShutdown
@@ -38,7 +39,7 @@ module DigitalFabric
38
39
  process_incoming_messages(true)
39
40
  ensure
40
41
  keep_alive_timer&.stop
41
- @service.unmount(self)
42
+ unmount
42
43
  end
43
44
 
44
45
  def process_incoming_messages(shutdown = false)
@@ -48,7 +49,15 @@ module DigitalFabric
48
49
  recv_df_message(msg)
49
50
  return if shutdown && @requests.empty?
50
51
  end
51
- rescue TimeoutError, IOError
52
+ rescue TimeoutError, IOError, SystemCallError
53
+ # ignore and just return in order to terminate the proxy
54
+ end
55
+
56
+ def unmount
57
+ return unless @mounted
58
+
59
+ @service.unmount(self)
60
+ @mounted = nil
52
61
  end
53
62
 
54
63
  def shutdown
@@ -82,9 +91,16 @@ module DigitalFabric
82
91
 
83
92
  def recv_df_message(message)
84
93
  @last_recv = Time.now
85
- return if message['kind'] == Protocol::PING
94
+ # puts "<<< #{message.inspect}"
86
95
 
87
- handler = @requests[message['id']]
96
+ case message[Protocol::Attribute::KIND]
97
+ when Protocol::PING
98
+ return
99
+ when Protocol::UNMOUNT
100
+ return unmount
101
+ end
102
+
103
+ handler = @requests[message[Protocol::Attribute::ID]]
88
104
  if !handler
89
105
  # puts "Unknown request id in #{message}"
90
106
  return
@@ -94,6 +110,8 @@ module DigitalFabric
94
110
  end
95
111
 
96
112
  def send_df_message(message)
113
+ # puts ">>> #{message.inspect}" unless message[Protocol::Attribute::KIND] == Protocol::PING
114
+
97
115
  @last_send = Time.now
98
116
  @conn << message.to_msgpack
99
117
  end
@@ -130,46 +148,51 @@ module DigitalFabric
130
148
  t1 = Time.now
131
149
  @service.record_latency_measurement(t1 - t0)
132
150
  end
133
- return if http_request_message(id, req, message)
151
+ kind = message[Protocol::Attribute::KIND]
152
+ attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
153
+ return if http_request_message(id, req, kind, attributes)
134
154
  end
135
155
  end
136
156
  rescue => e
137
- req.respond("Error: #{e.inspect}", ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
157
+ p "Internal server error: #{e.inspect}"
158
+ puts e.backtrace.join("\n")
159
+ http_request_send_error_response(e)
160
+ end
161
+
162
+ def http_request_send_error_response(error)
163
+ response = format("Error: %s\n%s", error.inspect, error.backtrace.join("\n"))
164
+ req.respond(response, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
165
+ rescue IOError, SystemCallError
166
+ # ignore
138
167
  end
139
168
 
140
169
  # @return [Boolean] true if response is complete
141
- def http_request_message(id, req, message)
142
- case message['kind']
170
+ def http_request_message(id, req, kind, message)
171
+ case kind
143
172
  when Protocol::HTTP_UPGRADE
144
- http_custom_upgrade(id, req, message)
173
+ http_custom_upgrade(id, req, *message)
145
174
  true
146
175
  when Protocol::HTTP_GET_REQUEST_BODY
147
- http_get_request_body(id, req, message)
176
+ http_get_request_body(id, req, *message)
148
177
  false
149
178
  when Protocol::HTTP_RESPONSE
150
- headers = message['headers']
151
- body = message['body']
152
- done = message['complete']
153
- if !req.headers_sent? && done
154
- req.respond(body, headers|| {})
155
- true
156
- else
157
- req.send_headers(headers) if headers && !req.headers_sent?
158
- req.send_chunk(body, done: done) if body or done
159
- done
160
- end
179
+ http_response(id, req, *message)
161
180
  else
162
181
  # invalid message
163
182
  true
164
183
  end
165
184
  end
166
185
 
186
+ def send_transfer_count(key, rx, tx)
187
+ send_df_message(Protocol.transfer_count(key, rx, tx))
188
+ end
189
+
167
190
  HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
168
191
 
169
- def http_custom_upgrade(id, req, message)
192
+ def http_custom_upgrade(id, req, headers)
170
193
  # send upgrade response
171
- upgrade_headers = message['headers'] ?
172
- message['headers'].merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
194
+ upgrade_headers = headers ?
195
+ headers.merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
173
196
  HTTP_RESPONSE_UPGRADE_HEADERS
174
197
  req.send_headers(upgrade_headers, true)
175
198
 
@@ -187,9 +210,9 @@ module DigitalFabric
187
210
  end
188
211
 
189
212
  def http_custom_upgrade_message(conn, message)
190
- case message['kind']
213
+ case message[Protocol::Attribute::KIND]
191
214
  when Protocol::CONN_DATA
192
- conn << message['data']
215
+ conn << message[:Protocol::Attribute::ConnData::DATA]
193
216
  false
194
217
  when Protocol::CONN_CLOSE
195
218
  true
@@ -199,8 +222,30 @@ module DigitalFabric
199
222
  end
200
223
  end
201
224
 
202
- def http_get_request_body(id, req, message)
203
- case (limit = message['limit'])
225
+ def http_response(id, req, body, headers, complete, transfer_count_key)
226
+ if !req.headers_sent? && complete
227
+ req.respond(body, headers|| {})
228
+ if transfer_count_key
229
+ rx, tx = req.transfer_counts
230
+ send_transfer_count(transfer_count_key, rx, tx)
231
+ end
232
+ true
233
+ else
234
+ req.send_headers(headers) if headers && !req.headers_sent?
235
+ req.send_chunk(body, done: complete) if body or complete
236
+
237
+ if complete && transfer_count_key
238
+ rx, tx = req.transfer_counts
239
+ send_transfer_count(transfer_count_key, rx, tx)
240
+ end
241
+ complete
242
+ end
243
+ rescue IOError, SystemCallError
244
+ # ignore error
245
+ end
246
+
247
+ def http_get_request_body(id, req, limit)
248
+ case limit
204
249
  when nil
205
250
  body = req.read
206
251
  else
@@ -230,9 +275,9 @@ module DigitalFabric
230
275
  with_request do |id|
231
276
  send_df_message(Protocol.ws_request(id, req.headers))
232
277
  response = receive
233
- case response['kind']
278
+ case response[0]
234
279
  when Protocol::WS_RESPONSE
235
- headers = response['headers'] || {}
280
+ headers = response[2] || {}
236
281
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
237
282
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
238
283
  req.respond(nil, headers)
@@ -244,6 +289,8 @@ module DigitalFabric
244
289
  req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
245
290
  end
246
291
  end
292
+ rescue IOError, SystemCallError
293
+ # ignore
247
294
  end
248
295
 
249
296
  def run_websocket_connection(id, websocket)
@@ -253,9 +300,9 @@ module DigitalFabric
253
300
  end
254
301
  end
255
302
  while (message = receive)
256
- case message['kind']
303
+ case message[Protocol::Attribute::KIND]
257
304
  when Protocol::WS_DATA
258
- websocket << message['data']
305
+ websocket << message[Protocol::Attribute::WS::DATA]
259
306
  when Protocol::WS_CLOSE
260
307
  return
261
308
  else
@@ -4,6 +4,7 @@ module DigitalFabric
4
4
  module Protocol
5
5
  PING = 'ping'
6
6
  SHUTDOWN = 'shutdown'
7
+ UNMOUNT = 'unmount'
7
8
 
8
9
  HTTP_REQUEST = 'http_request'
9
10
  HTTP_RESPONSE = 'http_response'
@@ -19,16 +20,68 @@ module DigitalFabric
19
20
  WS_DATA = 'ws_data'
20
21
  WS_CLOSE = 'ws_close'
21
22
 
23
+ TRANSFER_COUNT = 'transfer_count'
24
+
22
25
  SEND_TIMEOUT = 15
23
26
  RECV_TIMEOUT = SEND_TIMEOUT + 5
24
27
 
28
+ module Attribute
29
+ KIND = 0
30
+ ID = 1
31
+
32
+ module HttpRequest
33
+ HEADERS = 2
34
+ BODY_CHUNK = 3
35
+ COMPLETE = 4
36
+ end
37
+
38
+ module HttpResponse
39
+ BODY = 2
40
+ HEADERS = 3
41
+ COMPLETE = 4
42
+ TRANSFER_COUNT_KEY = 5
43
+ end
44
+
45
+ module HttpUpgrade
46
+ HEADERS = 2
47
+ end
48
+
49
+ module HttpGetRequestBody
50
+ LIMIT = 2
51
+ end
52
+
53
+ module HttpRequestBody
54
+ BODY = 2
55
+ COMPLETE = 3
56
+ end
57
+
58
+ module ConnectionData
59
+ DATA = 2
60
+ end
61
+
62
+ module WS
63
+ HEADERS = 2
64
+ DATA = 2
65
+ end
66
+
67
+ module TransferCount
68
+ KEY = 1
69
+ RX = 2
70
+ TX = 3
71
+ end
72
+ end
73
+
25
74
  class << self
26
75
  def ping
27
- { kind: PING }
76
+ [ PING ]
28
77
  end
29
78
 
30
79
  def shutdown
31
- { kind: SHUTDOWN }
80
+ [ SHUTDOWN ]
81
+ end
82
+
83
+ def unmount
84
+ [ UNMOUNT ]
32
85
  end
33
86
 
34
87
  DF_UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
@@ -43,47 +96,51 @@ module DigitalFabric
43
96
  end
44
97
 
45
98
  def http_request(id, req)
46
- { kind: HTTP_REQUEST, id: id, headers: req.headers, body: req.next_chunk, complete: req.complete? }
99
+ [ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
47
100
  end
48
101
 
49
- def http_response(id, body, headers, complete)
50
- { kind: HTTP_RESPONSE, id: id, body: body, headers: headers, complete: complete }
102
+ def http_response(id, body, headers, complete, transfer_count_key = nil)
103
+ [ HTTP_RESPONSE, id, body, headers, complete, transfer_count_key ]
51
104
  end
52
105
 
53
106
  def http_upgrade(id, headers)
54
- { kind: HTTP_UPGRADE, id: id }
107
+ [ HTTP_UPGRADE, id, headers ]
55
108
  end
56
109
 
57
110
  def http_get_request_body(id, limit = nil)
58
- { kind: HTTP_GET_REQUEST_BODY, id: id, limit: limit }
111
+ [ HTTP_GET_REQUEST_BODY, id, limit ]
59
112
  end
60
113
 
61
114
  def http_request_body(id, body, complete)
62
- { kind: HTTP_REQUEST_BODY, id: id, body: body, complete: complete }
115
+ [ HTTP_REQUEST_BODY, id, body, complete ]
63
116
  end
64
117
 
65
118
  def connection_data(id, data)
66
- { kind: CONN_DATA, id: id, data: data }
119
+ [ CONN_DATA, id, data ]
67
120
  end
68
121
 
69
122
  def connection_close(id)
70
- { kind: CONN_CLOSE, id: id }
123
+ [ CONN_CLOSE, id ]
71
124
  end
72
125
 
73
126
  def ws_request(id, headers)
74
- { kind: WS_REQUEST, id: id, headers: headers }
127
+ [ WS_REQUEST, id, headers ]
75
128
  end
76
129
 
77
130
  def ws_response(id, headers)
78
- { kind: WS_RESPONSE, id: id, headers: headers }
131
+ [ WS_RESPONSE, id, headers ]
79
132
  end
80
133
 
81
134
  def ws_data(id, data)
82
- { id: id, kind: WS_DATA, data: data }
135
+ [ WS_DATA, id, data ]
83
136
  end
84
137
 
85
138
  def ws_close(id)
86
- { id: id, kind: WS_CLOSE }
139
+ [WS_CLOSE, id ]
140
+ end
141
+
142
+ def transfer_count(key, rx, tx)
143
+ [ TRANSFER_COUNT, key, rx, tx ]
87
144
  end
88
145
  end
89
146
  end
@@ -6,40 +6,40 @@ module DigitalFabric
6
6
  class RequestAdapter
7
7
  def initialize(agent, msg)
8
8
  @agent = agent
9
- @id = msg['id']
9
+ @id = msg[Protocol::Attribute::ID]
10
10
  end
11
11
 
12
12
  def protocol
13
13
  'df'
14
14
  end
15
15
 
16
- def get_body_chunk
16
+ def get_body_chunk(request)
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request
20
+ def consume_request(request)
21
21
  @agent.get_http_request_body(@id, nil)
22
22
  end
23
23
 
24
- def respond(body, headers)
24
+ def respond(request, body, headers)
25
25
  @agent.send_df_message(
26
26
  Protocol.http_response(@id, body, headers, true)
27
27
  )
28
28
  end
29
29
 
30
- def send_headers(headers, opts = {})
30
+ def send_headers(request, headers, opts = {})
31
31
  @agent.send_df_message(
32
32
  Protocol.http_response(@id, nil, headers, false)
33
33
  )
34
34
  end
35
35
 
36
- def send_chunk(body, done: )
36
+ def send_chunk(request, body, done: )
37
37
  @agent.send_df_message(
38
38
  Protocol.http_response(@id, body, nil, done)
39
39
  )
40
40
  end
41
41
 
42
- def finish
42
+ def finish(request)
43
43
  @agent.send_df_message(
44
44
  Protocol.http_response(@id, nil, nil, true)
45
45
  )
@@ -99,10 +99,12 @@ module DigitalFabric
99
99
  end
100
100
 
101
101
  agent.http_request(req)
102
- rescue IOError, SystemCallError
102
+ rescue IOError, SystemCallError, HTTP2::Error::StreamClosed
103
103
  @counters[:errors] += 1
104
104
  rescue => e
105
105
  @counters[:errors] += 1
106
+ puts '*' * 40
107
+ p req
106
108
  p e
107
109
  puts e.backtrace.join("\n")
108
110
  req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -172,7 +174,7 @@ module DigitalFabric
172
174
  def find_agent(req)
173
175
  compile_agent_routes if @routing_changed
174
176
 
175
- host = req.headers['host'] || INVALID_HOST
177
+ host = req.headers[':authority'] || req.headers['host'] || INVALID_HOST
176
178
  path = req.headers[':path']
177
179
 
178
180
  route = @route_keys.find do |route|
@@ -180,11 +182,11 @@ module DigitalFabric
180
182
  end
181
183
  return @routes[route] if route
182
184
 
183
- # search for a known route for an agent that recently unmounted
184
- route, wait_list = @waiting_lists.find do |route, _|
185
- (host == route[:host]) || (path =~ route[:path_regexp])
186
- end
187
- return wait_for_agent(wait_list) if route
185
+ # # search for a known route for an agent that recently unmounted
186
+ # route, wait_list = @waiting_lists.find do |route, _|
187
+ # (host == route[:host]) || (path =~ route[:path_regexp])
188
+ # end
189
+ # return wait_for_agent(wait_list) if route
188
190
 
189
191
  nil
190
192
  end
@@ -29,8 +29,10 @@ module Tipi
29
29
 
30
30
  # return [Boolean] true if client loop should stop
31
31
  def handle_incoming_data(data, &block)
32
+ rx = data.bytesize
32
33
  @parser << data
33
34
  while (request = @requests_head)
35
+ request.headers[':rx'] = rx
34
36
  if @first
35
37
  request.headers[':first'] = true
36
38
  @first = nil
@@ -53,10 +55,11 @@ module Tipi
53
55
 
54
56
  # Reads a body chunk for the current request. Transfers control to the parse
55
57
  # loop, and resumes once the parse_loop has fired the on_body callback
56
- def get_body_chunk
58
+ def get_body_chunk(request)
57
59
  @waiting_for_body_chunk = true
58
60
  @next_chunk = nil
59
61
  while !@requests_tail.complete? && (data = @conn.readpartial(8192))
62
+ request.rx_incr(data.bytesize)
60
63
  @parser << data
61
64
  return @next_chunk if @next_chunk
62
65
 
@@ -70,9 +73,10 @@ module Tipi
70
73
  # Waits for the current request to complete. Transfers control to the parse
71
74
  # loop, and resumes once the parse_loop has fired the on_message_complete
72
75
  # callback
73
- def consume_request
76
+ def consume_request(request)
74
77
  request = @requests_head
75
78
  @conn.recv_loop do |data|
79
+ request.rx_incr(data.bytesize)
76
80
  @parser << data
77
81
  return if request.complete?
78
82
  end
@@ -185,8 +189,8 @@ module Tipi
185
189
  )
186
190
  end
187
191
 
188
- def websocket_connection(req)
189
- Tipi::Websocket.new(@conn, req.headers)
192
+ def websocket_connection(request)
193
+ Tipi::Websocket.new(@conn, request.headers)
190
194
  end
191
195
 
192
196
  def scheme_from_connection
@@ -200,52 +204,55 @@ module Tipi
200
204
 
201
205
  # Sends response including headers and body. Waits for the request to complete
202
206
  # if not yet completed. The body is sent using chunked transfer encoding.
207
+ # @param request [Qeweney::Request] HTTP request
203
208
  # @param body [String] response body
204
209
  # @param headers
205
- def respond(body, headers)
206
- consume_request if @parsing
207
- data = [format_headers(headers, body, false), body]
208
-
209
- # if body
210
- # if @parser.http_minor == 0
211
- # data << body
212
- # else
213
- # # data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
214
- # data << "#{body.bytesize.to_s(16)}\r\n#{body}\r\n0\r\n\r\n"
215
- # end
216
- # end
217
- # Polyphony.backend_sendv(@conn, data, 0)
218
- @conn.write(*data)
210
+ def respond(request, body, headers)
211
+ consume_request(request) if @parsing
212
+ formatted_headers = format_headers(headers, body, false)
213
+ request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
214
+ if body
215
+ @conn.write(formatted_headers, body)
216
+ else
217
+ @conn.write(formatted_headers)
218
+ end
219
219
  end
220
220
 
221
221
  # Sends response headers. If empty_response is truthy, the response status
222
222
  # code will default to 204, otherwise to 200.
223
+ # @param request [Qeweney::Request] HTTP request
223
224
  # @param headers [Hash] response headers
224
225
  # @param empty_response [boolean] whether a response body will be sent
225
226
  # @param chunked [boolean] whether to use chunked transfer encoding
226
227
  # @return [void]
227
- def send_headers(headers, empty_response: false, chunked: true)
228
- data = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
229
- @conn.write(data)
228
+ def send_headers(request, headers, empty_response: false, chunked: true)
229
+ formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
230
+ request.tx_incr(formatted_headers.bytesize)
231
+ @conn.write(formatted_headers)
230
232
  end
231
233
 
232
234
  # Sends a response body chunk. If no headers were sent, default headers are
233
235
  # sent using #send_headers. if the done option is true(thy), an empty chunk
234
236
  # will be sent to signal response completion to the client.
237
+ # @param request [Qeweney::Request] HTTP request
235
238
  # @param chunk [String] response body chunk
236
239
  # @param done [boolean] whether the response is completed
237
240
  # @return [void]
238
- def send_chunk(chunk, done: false)
239
- data = []
241
+ def send_chunk(request, chunk, done: false)
242
+ data = +''
240
243
  data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
241
244
  data << "0\r\n\r\n" if done
242
- @conn.write(data.join) unless data.empty?
245
+ return if data.empty?
246
+
247
+ request.tx_incr(data.bytesize)
248
+ @conn.write(data)
243
249
  end
244
250
 
245
251
  # Finishes the response to the current request. If no headers were sent,
246
252
  # default headers are sent using #send_headers.
247
253
  # @return [void]
248
- def finish
254
+ def finish(request)
255
+ request.tx_incr(5)
249
256
  @conn << "0\r\n\r\n"
250
257
  end
251
258
 
@@ -255,6 +262,8 @@ module Tipi
255
262
 
256
263
  private
257
264
 
265
+ INTERNAL_HEADER_REGEXP = /^:/.freeze
266
+
258
267
  # Formats response headers into an array. If empty_response is true(thy),
259
268
  # the response status code will default to 204, otherwise to 200.
260
269
  # @param headers [Hash] response headers
@@ -266,7 +275,7 @@ module Tipi
266
275
  status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
267
276
  lines = format_status_line(body, status, chunked)
268
277
  headers.each do |k, v|
269
- next if k =~ /^:/
278
+ next if k =~ INTERNAL_HEADER_REGEXP
270
279
 
271
280
  collect_header_lines(lines, k, v)
272
281
  end