tipi 0.38 → 0.42

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +5 -1
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +58 -16
  7. data/Rakefile +7 -3
  8. data/TODO.md +77 -1
  9. data/benchmarks/bm_http1_parser.rb +61 -0
  10. data/bin/benchmark +37 -0
  11. data/bin/h1pd +6 -0
  12. data/bin/tipi +3 -21
  13. data/df/sample_agent.rb +1 -1
  14. data/df/server.rb +16 -47
  15. data/df/server_utils.rb +178 -0
  16. data/examples/full_service.rb +13 -0
  17. data/examples/http1_parser.rb +55 -0
  18. data/examples/http_server.rb +15 -3
  19. data/examples/http_server_forked.rb +5 -1
  20. data/examples/http_server_routes.rb +29 -0
  21. data/examples/http_server_static.rb +26 -0
  22. data/examples/http_server_throttled.rb +3 -2
  23. data/examples/https_server.rb +6 -4
  24. data/examples/https_wss_server.rb +2 -1
  25. data/examples/rack_server.rb +5 -0
  26. data/examples/rack_server_https.rb +1 -1
  27. data/examples/rack_server_https_forked.rb +4 -3
  28. data/examples/routing_server.rb +5 -4
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +2 -8
  31. data/examples/ws_page.html +2 -2
  32. data/ext/tipi/extconf.rb +13 -0
  33. data/ext/tipi/http1_parser.c +823 -0
  34. data/ext/tipi/http1_parser.h +18 -0
  35. data/ext/tipi/tipi_ext.c +5 -0
  36. data/lib/tipi.rb +89 -1
  37. data/lib/tipi/acme.rb +308 -0
  38. data/lib/tipi/cli.rb +30 -0
  39. data/lib/tipi/digital_fabric/agent.rb +22 -17
  40. data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
  41. data/lib/tipi/digital_fabric/executive.rb +6 -2
  42. data/lib/tipi/digital_fabric/protocol.rb +87 -15
  43. data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
  44. data/lib/tipi/digital_fabric/service.rb +77 -51
  45. data/lib/tipi/http1_adapter.rb +116 -117
  46. data/lib/tipi/http2_adapter.rb +56 -10
  47. data/lib/tipi/http2_stream.rb +106 -53
  48. data/lib/tipi/rack_adapter.rb +2 -53
  49. data/lib/tipi/response_extensions.rb +17 -0
  50. data/lib/tipi/version.rb +1 -1
  51. data/security/http1.rb +12 -0
  52. data/test/helper.rb +60 -11
  53. data/test/test_http1_parser.rb +586 -0
  54. data/test/test_http_server.rb +0 -27
  55. data/test/test_request.rb +1 -28
  56. data/tipi.gemspec +11 -5
  57. metadata +96 -22
  58. data/e +0 -0
@@ -6,40 +6,36 @@ 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
21
- @agent.get_http_request_body(@id, nil)
22
- end
23
-
24
- def respond(body, headers)
20
+ def respond(request, body, headers)
25
21
  @agent.send_df_message(
26
22
  Protocol.http_response(@id, body, headers, true)
27
23
  )
28
24
  end
29
25
 
30
- def send_headers(headers, opts = {})
26
+ def send_headers(request, headers, opts = {})
31
27
  @agent.send_df_message(
32
28
  Protocol.http_response(@id, nil, headers, false)
33
29
  )
34
30
  end
35
31
 
36
- def send_chunk(body, done: )
32
+ def send_chunk(request, body, done: )
37
33
  @agent.send_df_message(
38
34
  Protocol.http_response(@id, body, nil, done)
39
35
  )
40
36
  end
41
37
 
42
- def finish
38
+ def finish(request)
43
39
  @agent.send_df_message(
44
40
  Protocol.http_response(@id, nil, nil, true)
45
41
  )
@@ -13,26 +13,22 @@ module DigitalFabric
13
13
  @token = token
14
14
  @agents = {}
15
15
  @routes = {}
