tipi 0.41 → 0.42

Sign up to get free protection for your applications and to get access to all the features.
@@ -172,8 +172,9 @@ module DigitalFabric
172
172
  http_request(req)
173
173
  rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
174
174
  # ignore
175
- rescue Polyphony::Terminate
175
+ rescue Polyphony::Terminate => e
176
176
  req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
177
+ raise e
177
178
  ensure
178
179
  @requests.delete(id)
179
180
  @long_running_requests.delete(id)
@@ -187,7 +188,6 @@ module DigitalFabric
187
188
  complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]
188
189
  req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))
189
190
  req.buffer_body_chunk(body_chunk) if body_chunk
190
- req.complete! if complete
191
191
  req
192
192
  end
193
193
 
@@ -36,7 +36,7 @@ module DigitalFabric
36
36
  process_incoming_messages(false)
37
37
  rescue GracefulShutdown
38
38
  puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
39
- process_incoming_messages(true)
39
+ move_on_after(15) { process_incoming_messages(true) }
40
40
  ensure
41
41
  # keep_alive_timer&.stop
42
42
  unmount
@@ -60,7 +60,7 @@ module DigitalFabric
60
60
  @mounted = nil
61
61
  end
62
62
 
63
- def shutdown
63
+ def send_shutdown
64
64
  send_df_message(Protocol.shutdown)
65
65
  @fiber.raise GracefulShutdown.new
66
66
  end
@@ -144,7 +144,8 @@ module DigitalFabric
144
144
  t0 = Time.now
145
145
  t1 = nil
146
146
  with_request do |id|
147
- send_df_message(Protocol.http_request(id, req))
147
+ msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
+ send_df_message(msg)
148
149
  while (message = receive)
149
150
  unless t1
150
151
  t1 = Time.now
@@ -16,7 +16,7 @@ module DigitalFabric
16
16
  @service.mount(route, self)
17
17
  @current_request_count = 0
18
18
  # @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
19
- # update_service_stats
19
+ update_service_stats
20
20
  end
21
21
 
22
22
  def current_request_count
@@ -33,9 +33,13 @@ module DigitalFabric
33
33
  req.respond(message.to_json, { 'Content-Type' => 'text.json' })
34
34
  when '/stream/stats'
35
35
  stream_stats(req)
36
+ when '/upload'
37
+ req.respond("body: #{req.read.inspect}")
36
38
  else
37
39
  req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
38
40
  end
41
+ rescue => e
42
+ puts "Error: #{e.inspect}"
39
43
  ensure
40
44
  @current_request_count -= 1
41
45
  end
@@ -43,7 +47,7 @@ module DigitalFabric
43
47
  def stream_stats(req)
44
48
  req.send_headers({ 'Content-Type' => 'text/event-stream' })
45
49
 
46
- @service.timer.every(10) do
50
+ every(10) do
47
51
  message = last_service_stats
48
52
  req.send_chunk(format_sse_event(message.to_json))
49
53
  end
@@ -102,8 +102,8 @@ module DigitalFabric
102
102
  DF_UPGRADE_RESPONSE
103
103
  end
104
104
 
