HTTP/2 Connection Pooling Integration Plan#
Problem Statement#
The current architecture has two separate connection management systems that conflict:
- Conpool: Manages TCP/TLS connections with one-connection-per-request semantics
- H2_adapter: Has its own hashtable cache for HTTP/2 client state
This creates several issues:
Issue 1: Duplicate Connection Management#
Current Flow:
Requests.make_request
│
├── Conpool.connection_with_info ──────┐
│ (gets TCP/TLS connection) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ If ALPN = "h2": │ │
│ │ H2_adapter.request │◄┘
│ │ │ │
│ │ ├── Own hashtable cache │ ← Duplicates Conpool!
│ │ └── get_or_create_client │
│ └─────────────────────────────────┘
│
└── When switch closes → Connection returned to Conpool
BUT H2_adapter cache still references it!
Issue 2: No True Multiplexing#
HTTP/2's key advantage is stream multiplexing - multiple concurrent requests on one connection. Currently:
- Each
Requests.get/post/...call gets a fresh connection from Conpool - Even though HTTP/2 could handle multiple streams on one connection
- We pay full connection setup cost per request
Issue 3: Race Conditions#
H2_adapterusesMutex.t(Unix/pthreads mutex)- This blocks the entire OS thread in Eio, breaking cooperative scheduling
- Should use
Eio.Mutex.tfor proper Eio integration
Issue 4: Connection Lifecycle Mismatch#
- Conpool expects: get connection → use → release back to pool
- HTTP/2 expects: establish connection → keep open → multiplex many requests → eventually close
- These models are fundamentally different
Proposed Solution: Protocol-Aware Connection Abstraction#
Design Goals#
- Unified API: Users don't need to know if they're using HTTP/1.1 or HTTP/2
- True HTTP/2 Multiplexing: Multiple concurrent requests share one connection
- Eio-Native: Use Eio concurrency primitives throughout
- Backward Compatible: Existing Requests API unchanged
- Efficient Resource Use: Minimize connection count for HTTP/2
Architecture Overview#
Requests Session
│
▼
┌────────────────────┐
│ Protocol Router │
│ (chooses handler) │
└─────────┬──────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ HTTP/1.1 Handler │ │ HTTP/2 Handler │
│ │ │ │
│ Uses Conpool as-is │ │ H2_connection_pool │
│ (1 conn = 1 req) │ │ (1 conn = N reqs) │
└─────────────────────┘ └─────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌────────────────┐
│ Conpool │ │ H2_connection │
│ (TCP/TLS)│ │ (multiplexed) │
└──────────┘ └────────────────┘
Key Components#
1. H2_connection_pool - HTTP/2 Connection Manager#
A new module that manages HTTP/2 connections with proper multiplexing:
(** HTTP/2 Connection Pool.
Unlike Conpool which manages one-request-per-connection for HTTP/1.1,
this module manages long-lived HTTP/2 connections with stream multiplexing.
Each endpoint (host:port) has at most one HTTP/2 connection with multiple
streams. When MAX_CONCURRENT_STREAMS is reached, requests queue until
a stream slot becomes available. *)
type t
val create : sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> unit -> t
(** Create a new HTTP/2 connection pool. *)
type endpoint = {
host : string;
port : int;
}
val request :
t ->
endpoint:endpoint ->
establish:(unit -> Eio.Flow.two_way_ty Eio.Resource.t) ->
meth:Method.t ->
uri:Uri.t ->
headers:Headers.t ->
body:Body.t ->
Response.t
(** Make an HTTP/2 request, multiplexing on existing connection if available.
@param establish Function to create new TCP/TLS connection if needed.
This is called at most once per endpoint.
@raise Error.H2_protocol_error on protocol errors *)
2. H2_multiplexed_connection - Per-Endpoint Connection State#
(** A single multiplexed HTTP/2 connection to an endpoint. *)
type t = {
flow : Eio.Flow.two_way_ty Eio.Resource.t;
client : H2_client.t;
mutex : Eio.Mutex.t; (* Eio mutex for stream allocation *)
mutable active_streams : int;
max_concurrent_streams : int; (* From SETTINGS *)
stream_available : Eio.Condition.t; (* Signal when stream frees *)
mutable closed : bool;
}
val acquire_stream : t -> unit
(** Block until a stream slot is available, then reserve it. *)
val release_stream : t -> unit
(** Release a stream slot, signaling waiters. *)
val request : t -> H2_protocol.request -> H2_protocol.response
(** Make a request on this connection. *)
3. Protocol Router Integration#
Modify Requests.make_request to route based on cached protocol knowledge:
(* In requests.ml *)
type protocol_hint =
| Unknown (* First request to this endpoint *)
| Definitely_h1 (* Server doesn't support H2 *)
| Definitely_h2 (* ALPN negotiated H2 *)
(* Protocol hint cache per endpoint *)
let protocol_hints : (string, protocol_hint) Hashtbl.t = Hashtbl.create 64
let make_request_internal ... =
let endpoint_key = Printf.sprintf "%s:%d" host port in
match Hashtbl.find_opt protocol_hints endpoint_key with
| Some Definitely_h2 ->
(* Use HTTP/2 pool directly - no need for ALPN *)
H2_connection_pool.request h2_pool ~endpoint ...
| Some Definitely_h1 ->
(* Use HTTP/1.1 via Conpool *)
Conpool.with_connection http_pool endpoint (fun flow ->
Http_client.make_request ...)
| None | Some Unknown ->
(* First request - use Conpool to discover protocol via ALPN *)
let conn_info = Conpool.connection_with_info ~sw pool endpoint in
match conn_info.tls_epoch with
| Some { alpn_protocol = Some "h2"; _ } ->
Hashtbl.replace protocol_hints endpoint_key Definitely_h2;
(* Hand off connection to H2 pool, make request *)
H2_connection_pool.adopt_connection h2_pool ~endpoint conn_info.flow;
H2_connection_pool.request h2_pool ~endpoint ...
| _ ->
Hashtbl.replace protocol_hints endpoint_key Definitely_h1;
Http_client.make_request ... conn_info.flow
Implementation Phases#
Phase 1: Fix Immediate Issues (Low Risk)#
Goal: Fix race conditions and improve current implementation without architectural changes.
-
Replace Unix.Mutex with Eio.Mutex in H2_adapter
- File:
lib/h2/h2_adapter.ml - Change
Mutex.ttoEio.Mutex.t - Update
with_mutexto useEio.Mutex.use_rw
- File:
-
Add connection validity checks
- Before reusing cached H2_client, verify the underlying flow is still open
- Handle GOAWAY frames properly
-
Add tests for concurrent HTTP/2 requests
- Test multiple fibers making requests to same endpoint
- Verify no race conditions
Estimated scope: ~50 lines changed, low risk
Phase 2: H2_connection_pool Module (Medium Risk)#
Goal: Create proper HTTP/2 connection pooling with multiplexing.
-
Create
lib/h2/h2_connection_pool.ml[i]- Connection state per endpoint
- Stream slot management with Eio.Condition
- Automatic connection establishment on first request
- Connection health monitoring
-
Create
lib/h2/h2_multiplexed_connection.ml[i]- Wrap H2_client with stream counting
- MAX_CONCURRENT_STREAMS enforcement
- Proper cleanup on connection close
-
Add comprehensive tests
- Multiplexing: N concurrent requests on 1 connection
- Stream exhaustion: >MAX_CONCURRENT_STREAMS requests
- Connection failure: mid-request disconnection
- GOAWAY handling
Estimated scope: ~400 lines new code, 2 new modules
Phase 3: Protocol Router Integration (Medium Risk)#
Goal: Integrate H2_connection_pool with Requests session.
-
Add H2_connection_pool to Requests.t
type t = T : { ... h2_pool : H2_connection_pool.t; (* NEW *) protocol_hints : (string, protocol_hint) Hashtbl.t; (* NEW *) } -> t -
Modify connection routing in make_request
- Check protocol hints before connecting
- Update hints based on ALPN results
- Route HTTP/2 to H2_connection_pool
-
Handle protocol downgrade
- If server sends HTTP_1_1_REQUIRED error, update hint
- Retry request via HTTP/1.1
-
Update statistics tracking
- Add HTTP/2 connection/stream stats
- Expose via
Requests.stats
Estimated scope: ~200 lines changed in requests.ml
Phase 4: Connection Handoff (Higher Risk)#
Goal: Seamlessly transfer connections from Conpool to H2_pool.
-
Add connection "adoption" to H2_connection_pool
- Accept an already-established TLS flow
- Perform H2 handshake
- Add to pool for future reuse
-
Prevent Conpool from reclaiming H2 connections
- When connection is handed to H2_pool, don't return it to Conpool
- This requires careful lifetime management
-
Handle edge cases
- Connection fails during adoption
- Server rejects H2 after ALPN (rare but possible)
- TLS session resumption
Estimated scope: ~150 lines, careful lifetime management needed
Phase 5: Optimizations (Lower Priority)#
-
Preemptive connection establishment
- For known H2 endpoints, establish connection before first request
- Reduces latency for subsequent requests
-
Connection warming
- Maintain minimum connections to frequently-used endpoints
- Background PING to keep connections alive
-
Load balancing across connections
- For very high throughput, allow multiple H2 connections per endpoint
- Distribute streams across connections
Data Structures#
H2_connection_pool State#
┌─────────────────────────────────────────────────────────────┐
│ H2_connection_pool.t │
├─────────────────────────────────────────────────────────────┤
│ connections: (endpoint, H2_multiplexed_connection.t) Hashtbl│
│ mutex: Eio.Mutex.t │
│ sw: Eio.Switch.t │
│ clock: Eio.Time.clock │
└─────────────────────────────────────────────────────────────┘
│
│ per endpoint
▼
┌─────────────────────────────────────────────────────────────┐
│ H2_multiplexed_connection.t │
├─────────────────────────────────────────────────────────────┤
│ flow: Eio.Flow.two_way │
│ client: H2_client.t │
│ hpack_encoder: H2_hpack.Encoder.t │
│ hpack_decoder: H2_hpack.Decoder.t │
│ active_streams: int (mutable) │
│ max_concurrent_streams: int (from SETTINGS) │
│ stream_available: Eio.Condition.t │
│ closed: bool (mutable) │
│ reader_fiber: unit Eio.Fiber.t (reads frames) │
│ last_stream_id: int32 (for GOAWAY) │
└─────────────────────────────────────────────────────────────┘
Request Flow with Multiplexing#
Fiber A: GET /users ───┐
│
Fiber B: GET /posts ───┼───► H2_connection_pool.request
│ │
Fiber C: GET /comments ───┘ │
▼
┌─────────────────────┐
│ acquire_stream() │
│ (blocks if at max) │
└─────────┬───────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
Stream 1 Stream 3 Stream 5
(Fiber A) (Fiber B) (Fiber C)
│ │ │
└───────────────────┼───────────────────┘
│
▼
┌─────────────────────┐
│ Single TCP/TLS conn │
│ to example.com:443 │
└─────────────────────┘
API Changes#
New Types in Requests#
(** HTTP/2 connection statistics *)
type h2_stats = {
connections : int; (** Active HTTP/2 connections *)
total_streams : int; (** Total streams opened *)
active_streams : int; (** Currently active streams *)
max_concurrent : int; (** Max concurrent streams (from SETTINGS) *)
}
(** Extended session statistics *)
type stats = {
(* existing fields *)
requests_made : int;
total_time : float;
retries_count : int;
(* new HTTP/2 fields *)
h2_stats : h2_stats option; (** HTTP/2 statistics, if any H2 connections *)
}
Backward Compatibility#
- All existing
Requests.*functions unchanged - Same API for HTTP/1.1 and HTTP/2 (protocol transparent)
- New
h2_statsfield is optional, existing code ignores it
Testing Strategy#
Unit Tests#
-
H2_connection_pool tests
- Create pool, make request, verify stream counting
- MAX_CONCURRENT_STREAMS enforcement
- Connection reuse verification
-
Stream slot management
- Concurrent requests within limit
- Blocking when at limit
- Proper cleanup on request completion
Integration Tests#
-
Protocol selection
- ALPN negotiation (mock TLS)
- Protocol hint caching
- Fallback to HTTP/1.1
-
Concurrent requests
- N fibers making requests to same H2 endpoint
- Verify single connection used
- Verify all requests complete
-
Error handling
- Connection drops mid-stream
- GOAWAY during request
- Server-side RST_STREAM
Stress Tests#
-
High concurrency
- 100 concurrent requests to same endpoint
- Verify multiplexing works
- Measure latency vs HTTP/1.1
-
Connection churn
- Repeated connect/disconnect cycles
- No resource leaks
Risks and Mitigations#
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Stream deadlock | Medium | High | Timeout on stream acquisition, tests |
| Memory leak | Medium | Medium | Careful resource cleanup, Eio.Switch |
| Race conditions | Medium | High | Eio.Mutex throughout, no Unix.Mutex |
| Performance regression | Low | Medium | Benchmark before/after |
| Compatibility issues | Low | Medium | Extensive testing with real servers |
Success Criteria#
- Correctness: All existing tests pass
- Multiplexing: Concurrent H2 requests share single connection
- Performance: H2 requests to same host faster than H1.1
- Resource efficiency: Connection count reduced for H2 hosts
- No regressions: HTTP/1.1 behavior unchanged
References#
- RFC 9113 Section 5.1.2 (Stream Concurrency)
- RFC 9113 Section 6.5.2 (SETTINGS_MAX_CONCURRENT_STREAMS)
- RFC 9113 Section 6.8 (GOAWAY)
- Eio documentation (structured concurrency, Mutex, Condition)
- Current H2_client implementation