16
- @waiting_lists = {} # hash mapping routes to arrays of requests waiting for an agent to mount
17
16
  @counters = {
18
17
  connections: 0,
19
18
  http_requests: 0,
20
19
  errors: 0
21
20
  }
22
21
  @connection_count = 0
22
+ @current_request_count = 0
23
23
  @http_latency_accumulator = 0
24
24
  @http_latency_counter = 0
25
+ @http_latency_max = 0
25
26
  @last_counters = @counters.merge(stamp: Time.now.to_f - 1)
26
27
  @fiber = Fiber.current
27
- @timer = Polyphony::Timer.new(resolution: 1)
28
-
29
- stats_updater = spin { @timer.every(10) { update_stats } }
30
- @stats = {}
31
-
32
- @current_request_count = 0
28
+ # @timer = Polyphony::Timer.new('service_timer', resolution: 5)
33
29
  end
34
30
 
35
- def update_stats
31
+ def calculate_stats
36
32
  now = Time.now.to_f
37
33
  elapsed = now - @last_counters[:stamp]
38
34
  connections = @counters[:connections] - @last_counters[:connections]
@@ -40,23 +36,61 @@ module DigitalFabric
40
36
  errors = @counters[:errors] - @last_counters[:errors]
41
37
  @last_counters = @counters.merge(stamp: now)
42
38
 
43
- average_latency = @http_latency_counter > 0 ?
44
- @http_latency_accumulator / @http_latency_counter :
45
- 0
39
+ average_latency = @http_latency_counter == 0 ? 0 :
40
+ @http_latency_accumulator / @http_latency_counter
46
41
  @http_latency_accumulator = 0
47
42
  @http_latency_counter = 0
48
-
49
- @stats = {
50
- connection_rate: connections / elapsed,
51
- http_request_rate: http_requests / elapsed,
52
- error_rate: errors / elapsed,
53
- average_latency: average_latency,
54
- agent_count: @agents.size,
55
- connection_count: @connection_count,
56
- concurrent_requests: @current_request_count
43
+ max_latency = @http_latency_max
44
+ @http_latency_max = 0
45
+
46
+ cpu, rss = pid_cpu_and_rss(Process.pid)
47
+
48
+ backend_stats = Thread.backend.stats
49
+ op_rate = backend_stats[:op_count] / elapsed
50
+ switch_rate = backend_stats[:switch_count] / elapsed
51
+ poll_rate = backend_stats[:poll_count] / elapsed
52
+
53
+ {
54
+ service: {
55
+ agent_count: @agents.size,
56
+ connection_count: @connection_count,
57
+ connection_rate: connections / elapsed,
58
+ error_rate: errors / elapsed,
59
+ http_request_rate: http_requests / elapsed,
60
+ latency_avg: average_latency,
61
+ latency_max: max_latency,
62
+ pending_requests: @current_request_count,
63
+ },
64
+ backend: {
65
+ op_rate: op_rate,
66
+ pending_ops: backend_stats[:pending_ops],
67
+ poll_rate: poll_rate,
68
+ runqueue_size: backend_stats[:runqueue_size],
69
+ runqueue_high_watermark: backend_stats[:runqueue_max_length],
70
+ switch_rate: switch_rate,
71
+
72
+ },
73
+ process: {
74
+ cpu_usage: cpu,
75
+ rss: rss.to_f / 1024,
76
+ }
57
77
  }
58
78
  end
59
79
 
80
+ def pid_cpu_and_rss(pid)
81
+ s = `ps -p #{pid} -o %cpu,rss`
82
+ cpu, rss = s.lines[1].chomp.strip.split(' ')
83
+ [cpu.to_f, rss.to_i]
84
+ rescue Polyphony::BaseException
85
+ raise
86
+ rescue Exception
87
+ [nil, nil]
88
+ end
89
+
90
+ def get_stats
91
+ calculate_stats
92
+ end
93
+
60
94
  def incr_connection_count
61
95
  @connection_count += 1
62
96
  end
@@ -77,32 +111,36 @@ module DigitalFabric
77
111
  count
78
112
  end
79
113
 
80
- def record_latency_measurement(latency)
114
+ def record_latency_measurement(latency, req)
81
115
  @http_latency_accumulator += latency
82
116
  @http_latency_counter += 1
117
+ @http_latency_max = latency if latency > @http_latency_max
118
+ return if latency < 1.0
119
+
120
+ puts format('slow request (%.1f): %p', latency, req.headers)
83
121
  end
84
122
 
85
- def http_request(req)
123
+ def http_request(req, allow_df_upgrade = false)
86
124
  @current_request_count += 1
87
125
  @counters[:http_requests] += 1
88
126
  @counters[:connections] += 1 if req.headers[':first']
89
127
 
90
- return upgrade_request(req) if req.upgrade_protocol
128
+ return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
91
129
 
92
130
  inject_request_headers(req)
93
131
  agent = find_agent(req)
94
132
  unless agent
95
- return req.respond('pong') if req.query[:q] == 'ping'
96
-
97
133
  @counters[:errors] += 1
98
134
  return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
99
135
  end
100
136
 
101
137
  agent.http_request(req)
102
- rescue IOError, SystemCallError
138
+ rescue IOError, SystemCallError, HTTP2::Error::StreamClosed
103
139
  @counters[:errors] += 1
104
140
  rescue => e
105
141
  @counters[:errors] += 1
142
+ puts '*' * 40
143
+ p req
106
144
  p e
107
145
  puts e.backtrace.join("\n")
108
146
  req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -118,10 +156,14 @@ module DigitalFabric
118
156
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
119
157
  end
120
158
 
121
- def upgrade_request(req)
159
+ def upgrade_request(req, allow_df_upgrade)
122
160
  case (protocol = req.upgrade_protocol)
123
161
  when 'df'
124
- df_upgrade(req)
162
+ if allow_df_upgrade
163
+ df_upgrade(req)
164
+ else
165
+ req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
166
+ end
125
167
  else
126
168
  agent = find_agent(req)
127
169
  unless agent
@@ -134,12 +176,16 @@ module DigitalFabric
134
176
  end
135
177
 
136
178
  def df_upgrade(req)
179
+ # we don't want to count connected agents
180
+ @current_request_count -= 1
137
181
  if req.headers['df-token'] != @token
138
182
  return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)
