Skip to content

Commit 43940ca

Browse files
committed
Stateful HTTP/1 connection handling.
1 parent 2f3180a commit 43940ca

File tree

14 files changed

+80
-28
lines changed

14 files changed

+80
-28
lines changed

async-http.gemspec

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Gem::Specification.new do |spec|
2828
spec.add_dependency "async-pool", "~> 0.7"
2929
spec.add_dependency "io-endpoint", "~> 0.11"
3030
spec.add_dependency "io-stream", "~> 0.4"
31-
spec.add_dependency "protocol-http", "~> 0.35"
32-
spec.add_dependency "protocol-http1", "~> 0.20"
31+
spec.add_dependency "protocol-http", "~> 0.37"
32+
spec.add_dependency "protocol-http1", "~> 0.25"
3333
spec.add_dependency "protocol-http2", "~> 0.18"
3434
spec.add_dependency "traces", ">= 0.10"
3535
end

fixtures/async/http/a_protocol.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'tempfile'
1515

1616
require 'protocol/http/body/file'
17+
require "protocol/http/body/buffered"
1718

1819
require 'sus/fixtures/async/http'
1920

@@ -100,7 +101,7 @@ module HTTP
100101
end
101102

102103
with 'buffered body' do
103-
let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])}
104+
let(:body) {::Protocol::HTTP::Body::Buffered.new(["Hello World"])}
104105
let(:response) {::Protocol::HTTP::Response[200, {}, body]}
105106

106107
let(:app) do
@@ -410,7 +411,7 @@ module HTTP
410411
end
411412

412413
with 'body with incorrect length' do
413-
let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)}
414+
let(:bad_body) {::Protocol::HTTP::Body::Buffered.new(["Borked"], 10)}
414415

415416
let(:app) do
416417
::Protocol::HTTP::Middleware.for do |request|

lib/async/http/body/finishable.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,25 @@
99
module Async
1010
module HTTP
1111
module Body
12+
# Keeps track of whether a body is being read, and if so, waits for it to be closed.
1213
class Finishable < ::Protocol::HTTP::Body::Wrapper
1314
def initialize(body)
1415
super(body)
1516

1617
@closed = Async::Variable.new
1718
@error = nil
19+
20+
@reading = false
21+
end
22+
23+
def reading?
24+
@reading
25+
end
26+
27+
def read
28+
@reading = true
29+
30+
super
1831
end
1932

2033
def close(error = nil)
@@ -27,7 +40,11 @@ def close(error = nil)
2740
end
2841

2942
def wait
30-
@closed.wait
43+
if @reading
44+
@closed.wait
45+
else
46+
self.discard
47+
end
3148
end
3249

3350
def inspect

lib/async/http/client.rb

+3-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'traces/provider'
1515

1616
require_relative 'protocol'
17+
require_relative 'body/finishable'
1718

