tipi 0.37 → 0.40

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.
@@ -16,7 +16,9 @@ module Tipi
16
16
  @opts = opts
17
17
  @upgrade_headers = upgrade_headers
18
18
  @first = true
19
-
19
+ @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
20
+ @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
21
+
20
22
  @interface = ::HTTP2::Server.new
21
23
  @connection_fiber = Fiber.current
22
24
  @interface.on(:frame, &method(:send_frame))
@@ -24,6 +26,9 @@ module Tipi
24
26
  end
25
27
 
26
28
  def send_frame(data)
29
+ if @transfer_count_request
30
+ @transfer_count_request.tx_incr(data.bytesize)
31
+ end
27
32
  @conn << data
28
33
  rescue Exception => e
29
34
  @connection_fiber.transfer e
@@ -38,6 +43,7 @@ module Tipi
38
43
 
39
44
  def upgrade
40
45
  @conn << UPGRADE_MESSAGE
46
+ @tx += UPGRADE_MESSAGE.bytesize
41
47
  settings = @upgrade_headers['http2-settings']
42
48
  Fiber.current.schedule(nil)
43
49
  @interface.upgrade(settings, @upgrade_headers, '')
@@ -49,16 +55,31 @@ module Tipi
49
55
  def each(&block)
50
56
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
51
57
  upgrade if @upgrade_headers
52
-
53
- @conn.recv_loop(&@interface.method(:<<))
58
+
59
+ @conn.recv_loop do |data|
60
+ @rx += data.bytesize
61
+ @interface << data
62
+ end
54
63
  rescue SystemCallError, IOError
55
64
  # ignore
56
65
  ensure
57
66
  finalize_client_loop
58
67
  end
68
+
69
+ def get_rx_count
70
+ count = @rx
71
+ @rx = 0
72
+ count
73
+ end
74
+
75
+ def get_tx_count
76
+ count = @tx
77
+ @tx = 0
78
+ count
79
+ end
59
80
 
60
81
  def start_stream(stream, &block)
61
- stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
82
+ stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
62
83
  @first = nil if @first
63
84
  @streams[stream] = true
64
85
  end
@@ -66,11 +87,23 @@ module Tipi
66
87
  def finalize_client_loop
67
88
  @interface = nil
68
89
  @streams.each_key(&:stop)
90
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
69
91
  @conn.close
70
92
  end
71
93
 
72
94
  def close
95
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
73
96
  @conn.close
74
97
  end
98
+
99
+ def set_request_for_transfer_count(request)
100
+ @transfer_count_request = request
101
+ end
102
+
103
+ def unset_request_for_transfer_count(request)
104
+ return unless @transfer_count_request == request
105
+
106
+ @transfer_count_request = nil
107
+ end
75
108
  end
76
109
  end
@@ -9,13 +9,15 @@ module Tipi
9
9
  attr_accessor :__next__
10
10
  attr_reader :conn
11
11
 
12
- def initialize(stream, conn, first, &block)
12
+ def initialize(adapter, stream, conn, first, &block)
13
+ @adapter = adapter
13
14
  @stream = stream
14
15
  @conn = conn
15
16
  @first = first
16
17
  @connection_fiber = Fiber.current
17
18
  @stream_fiber = spin { |req| handle_request(req, &block) }
18
-
19
+ Thread.current.fiber_unschedule(@stream_fiber)
20
+
19
21
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
20
22
  # The request handler is run on a separate fiber for each stream, allowing
21
23
  # concurrent handling of incoming requests on the same HTTP/2 connection.
@@ -47,14 +49,17 @@ module Tipi
47
49
 
48
50
  def on_headers(headers)
49
51
  @request = Qeweney::Request.new(headers.to_h, self)
52
+ @request.rx_incr(@adapter.get_rx_count)
53
+ @request.tx_incr(@adapter.get_tx_count)
50
54
  if @first
51
55
  @request.headers[':first'] = true
52
56
  @first = false
53
57
  end
54
58
  @stream_fiber.schedule @request
55
59
  end
56
-
60
+
57
61
  def on_data(data)
62
+ data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
58
63
  if @waiting_for_body_chunk