139
183
  end
140
184
 
141
185
  req.adapter.conn << Protocol.df_upgrade_response
142
186
  AgentProxy.new(self, req)
187
+ ensure
188
+ @current_request_count += 1
143
189
  end
144
190
 
145
191
  def mount(route, agent)
@@ -149,11 +195,6 @@ module DigitalFabric
149
195
  @executive = agent if route[:executive]
150
196
  @agents[agent] = route
151
197
  @routing_changed = true
152
-
153
- if (waiting = @waiting_lists[route])
154
- waiting.each { |f| f.schedule(agent) }
155
- @waiting_lists.delete(route)
156
- end
157
198
  end
158
199
 
159
200
  def unmount(agent)
@@ -163,8 +204,6 @@ module DigitalFabric
163
204
  @executive = nil if route[:executive]
164
205
  @agents.delete(agent)
165
206
  @routing_changed = true
166
-
167
- @waiting_lists[route] ||= []
168
207
  end
169
208
 
170
209
  INVALID_HOST = 'INVALID_HOST'
@@ -172,7 +211,7 @@ module DigitalFabric
172
211
  def find_agent(req)
173
212
  compile_agent_routes if @routing_changed
174
213
 
175
- host = req.headers['host'] || INVALID_HOST
214
+ host = req.headers[':authority'] || req.headers['host'] || INVALID_HOST
176
215
  path = req.headers[':path']
177
216
 
178
217
  route = @route_keys.find do |route|
@@ -180,12 +219,6 @@ module DigitalFabric
180
219
  end
181
220
  return @routes[route] if route
182
221
 
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
188
-
189
222
  nil
190
223
  end
191
224
 