105
- def http_request(id, req)
106
- [ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
105
+ def http_request(id, headers, buffered_chunk, complete)
106
+ [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
107
107
  end
108
108
 
109
109
  def http_response(id, body, headers, complete, transfer_count_key = nil)
@@ -17,10 +17,6 @@ module DigitalFabric
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request(request)
21
- @agent.get_http_request_body(@id, nil)
22
- end
23
-
24
20
  def respond(request, body, headers)
25
21
  @agent.send_df_message(
26
22
  Protocol.http_response(@id, body, headers, true)
@@ -25,7 +25,7 @@ module DigitalFabric
25
25
  @http_latency_max = 0
26
26
  @last_counters = @counters.merge(stamp: Time.now.to_f - 1)
27
27
  @fiber = Fiber.current
28
- @timer = Polyphony::Timer.new('service_timer', resolution: 5)
28
+ # @timer = Polyphony::Timer.new('service_timer', resolution: 5)
29
29
  end
30
30
 
31
31
  def calculate_stats
@@ -81,6 +81,8 @@ module DigitalFabric
81
81
  s = `ps -p #{pid} -o %cpu,rss`
82
82
  cpu, rss = s.lines[1].chomp.strip.split(' ')
83
83
  [cpu.to_f, rss.to_i]
84
+ rescue Polyphony::BaseException
85
+ raise
84
86
  rescue Exception
85
87
  [nil, nil]
86
88
  end
@@ -231,13 +233,6 @@ module DigitalFabric
231
233
  @route_keys = @routes.keys
232
234
  end
233
235
 
234
- def wait_for_agent(wait_list)
235
- wait_list << Fiber.current
236
- @timer.move_on_after(10) { suspend }
237
- ensure
238
- wait_list.delete(self)
239
- end
240
-
241
236
  def path_regexp(path)
242
237
  /^#{path}/
243
238
  end
@@ -245,8 +240,8 @@ module DigitalFabric
245
240
  def graceful_shutdown
246
241
  @shutdown = true
247
242
  @agents.keys.each do |agent|
248
- if agent.respond_to?(:shutdown)
249
- agent.shutdown
243
+ if agent.respond_to?(:send_shutdown)
244
+ agent.send_shutdown
250
245
  else
251
246
  @agents.delete(agent)
252
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,41 +14,56 @@ 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
- rx = data.bytesize
33
- @parser << data
34
- while (request = @requests_head)
35
- request.headers[':rx'] = rx
36
- if @first
37
- request.headers[':first'] = true
38
- @first = nil
39
- end
40
- return true if upgrade_connection(request.headers, &block)
41
-
42
- @requests_head = request.__next__
43
- block.call(request)
44
- 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'
45
63
  end
46
- nil
47
64
  end
48
65
 
49
66
  def finalize_client_loop
50
- # release references to various objects
51
- @requests_head = @requests_tail = nil
52
67
  @parser = nil
53
68
  @splicing_pipe = nil
54
69
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
@@ -57,82 +72,20 @@ module Tipi
57
72
 
58
73
  # Reads a body chunk for the current request. Transfers control to the parse
59
74
  # loop, and resumes once the parse_loop has fired the on_body callback
60
- def get_body_chunk(request)
61
- @waiting_for_body_chunk = true
62
- @next_chunk = nil
63
- while !@requests_tail.complete? && (data = @conn.readpartial(8192))
64
- request.rx_incr(data.bytesize)
65
- @parser << data
66
- return @next_chunk if @next_chunk
67
-
68
- snooze
69
- end
70
- nil
71
- ensure
72
- @waiting_for_body_chunk = nil
73
- end
74
-
75
- # Waits for the current request to complete. Transfers control to the parse
76
- # loop, and resumes once the parse_loop has fired the on_message_complete
77
- # callback
78
- def consume_request(request)
79
- request = @requests_head
80
- @conn.recv_loop do |data|
81
- request.rx_incr(data.bytesize)
82
- @parser << data
83
- return if request.complete?
84
- end
85
- end
86
-
87
- def protocol
88
- version = @parser.http_version
89
- "HTTP #{version.join('.')}"
90
- end
91
-
92
- def on_headers_complete(headers)
93
- headers = normalize_headers(headers)
94
- headers[':path'] = @parser.request_url
95
- headers[':method'] = @parser.http_method.downcase
96
- scheme = (proto = headers['x-forwarded-proto']) ?
97
- proto.downcase : scheme_from_connection
98
- headers[':scheme'] = scheme
99
- queue_request(Qeweney::Request.new(headers, self))
75
+ def get_body_chunk(request, buffered_only = false)
76
+ @parser.read_body_chunk(buffered_only)
100
77
  end
101
78
 
102
- def normalize_headers(headers)
103
- headers.each_with_object({}) do |(k, v), h|
104
- k = k.downcase
105
- hk = h[k]
106
- if hk
107
- hk = h[k] = [hk] unless hk.is_a?(Array)
108
- v.is_a?(Array) ? hk.concat(v) : hk << v
109
- else
110
- h[k] = v
111
- end
112
- end
79
+ def get_body(request)
80
+ @parser.read_body
113
81
  end
114
-
115
- def queue_request(request)
116
- if @requests_head
117
- @requests_tail.__next__ = request
118
- @requests_tail = request
119
- else
120
- @requests_head = @requests_tail = request
121
- end
122
- end
123
-
124
- def on_body(chunk)
125
- if @waiting_for_body_chunk
126
- @next_chunk = chunk
127
- @waiting_for_body_chunk = nil
128
- else
129
- @requests_tail.buffer_body_chunk(chunk)
130
- end
82
+
83
+ def complete?(request)
84
+ @parser.complete?
131
85
  end
132
86
 
133
- def on_message_complete
134
- @waiting_for_body_chunk = nil
135
- @requests_tail.complete!(@parser.keep_alive?)
87
+ def protocol
88
+ @protocol
136
89
  end
137
90
 
138
91
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
@@ -170,14 +123,15 @@ module Tipi
170
123
  end
171
124
 
172
125
  def upgrade_with_handler(handler, headers)
173
- @parser = @requests_head = @requests_tail = nil
126
+ @parser = nil
174
127
  handler.(self, headers)
175
128
  true
176
129
  end
177
130
 
178
131
  def upgrade_to_http2(headers, &block)
179
- @parser = @requests_head = @requests_tail = nil
180
- 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)
181
135
  true
182
136
  end
183
137
 
@@ -210,7 +164,6 @@ module Tipi
210
164
  # @param body [String] response body
211
165
  # @param headers
212
166
  def respond(request, body, headers)
213
- consume_request(request) if @parsing
214
167
  formatted_headers = format_headers(headers, body, false)
215
168
  request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
216
169
  if body
@@ -221,8 +174,6 @@ module Tipi
221
174
  end
222
175
 
223
176
  def respond_from_io(request, io, headers, chunk_size = 2**14)
224
- consume_request(request) if @parsing
225
-
226
177
  formatted_headers = format_headers(headers, true, true)
227
178
  request.tx_incr(formatted_headers.bytesize)
228
179
 
@@ -246,10 +197,14 @@ module Tipi
246
197
  # @param chunked [boolean] whether to use chunked transfer encoding
247
198
  # @return [void]
248
199
  def send_headers(request, headers, empty_response: false, chunked: true)
249
- formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
200
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
250
201
  request.tx_incr(formatted_headers.bytesize)
251
202
  @conn.write(formatted_headers)
252
203
  end
204
+
205
+ def http1_1?(request)
206
+ request.headers[':protocol'] == 'http/1.1'
207
+ end
253
208
 
254
209
  # Sends a response body chunk. If no headers were sent, default headers are
255
210
  # sent using #send_headers. if the done option is true(thy), an empty chunk
@@ -3,18 +3,30 @@
3
3
  require 'http/2'
4
4
  require_relative './http2_stream'
5
5
 
6
+ # patch to fix bug in HTTP2::Stream
7
+ class HTTP2::Stream
8
+ def end_stream?(frame)
9
+ case frame[:type]
10
+ when :data, :headers, :continuation
11
+ frame[:flags]&.include?(:end_stream)
12
+ else false
13
+ end
14
+ end
15
+ end
16
+
6
17
  module Tipi
7
18
  # HTTP2 server adapter
8
19
  class HTTP2Adapter
9
- def self.upgrade_each(socket, opts, headers, &block)
10
- adapter = new(socket, opts, headers)
20
+ def self.upgrade_each(socket, opts, headers, body, &block)
21
+ adapter = new(socket, opts, headers, body)
11
22
  adapter.each(&block)
12
23
  end
13
24
 
14
- def initialize(conn, opts, upgrade_headers = nil)
25
+ def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
15
26
  @conn = conn
16
27
  @opts = opts
17
28
  @upgrade_headers = upgrade_headers
29
+ @upgrade_body = upgrade_body
18
30
  @first = true
19
31
  @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
20
32
  @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
@@ -30,6 +42,8 @@ module Tipi
30
42
  @transfer_count_request.tx_incr(data.bytesize)
31
43
  end
32
44
  @conn << data
45
+ rescue Polyphony::BaseException
46
+ raise
33
47
  rescue Exception => e
34
48
  @connection_fiber.transfer e
35
49
  end
@@ -45,8 +59,7 @@ module Tipi
45
59
  @conn << UPGRADE_MESSAGE
46
60
  @tx += UPGRADE_MESSAGE.bytesize
47
61
  settings = @upgrade_headers['http2-settings']
48
- Fiber.current.schedule(nil)
49
- @interface.upgrade(settings, @upgrade_headers, '')
62
+ @interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
50
63
  ensure
51
64
  @upgrade_headers = nil
52
65
  end
@@ -60,7 +73,7 @@ module Tipi
60
73
  @rx += data.bytesize
61
74
  @interface << data
62
75
  end
63
- rescue SystemCallError, IOError
76
+ rescue SystemCallError, IOError, HTTP2::Error::Error
64
77
  # ignore
65
78
  ensure
66
79
  finalize_client_loop
@@ -15,8 +15,7 @@ module Tipi
15
15
  @conn = conn
16
16
  @first = first
17
17
  @connection_fiber = Fiber.current
18
- @stream_fiber = spin { |req| handle_request(req, &block) }
19
- Thread.current.fiber_unschedule(@stream_fiber)
18
+ @stream_fiber = spin { run(&block) }
20
19
 
21
20
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
22
21
  # The request handler is run on a separate fiber for each stream, allowing
@@ -34,16 +33,16 @@ module Tipi
34
33
  stream.on(:half_close, &method(:on_half_close))
35
34
  end
36
35
 
37
- def handle_request(request, &block)
36
+ def run(&block)
37
+ request = receive
38
38
  error = nil
39
39
  block.(request)
40
40
  @connection_fiber.schedule
41
- rescue Polyphony::MoveOn
42
- # ignore
41
+ rescue Polyphony::BaseException
42
+ raise
43
43
  rescue Exception => e
44
44
  error = e
45
45
  ensure
46
- @done = true
47
46
  @connection_fiber.schedule error
48
47
  end
49
48
 
@@ -55,29 +54,19 @@ module Tipi
55
54
  @request.headers[':first'] = true
56
55
  @first = false
57
56
  end
58
- @stream_fiber.schedule @request
57
+ @stream_fiber << @request
59
58
  end
60
59
 
61
60
  def on_data(data)
62
61
  data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
63
- if @waiting_for_body_chunk
64
- @waiting_for_body_chunk = nil
65
- @stream_fiber.schedule data
66
- else
67
- @request.buffer_body_chunk(data)
68
- end
62
+
63
+ (@buffered_chunks ||= []) << data
64
+ @get_body_chunk_fiber&.schedule
69
65
  end
70
66
 
71
67
  def on_half_close
72
- if @waiting_for_body_chunk
73
- @waiting_for_body_chunk = nil
74
- @stream_fiber.schedule
75
- elsif @waiting_for_half_close
76
- @waiting_for_half_close = nil
77
- @stream_fiber.schedule
78
- else
79
- @request.complete!
80
- end
68
+ @get_body_chunk_fiber&.schedule
69
+ @complete = true
81
70
  end
82
71
 
83
72
  def protocol
@@ -90,31 +79,38 @@ module Tipi
90
79
  ensure
91
80
  @adapter.unset_request_for_transfer_count(request)
92
81
  end
93
-
94
- def get_body_chunk(request)
95
- # called in the context of the stream fiber
96
- return nil if @request.complete?
82
+
83
+ def get_body_chunk(request, buffered_only = false)
84
+ @buffered_chunks ||= []
85
+ return @buffered_chunks.shift unless @buffered_chunks.empty?
86
+ return nil if @complete
97
87
 
98
- with_transfer_count(request) do
99
- @waiting_for_body_chunk = true
100
- # the chunk (or an exception) will be returned once the stream fiber is
101
- # resumed
88
+ begin
89
+ @get_body_chunk_fiber = Fiber.current
102
90
  suspend
91
+ ensure
92
+ @get_body_chunk_fiber = nil
103
93
  end
104
- ensure
105
- @waiting_for_body_chunk = nil
94
+ @buffered_chunks.shift
106
95
  end
107
-
108
- # Wait for request to finish
109
- def consume_request(request)
110
- return if @request.complete?
111
-
112
- with_transfer_count(request) do
113
- @waiting_for_half_close = true
114
- suspend
96
+
97
+ def get_body(request)
98
+ @buffered_chunks ||= []
99
+ return @buffered_chunks.join if @complete
100
+
101
+ while !@complete
102
+ begin
103
+ @get_body_chunk_fiber = Fiber.current
104
+ suspend
105
+ ensure
106
+ @get_body_chunk_fiber = nil
107
+ end
115
108
  end
116
- ensure
117
- @waiting_for_half_close = nil
109
+ @buffered_chunks.join
110
+ end
111
+
112
+ def complete?(request)
113
+ @complete
118
114
  end
119
115
 
120
116
  # response API
@@ -194,7 +190,7 @@ module Tipi
194
190
  end
195
191
 
196
192
  def stop
197
- return if @done
193
+ return if @complete
198
194
 
199
195
  @stream.close
200
196
  @stream_fiber.schedule(Polyphony::MoveOn.new)