59
64
  @waiting_for_body_chunk = nil
60
65
  @stream_fiber.schedule data
@@ -62,7 +67,7 @@ module Tipi
62
67
  @request.buffer_body_chunk(data)
63
68
  end
64
69
  end
65
-
70
+
66
71
  def on_half_close
67
72
  if @waiting_for_body_chunk
68
73
  @waiting_for_body_chunk = nil
@@ -78,62 +83,114 @@ module Tipi
78
83
  def protocol
79
84
  'h2'
80
85
  end
86
+
87
+ def with_transfer_count(request)
88
+ @adapter.set_request_for_transfer_count(request)
89
+ yield
90
+ ensure
91
+ @adapter.unset_request_for_transfer_count(request)
92
+ end
81
93
 
82
- def get_body_chunk
94
+ def get_body_chunk(request)
83
95
  # called in the context of the stream fiber
84
96
  return nil if @request.complete?
85
97
 
86
- @waiting_for_body_chunk = true
87
- # the chunk (or an exception) will be returned once the stream fiber is
88
- # resumed
89
- suspend
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
102
+ suspend
103
+ end
90
104
  ensure
91
105
  @waiting_for_body_chunk = nil
92
106
  end
93
107
 
94
108
  # Wait for request to finish
95
- def consume_request
109
+ def consume_request(request)
96
110
  return if @request.complete?
97
111
 
98
- @waiting_for_half_close = true
99
- suspend
112
+ with_transfer_count(request) do
113
+ @waiting_for_half_close = true
114
+ suspend
115
+ end
100
116
  ensure
101
117
  @waiting_for_half_close = nil
102
118
  end
103
119
 
104
120
  # response API
105
- def respond(chunk, headers)
121
+ def respond(request, chunk, headers)
106
122
  headers[':status'] ||= Qeweney::Status::OK
107
- @stream.headers(headers, end_stream: false)
108
- @stream.data(chunk, end_stream: true)
109
- @headers_sent = true
123
+ headers[':status'] = headers[':status'].to_s
124
+ with_transfer_count(request) do
125
+ @stream.headers(transform_headers(headers))
126
+ @headers_sent = true
127
+ @stream.data(chunk || '')
128
+ end
129
+ rescue HTTP2::Error::StreamClosed
130
+ # ignore
131
+ end
132
+
133
+ def respond_from_io(request, io, headers, chunk_size = 2**16)
134
+ headers[':status'] ||= Qeweney::Status::OK
135
+ headers[':status'] = headers[':status'].to_s
136
+ with_transfer_count(request) do
137
+ @stream.headers(transform_headers(headers))
138
+ @headers_sent = true
139
+ while (chunk = io.read(chunk_size))
140
+ @stream.data(chunk)
141
+ end
142
+ end
143
+ rescue HTTP2::Error::StreamClosed
144
+ # ignore
145
+ end
146
+
147
+ def transform_headers(headers)
148
+ headers.each_with_object([]) do |(k, v), a|
149
+ if v.is_a?(Array)
150
+ v.each { |vv| a << [k, vv.to_s] }
151
+ else
152
+ a << [k, v.to_s]
153
+ end
154
+ end
110
155
  end
111
156
 
112
- def send_headers(headers, empty_response = false)
157
+ def send_headers(request, headers, empty_response: false)
113
158
  return if @headers_sent
114
159
 
115
160
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
116
- @stream.headers(headers, end_stream: false)
161
+ with_transfer_count(request) do
162
+ @stream.headers(transform_headers(headers), end_stream: false)
163
+ end
117
164
  @headers_sent = true
165
+ rescue HTTP2::Error::StreamClosed
166
+ # ignore
118
167
  end
119
168
 
120
- def send_chunk(chunk, done: false)
169
+ def send_chunk(request, chunk, done: false)
121
170
  send_headers({}, false) unless @headers_sent
122
171
 
123
172
  if chunk
124
- @stream.data(chunk, end_stream: done)
173
+ with_transfer_count(request) do
174
+ @stream.data(chunk, end_stream: done)
175
+ end
125
176
  elsif done
126
177
  @stream.close