@@ -200,13 +233,6 @@ module DigitalFabric
200
233
  @route_keys = @routes.keys
201
234
  end
202
235
 
203
- def wait_for_agent(wait_list)
204
- wait_list << Fiber.current
205
- @timer.move_on_after(10) { suspend }
206
- ensure
207
- wait_list.delete(self)
208
- end
209
-
210
236
  def path_regexp(path)
211
237
  /^#{path}/
212
238
  end
@@ -214,8 +240,8 @@ module DigitalFabric
214
240
  def graceful_shutdown
215
241
  @shutdown = true
216
242
  @agents.keys.each do |agent|
217
- if agent.respond_to?(:shutdown)
218
- agent.shutdown
243
+ if agent.respond_to?(:send_shutdown)
244
+ agent.send_shutdown
219
245
  else
220
246
  @agents.delete(agent)
221
247
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http/parser'
3
+ require 'tipi_ext'
4
4
  require_relative './http2_adapter'
5
5
  require 'qeweney/request'
6
6
 
@@ -14,119 +14,78 @@ module Tipi
14
14
  @conn = conn
15
15
  @opts = opts
16
16
  @first = true
17
- @parser = ::HTTP::Parser.new(self)
17
+ @parser = Tipi::HTTP1Parser.new(@conn)
18
18
  end
19
19
 
20
20
  def each(&block)
21
- @conn.recv_loop do |data|
22
- return if handle_incoming_data(data, &block)
21
+ while true
22
+ headers = @parser.parse_headers
23
+ break unless headers
24
+
25
+ # handle_request returns true if connection is not persistent or was
26
+ # upgraded
27
+ break if handle_request(headers, &block)
23
28
  end
29
+ rescue Tipi::HTTP1Parser::Error
30
+ # ignore
24
31
  rescue SystemCallError, IOError
25
32
  # ignore
26
33
  ensure
27
34
  finalize_client_loop
28
35
  end
29
36
 
30
- # return [Boolean] true if client loop should stop
31
- def handle_incoming_data(data, &block)
32
- @parser << data
33
- while (request = @requests_head)
34
- if @first
35
- request.headers[':first'] = true
36
- @first = nil
37
- end
38
- return true if upgrade_connection(request.headers, &block)
39
-
40
- @requests_head = request.__next__
41
- block.call(request)
42
- return true unless request.keep_alive?
37
+ def handle_request(headers, &block)
38
+ scheme = (proto = headers['x-forwarded-proto']) ?
39
+ proto.downcase : scheme_from_connection
40
+ headers[':scheme'] = scheme
41
+ @protocol = headers[':protocol']
42
+ if @first
43
+ headers[':first'] = true
44
+ @first = nil
45
+ end
46
+
47
+ return true if upgrade_connection(headers, &block)
48
+
49
+ request = Qeweney::Request.new(headers, self)
50
+ if !@parser.complete?
51
+ request.buffer_body_chunk(@parser.read_body_chunk(true))
52
+ end
53
+ block.call(request)
54
+ return !persistent_connection?(headers)
55
+ end
56
+
57
+ def persistent_connection?(headers)
58
+ if headers[':protocol'] == 'http/1.1'
59
+ return headers['connection'] != 'close'
60
+ else
61
+ connection = headers['connection']
62
+ return connection && connection != 'close'
43
63
  end
44
- nil
45
64
  end
46
65
 
47
66
  def finalize_client_loop
48
- # release references to various objects
49
- @requests_head = @requests_tail = nil
50
67
  @parser = nil
68
+ @splicing_pipe = nil
69
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
51
70
  @conn.close
52
71
  end
53
72
 
54
73
  # Reads a body chunk for the current request. Transfers control to the parse
55
74
  # loop, and resumes once the parse_loop has fired the on_body callback