1819
module Async
1920
module HTTP
@@ -140,7 +141,7 @@ def call(request)
140141
def inspect
141142
"#<#{self.class} authority=#{@authority.inspect}>"
142143
end
143-
144+
144145
Traces::Provider(self) do
145146
def call(request)
146147
attributes = {
@@ -186,13 +187,7 @@ def call(request)
186187
def make_response(request, connection)
187188
response = request.call(connection)
188189

189-
# The connection won't be released until the body is completely read/released.
190-
::Protocol::HTTP::Body::Completable.wrap(response) do
191-
# TODO: We should probably wait until the request is fully consumed and/or the connection is ready before releasing it back into the pool.
192-
193-
# Release the connection back into the pool:
194-
@pool.release(connection)
195-
end
190+
response.pool = @pool
196191

197192
return response
198193
end

lib/async/http/protocol/http1/client.rb

+18-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@ module HTTP
1010
module Protocol
1111
module HTTP1
1212
class Client < Connection
13+
def initialize(...)
14+
super
15+
16+
@pool = nil
17+
end
18+
19+
attr_accessor :pool
20+
21+
def closed!
22+
super
23+
24+
if pool = @pool
25+
@pool = nil
26+
pool.release(self)
27+
end
28+
end
29+
1330
# Used by the client to send requests to the remote server.
1431
def call(request, task: Task.current)
15-
# We need to keep track of connections which are not in the initial "ready" state.
16-
@ready = false
17-
1832
Console.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
1933

2034
# Mark the start of the trailers:
@@ -54,12 +68,11 @@ def call(request, task: Task.current)
5468
end
5569

5670
response = Response.read(self, request)
57-
@ready = true
5871

5972
return response
6073
rescue
6174
# This will ensure that #reusable? returns false.
62-
@stream.close
75+
self.close
6376

6477
raise
6578
end

lib/async/http/protocol/http1/connection.rb

+3-4
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ class Connection < ::Protocol::HTTP1::Connection
1616
def initialize(stream, version)
1717
super(stream)
1818

19-
@ready = true
2019
@version = version
2120
end
2221

2322
def to_s
24-
"\#<#{self.class} negotiated #{@version}, currently #{@ready ? 'ready' : 'in-use'}>"
23+
"\#<#{self.class} negotiated #{@version}, #{@state}>"
2524
end
2625

2726
def as_json(...)
@@ -62,11 +61,11 @@ def concurrency
6261

6362
# Can we use this connection to make requests?
6463
def viable?
65-
@ready && @stream&.readable?
64+
self.idle? && @stream&.readable?
6665
end
6766

6867
def reusable?
69-
@ready && @persistent && @stream && !@stream.closed?
68+
@persistent && @stream && !@stream.closed?
7069
end
7170
end
7271
end

lib/async/http/protocol/http1/response.rb

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ def initialize(connection, version, status, reason, headers, body)
3939
super(version, status, headers, body, protocol)
4040
end
4141

42+
def pool=(pool)
43+
if @connection.idle? or @connection.closed?
44+
pool.release(@connection)
45+
else
46+
@connection.pool = pool
47+
end
48+
end
49+
4250
def connection
4351
@connection
4452
end

lib/async/http/protocol/http1/server.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def fail_request(status)
2222
write_body(@version, nil)
2323
rescue => error
2424
# At this point, there is very little we can do to recover:
25-
Console::Event::Failure.for(error).emit(self, "Failed to write failure response.", severity: :debug)
25+
Console::Event::Failure.for(error).emit(self, "Failed to write failure response!", severity: :debug)
2626
end
2727

2828
def next_request
@@ -37,7 +37,7 @@ def next_request
3737
end
3838

3939
return request
40-
rescue ::Protocol::HTTP1::BadRequest
40+
rescue ::Protocol::HTTP1::BadRequest => error
4141
fail_request(400)
4242
# Conceivably we could retry here, but we don't really know how bad the error is, so it's better to just fail:
4343
raise

lib/async/http/protocol/http2/response.rb

+10
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ def initialize(stream)
137137
attr :stream
138138
attr :request
139139

140+
def pool=(pool)
141+
# If we are already closed, the stream can be released now:
142+
if @stream.closed?
143+
pool.release(@stream.connection)
144+
else
145+
# Otherwise, we will release the stream when it is closed:
146+
@stream.pool = pool
147+
end
148+
end
149+
140150
def connection
141151
@stream.connection
142152
end

lib/async/http/protocol/http2/stream.rb

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def initialize(*)
2020

2121
@headers = nil
2222

23+
@pool = nil
24+
2325
# Input buffer, reading request body, or response body (receive_data):
2426
@length = nil
2527
@input = nil
@@ -30,6 +32,8 @@ def initialize(*)
3032

3133
attr_accessor :headers
3234

35+
attr_accessor :pool
36+
3337
attr :input
3438

3539
def add_header(key, value)
@@ -158,6 +162,10 @@ def closed(error)
158162
@output = nil
159163
end
160164

165+
if pool = @pool and @connection
166+
pool.release(@connection)
167+
end
168+
161169
return self
162170
end
163171
end

lib/async/http/proxy.rb

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def connect(&block)
9696
end
9797
else
9898
# This ensures we don't leave a response dangling:
99+
input.close
99100
response.close
100101

101102
raise ConnectFailure, response

test/async/http/middleware/location_redirector.rb

-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
with '301' do
1919
let(:app) do
2020
Protocol::HTTP::Middleware.for do |request|
21-
request.finish # TODO: request.discard - or some default handling?
22-
2321
case request.path
2422
when '/home'
2523
Protocol::HTTP::Response[301, {'location' => '/'}, []]

test/async/http/protocol/http11.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
response = client.get("/")
2222
connection = response.connection
2323

24-
expect(connection.as_json).to be == "#<Async::HTTP::Protocol::HTTP1::Client negotiated HTTP/1.1, currently ready>"
24+
expect(connection.as_json).to be =~ /Async::HTTP::Protocol::HTTP1::Client negotiated HTTP/
2525
ensure
2626
response&.close
2727
end
@@ -109,7 +109,7 @@ def around
109109
end
110110

111111
with 'full hijack with empty response' do
112-
let(:body) {Async::HTTP::Body::Buffered.new([], 0)}
112+
let(:body) {::Protocol::HTTP::Body::Buffered.new([], 0)}
113113

114114
let(:app) do
115115
::Protocol::HTTP::Middleware.for do |request|

test/async/http/proxy.rb

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
stream.close_read
4343

4444
stream.write(chunk)
45+
stream.close_write
46+
ensure
4547
stream.close
4648
end
4749
end

0 commit comments

Comments
 (0)