127
178
  end
179
+ rescue HTTP2::Error::StreamClosed
180
+ # ignore
128
181
  end
129
182
 
130
- def finish
183
+ def finish(request)
131
184
  if @headers_sent
132
185
  @stream.close
133
186
  else
134
187
  headers[':status'] ||= Qeweney::Status::NO_CONTENT
135
- @stream.headers(headers, end_stream: true)
188
+ with_transfer_count(request) do
189
+ @stream.headers(transform_headers(headers), end_stream: true)
190
+ end
136
191
  end
192
+ rescue HTTP2::Error::StreamClosed
193
+ # ignore
137
194
  end
138
195
 
139
196
  def stop
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney/request'
4
+
5
+ module Tipi
6
+ module ResponseExtensions
7
+ SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
8
+
9
+ def serve_io(io, opts)
10
+ if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
11
+ @adapter.respond_from_io(self, io, opts[:headers])
12
+ else
13
+ respond(io.read, opts[:headers] || {})
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.37'
4
+ VERSION = '0.40'
5
5
  end
@@ -60,8 +60,8 @@ class HTTP1ServerTest < MiniTest::Test
60
60
  connection << "GET / HTTP/1.0\r\n\r\n"
61
61
 
62
62
  response = connection.readpartial(8192)
63
- expected = <<~HTTP.chomp.http_lines
64
- HTTP/1.0 200
63
+ expected = <<~HTTP.chomp.http_lines.chomp
64
+ HTTP/1.1 200
65
65
  Content-Length: 13
66
66
 
67
67
  Hello, world!
@@ -78,14 +78,11 @@ class HTTP1ServerTest < MiniTest::Test
78
78
  connection << "GET / HTTP/1.1\r\n\r\n"
79
79
 
80
80
  response = connection.readpartial(8192)
81
- expected = <<~HTTP.http_lines
81
+ expected = <<~HTTP.http_lines.chomp
82
82
  HTTP/1.1 200
83
- Transfer-Encoding: chunked
83
+ Content-Length: 13
84
84
 
85
- d
86
85
  Hello, world!
87
- 0
88
-
89
86
  HTTP
90
87
  assert_equal(expected, response)
91
88
  end
@@ -98,26 +95,23 @@ class HTTP1ServerTest < MiniTest::Test
98
95
  connection << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
99
96
  response = connection.readpartial(8192)
100
97
  assert !connection.eof?
101
- assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
98
+ assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
102
99
 
103
100
  connection << "GET / HTTP/1.1\r\n\r\n"
104
101
  response = connection.readpartial(8192)
105
102
  assert !connection.eof?
106
- expected = <<~HTTP.http_lines
103
+ expected = <<~HTTP.http_lines.chomp
107
104
  HTTP/1.1 200
108
- Transfer-Encoding: chunked
105
+ Content-Length: 2
109
106
 
110
- 2
111
107
  Hi
112
- 0
113
-
114
108
  HTTP
115
109
  assert_equal(expected, response)
116
110
 
117
111
  connection << "GET / HTTP/1.0\r\n\r\n"
118
112
  response = connection.readpartial(8192)
119
113
  assert connection.eof?
120
- assert_equal("HTTP/1.0 200\r\nContent-Length: 2\r\n\r\nHi", response)
114
+ assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
121
115
  end
122
116
 
123
117
  def test_pipelining_client
@@ -130,24 +124,17 @@ class HTTP1ServerTest < MiniTest::Test
130
124
  end
131
125
 
132
126
  connection << "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
133
- 2.times { snooze }
127
+ sleep 0.01
134
128
  response = connection.readpartial(8192)
135
129
 
136
- expected = <<~HTTP.http_lines
130
+ expected = <<~HTTP.http_lines.chomp
137
131
  HTTP/1.1 200
138
- Transfer-Encoding: chunked
139
-
140
- d
141
- Hello, world!
142
- 0
132
+ Content-Length: 13
143
133
 
144
- HTTP/1.1 200
145
- Transfer-Encoding: chunked
134
+ Hello, world!HTTP/1.1 200
135
+ Content-Length: 14
146
136
 