56
- def get_body_chunk
57
- @waiting_for_body_chunk = true
58
- @next_chunk = nil
59
- while !@requests_tail.complete? && (data = @conn.readpartial(8192))
60
- @parser << data
61
- return @next_chunk if @next_chunk
62
-
63
- snooze
64
- end
65
- nil
66
- ensure
67
- @waiting_for_body_chunk = nil
68
- end
69
-
70
- # Waits for the current request to complete. Transfers control to the parse
71
- # loop, and resumes once the parse_loop has fired the on_message_complete
72
- # callback
73
- def consume_request
74
- request = @requests_head
75
- @conn.recv_loop do |data|
76
- @parser << data
77
- return if request.complete?
78
- end
79
- end
80
-
81
- def protocol
82
- version = @parser.http_version
83
- "HTTP #{version.join('.')}"
84
- end
85
-
86
- def on_headers_complete(headers)
87
- headers = normalize_headers(headers)
88
- headers[':path'] = @parser.request_url
89
- headers[':method'] = @parser.http_method.downcase
90
- scheme = (proto = headers['x-forwarded-proto']) ?
91
- proto.downcase : scheme_from_connection
92
- headers[':scheme'] = scheme
93
- queue_request(Qeweney::Request.new(headers, self))
75
+ def get_body_chunk(request, buffered_only = false)
76
+ @parser.read_body_chunk(buffered_only)
94
77
  end
95
78
 
96
- def normalize_headers(headers)
97
- headers.each_with_object({}) do |(k, v), h|
98
- k = k.downcase
99
- hk = h[k]
100
- if hk
101
- hk = h[k] = [hk] unless hk.is_a?(Array)
102
- v.is_a?(Array) ? hk.concat(v) : hk << v
103
- else
104
- h[k] = v
105
- end
106
- end
79
+ def get_body(request)
80
+ @parser.read_body
107
81
  end
108
-
109
- def queue_request(request)
110
- if @requests_head
111
- @requests_tail.__next__ = request
112
- @requests_tail = request
113
- else
114
- @requests_head = @requests_tail = request
115
- end
116
- end
117
-
118
- def on_body(chunk)
119
- if @waiting_for_body_chunk
120
- @next_chunk = chunk
121
- @waiting_for_body_chunk = nil
122
- else
123
- @requests_tail.buffer_body_chunk(chunk)
124
- end
82
+
83
+ def complete?(request)
84
+ @parser.complete?
125
85
  end
126
86
 
127
- def on_message_complete
128
- @waiting_for_body_chunk = nil
129
- @requests_tail.complete!(@parser.keep_alive?)
87
+ def protocol
88
+ @protocol
130
89
  end
131
90
 
132
91
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
@@ -164,14 +123,15 @@ module Tipi
164
123
  end
165
124
 
166
125
  def upgrade_with_handler(handler, headers)
167
- @parser = @requests_head = @requests_tail = nil
126
+ @parser = nil
168
127
  handler.(self, headers)
169
128
  true
170
129
  end
171
130
 
172
131
  def upgrade_to_http2(headers, &block)
173
- @parser = @requests_head = @requests_tail = nil
174
- HTTP2Adapter.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
132
+ headers = http2_upgraded_headers(headers)
133
+ body = @parser.read_body
134
+ HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
175
135
  true
176
136
  end
177
137
 
@@ -185,8 +145,8 @@ module Tipi
185
145
  )
186
146
  end
187
147
 
188
- def websocket_connection(req)
189
- Tipi::Websocket.new(@conn, req.headers)
148
+ def websocket_connection(request)
149
+ Tipi::Websocket.new(@conn, request.headers)
190
150
  end
191
151
 
192
152
  def scheme_from_connection
@@ -200,61 +160,100 @@ module Tipi
200
160
 
201
161
  # Sends response including headers and body. Waits for the request to complete
202
162
  # if not yet completed. The body is sent using chunked transfer encoding.
163
+ # @param request [Qeweney::Request] HTTP request
203
164
  # @param body [String] response body
204
165
  # @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)
166
+ def respond(request, body, headers)
167
+ formatted_headers = format_headers(headers, body, false)
168
+ request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
169
+ if body
170
+ @conn.write(formatted_headers, body)
171
+ else
172
+ @conn.write(formatted_headers)
173
+ end
219
174
  end
