tipi 0.41 → 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.
@@ -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)