147
- e
148
137
  Hello, foobar!
149
- 0
150
-
151
138
  HTTP
152
139
  assert_equal(expected, response)
153
140
  end
@@ -172,22 +159,22 @@ class HTTP1ServerTest < MiniTest::Test
172
159
  6
173
160
  foobar
174
161
  HTTP
175
- 20.times { snooze }
162
+ sleep 0.01
176
163
  assert request
177
164
  assert_equal %w[foobar], chunks
178
165
  assert !request.complete?
179
166
 
180
167
  connection << "6\r\nbazbud\r\n"
181
- 20.times { snooze }
168
+ sleep 0.01
182
169
  assert_equal %w[foobar bazbud], chunks
183
170
  assert !request.complete?
184
171
 
185
172
  connection << "0\r\n\r\n"
186
- 20.times { snooze }
173
+ sleep 0.01
187
174
  assert_equal %w[foobar bazbud], chunks
188
175
  assert request.complete?
189
176
 
190
- 2.times { snooze }
177
+ sleep 0.01
191
178
 
192
179
  response = connection.readpartial(8192)
193
180
 
@@ -210,7 +197,8 @@ class HTTP1ServerTest < MiniTest::Test
210
197
 
211
198
  opts = {
212
199
  upgrade: {
213
- echo: lambda do |conn, _headers|
200
+ echo: lambda do |adapter, _headers|
201
+ conn = adapter.conn
214
202
  conn << <<~HTTP.http_lines
215
203
  HTTP/1.1 101 Switching Protocols
216
204
  Upgrade: echo
@@ -231,14 +219,11 @@ class HTTP1ServerTest < MiniTest::Test
231
219
  connection << "GET / HTTP/1.1\r\n\r\n"
232
220
  response = connection.readpartial(8192)
233
221
  assert !connection.eof?
234
- expected = <<~HTTP.http_lines
222
+ expected = <<~HTTP.http_lines.chomp
235
223
  HTTP/1.1 200
236
- Transfer-Encoding: chunked
224
+ Content-Length: 2
237
225
 
238
- 2
239
226
  Hi
240
- 0
241
-
242
227
  HTTP
243
228
  assert_equal(expected, response)
244
229
 
@@ -271,7 +256,7 @@ class HTTP1ServerTest < MiniTest::Test
271
256
  connection.close
272
257
  assert !done
273
258
 
274
- 12.times { snooze }
259
+ sleep 0.01
275
260
  assert done
276
261
  end
277
262
 
data/test/test_request.rb CHANGED
@@ -60,7 +60,7 @@ class RequestHeadersTest < MiniTest::Test
60
60
 
61
61
  connection << "GET /titi HTTP/1.1\r\nHost: blah.com\r\nFoo: bar\r\nhi: 1\r\nHi: 2\r\nhi: 3\r\n\r\n"
62
62
 
63
- snooze
63
+ sleep 0.01
64
64
 
65
65
  assert_kind_of Qeweney::Request, req
66
66
  assert_equal 'blah.com', req.headers['host']
@@ -78,7 +78,7 @@ class RequestHeadersTest < MiniTest::Test
78
78
  end
79
79
 
80
80
  connection << "GET /titi HTTP/1.1\nHost: blah.com\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
81
- snooze
81
+ sleep 0.01
82
82
  assert_equal 'blah.com', req.host
83
83
  end
84
84
 
@@ -90,7 +90,7 @@ class RequestHeadersTest < MiniTest::Test
90
90
  end
91
91
 
92
92
  connection << "GET /titi HTTP/1.1\nConnection: keep-alive\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
93
- snooze
93
+ sleep 0.01
94
94
  assert_equal 'keep-alive', req.connection
95
95
  end
96
96
 
@@ -102,7 +102,7 @@ class RequestHeadersTest < MiniTest::Test
102
102
  end
103
103
 
104
104
  connection << "GET /titi HTTP/1.1\nConnection: upgrade\nUpgrade: foobar\n\n"
105
- snooze
105
+ sleep 0.01
106
106
  assert_equal 'foobar', req.upgrade_protocol
107
107
  end
108
108
  end