tipi 0.38 → 0.42

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