175
+
176
+ def respond_from_io(request, io, headers, chunk_size = 2**14)
177
+ formatted_headers = format_headers(headers, true, true)
178
+ request.tx_incr(formatted_headers.bytesize)
220
179
 
180
+ # assume chunked encoding
181
+ Thread.current.backend.splice_chunks(
182
+ io,
183
+ @conn,
184
+ formatted_headers,
185
+ "0\r\n\r\n",
186
+ ->(len) { "#{len.to_s(16)}\r\n" },
187
+ "\r\n",
188
+ chunk_size
189
+ )
190
+ end
191
+
221
192
  # Sends response headers. If empty_response is truthy, the response status
222
193
  # code will default to 204, otherwise to 200.
194
+ # @param request [Qeweney::Request] HTTP request
223
195
  # @param headers [Hash] response headers
224
196
  # @param empty_response [boolean] whether a response body will be sent
225
197
  # @param chunked [boolean] whether to use chunked transfer encoding
226
198
  # @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)
199
+ def send_headers(request, headers, empty_response: false, chunked: true)
200
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
201
+ request.tx_incr(formatted_headers.bytesize)
202
+ @conn.write(formatted_headers)
203
+ end
204
+
205
+ def http1_1?(request)
206
+ request.headers[':protocol'] == 'http/1.1'
230
207
  end
231
208
 
232
209
  # Sends a response body chunk. If no headers were sent, default headers are
233
210
  # sent using #send_headers. if the done option is true(thy), an empty chunk
234
211
  # will be sent to signal response completion to the client.
212
+ # @param request [Qeweney::Request] HTTP request
235
213
  # @param chunk [String] response body chunk
236
214
  # @param done [boolean] whether the response is completed
237
215
  # @return [void]
238
- def send_chunk(chunk, done: false)
239
- data = []
216
+ def send_chunk(request, chunk, done: false)
217
+ data = +''
240
218
  data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
241
219
  data << "0\r\n\r\n" if done
242
- @conn.write(data.join) unless data.empty?
220
+ return if data.empty?
221
+
222
+ request.tx_incr(data.bytesize)
223
+ @conn.write(data)
243
224
  end
244
225
 
226
+ def send_chunk_from_io(request, io, r, w, chunk_size)
227
+ len = w.splice(io, chunk_size)
228
+ if len > 0
229
+ Thread.current.backend.chain(
230
+ [:write, @conn, "#{len.to_s(16)}\r\n"],
231
+ [:splice, r, @conn, len],
232
+ [:write, @conn, "\r\n"]
233
+ )
234
+ else
235
+ @conn.write("0\r\n\r\n")
236
+ end
237
+ len
238
+ end
239
+
245
240
  # Finishes the response to the current request. If no headers were sent,
246
241
  # default headers are sent using #send_headers.
247
242
  # @return [void]
248
- def finish
243
+ def finish(request)
244
+ request.tx_incr(5)
249
245
  @conn << "0\r\n\r\n"
250
246
  end
251
247
 
252
248
  def close
249
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
253
250
  @conn.close
254
251
  end
255
252
 
256
253
  private
257
254
 
255
+ INTERNAL_HEADER_REGEXP = /^:/.freeze
256
+
258
257
  # Formats response headers into an array. If empty_response is true(thy),
259
258
  # the response status code will default to 204, otherwise to 200.
260
259
  # @param headers [Hash] response headers
@@ -266,7 +265,7 @@ module Tipi
266
265
  status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
267
266
  lines = format_status_line(body, status, chunked)
268
267
  headers.each do |k, v|
269
- next if k =~ /^:/
268
+ next if k =~ INTERNAL_HEADER_REGEXP
270
269
 
271
270
  collect_header_lines(lines, k, v)
272
271
  end
@@ -294,7 +293,7 @@ module Tipi
294
293
  if chunked
295
294
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
296
295
  else
297
- +"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
296
+ +"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
298
297
  end
299
298
  end
300
299