Skip to content

Commit 282c7fb

Browse files
committed
Share persistent connection pool across chained sessions
Child sessions created by chaining now share the same pool, so connection reuse works regardless of whether options are baked-in up front or chained per-request. Closes #372.
1 parent b0b4799 commit 282c7fb

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4343
`HTTP::Client` per origin, so redirects to a different domain transparently
4444
open (and reuse) a separate persistent connection. Cookie management is
4545
preserved across all hops. ([#557])
46+
- Chaining configuration methods (`.headers`, `.auth`, `.cookies`, etc.) on a
47+
persistent session no longer breaks connection reuse. Child sessions created
48+
by chaining now share the parent's connection pool, so
49+
`HTTP.persistent(host).headers(...).get(path)` reuses the same underlying
50+
TCP connection across calls. ([#372])
4651

4752
### Changed
4853

@@ -221,6 +226,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
221226
[#223]: https://github.com/httprb/http/issues/223
222227
[#358]: https://github.com/httprb/http/issues/358
223228
[#371]: https://github.com/httprb/http/issues/371
229+
[#372]: https://github.com/httprb/http/issues/372
224230
[#447]: https://github.com/httprb/http/issues/447
225231
[#448]: https://github.com/httprb/http/issues/448
226232
[#449]: https://github.com/httprb/http/issues/449

lib/http/session.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ class Session
5656
# session = HTTP::Session.new(headers: {"Accept" => "application/json"})
5757
#
5858
# @param default_options [HTTP::Options, nil] existing options instance
59+
# @param clients [Hash, nil] shared connection pool (internal use)
5960
# @param options [Hash] keyword options (see HTTP::Options#initialize)
6061
# @return [HTTP::Session] a new session instance
6162
# @api public
62-
def initialize(default_options = nil, **)
63+
def initialize(default_options = nil, clients: nil, **)
6364
@default_options = HTTP::Options.new(default_options, **)
64-
@clients = {}
65+
@clients = clients || {}
6566
end
6667

6768
# Close all persistent connections held by this session
@@ -130,6 +131,23 @@ def request(verb, uri,
130131

131132
private
132133

134+
# Create a new session with the given options
135+
#
136+
# When the current session is persistent, the child session shares the
137+
# same connection pool so that chaining methods like {Chainable#headers}
138+
# or {Chainable#auth} do not break connection reuse.
139+
#
140+
# @param options [HTTP::Options] options for the new session
141+
# @return [HTTP::Session]
142+
# @api private
143+
def branch(options)
144+
if persistent?
145+
self.class.new(options, clients: @clients)
146+
else
147+
self.class.new(options)
148+
end
149+
end
150+
133151
# Execute a request with cookie management
134152
#
135153
# @param client [HTTP::Client, nil] the client (nil when persistent; looked up from pool)

sig/http.rbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ module HTTP
182182
@default_options: Options
183183
@clients: Hash[String, Client]
184184

185-
def initialize: (?Options? default_options, **untyped) -> void
185+
def initialize: (?Options? default_options, ?clients: Hash[String, Client]?, **untyped) -> void
186186
def close: () -> void
187187
def request: (
188188
verb verb,
@@ -238,6 +238,7 @@ module HTTP
238238

239239
private
240240

241+
def branch: (Options options) -> Session
241242
def perform_request: (Client? client, verb verb, untyped uri, Options merged) -> Response
242243
def perform_redirects: (CookieJar jar, Client client, Request req, Response res, Options opts) -> Response
243244
def redirect_client: (Client client, Request request) -> Client

test/http/session_test.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,49 @@
277277
end
278278
end
279279

280+
describe "persistent connection reuse with chaining" do
281+
it "reuses connections when chaining headers on a persistent session" do
282+
session = HTTP.persistent(dummy.endpoint)
283+
284+
sock1 = session.headers("Accept" => "application/json").get("#{dummy.endpoint}/socket/1").to_s
285+
sock2 = session.headers("Accept" => "text/html").get("#{dummy.endpoint}/socket/2").to_s
286+
287+
refute_equal "", sock1
288+
assert_equal sock1, sock2
289+
ensure
290+
session&.close
291+
end
292+
293+
it "reuses connections when chaining auth on a persistent session" do
294+
session = HTTP.persistent(dummy.endpoint)
295+
296+
sock1 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/1").to_s
297+
sock2 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/2").to_s
298+
299+
refute_equal "", sock1
300+
assert_equal sock1, sock2
301+
ensure
302+
session&.close
303+
end
304+
305+
it "shares the connection pool across chained sessions" do
306+
session = HTTP.persistent(dummy.endpoint)
307+
chained = session.headers("Accept" => "application/json")
308+
309+
assert_same session.instance_variable_get(:@clients),
310+
chained.instance_variable_get(:@clients)
311+
ensure
312+
session&.close
313+
end
314+
315+
it "does not share pool for non-persistent sessions" do
316+
chained = session.headers("Accept" => "application/json")
317+
318+
refute_same session.instance_variable_get(:@clients),
319+
chained.instance_variable_get(:@clients)
320+
end
321+
end
322+
280323
describe "base_uri" do
281324
it "returns a Session from base_uri" do
282325
chained = session.base_uri(dummy.endpoint)

0 commit comments

Comments
 (0)