-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Problem
hyper's "legacy" pool did many things at once, and needed to support the ability to turn off various parts of it.
Users often ask to be able to customize parts of the connection pool. Either with smaller options, such as adding a limit for maximum number of connections, or modifying behavior, such as manually evicting specific connections.
Solution
We plan to create several new utility types, all that are kinds of pools, to separate out the concerns. This will allow each type to worry about a single use case. (See #3849).
Developer Experience (DX)
Before getting to the implementation, this considers the developer experience of using the pool types and their composition to create their own pooling needs.
HTTP/1 pool to a single host
let svc = ServiceBuilder::new()
.layer(pool::cache())
.layer(conn::http1())
.layer(connect:tcp())
.service(dns::hickory());
HTTP/2 pool to a single host
let svc = ServiceBuilder::new()
.layer(pool::singleton())
.layer(conn::http2())
.layer(connect:tcp())
.service(dns::hickory());
Adding expiration
let svc = ServiceBuilder::new()
.layer(pool::expire(|svc, now| {
// enforce a max lifetime of 300s
svc.retain(|inner| {
now - inner.meta().connected_at < Duration::from_secs(300)
});
// check again in
Duration::from_secs(30)
}))
.layer(pool::singleton())
// record some metadata when created
.layer(svc::meta(|_s| MyMeta {
connected_at: Instant::now(),
}))
.layer(conn::http2())
.layer(connect:tcp())
.service(dns::hickory());
General purpose pool
let connect = svc::builder()
.layer(tls::rustls())
.layer(tls::alpn(["h2", "http/1.1"]))
.layer(connect::tcp())
.service(dns::hickory());
// layer will be applied after negotiation
let http1 = svc::builder()
.layer(pool::cache())
.layer(conn::http1());
let http2 = svc::builder()
.layer(pool::singleton())
.layer(conn::http2());
let svc = pool::map::builder()
.key(pool::map::scheme_and_auth())
.value(move |req| {
pool::negotiate()
.connect(connect.clone())
.inspect(|c| {
c.negotiated_protocols() == b"h2"
})
.left(http1.clone())
.right(http2.clone())
});
Implementation
The DX examples are purposefully more concise, while still being possible. The implementation will include types and builders that allow more customization, as desired. But they will also start conservatively, trying to not expose implementation internals while these types are explored more in the real world.
This may mean expose publicly Builder types, but returning currently-unnameable Service
or Layer
types.
The module structure will be in hyper_util::client::pool
, with the following submodules:
cache
expire
map
negotiate
singleton
They each work with different layers, so they are laid out below grouped by role, instead of alphabetically.
Cache
The cache is a single list of cached services, bundled with a MakeService
. Calling the cache returns either an existing service, or makes a new one. The returned impl Service
can be used to send requests, and when dropped, it will try to be returned back to the cache.
Singleton
The singleton pool combines a MakeService
that should only produce a single active connection. It can bundle all concurrent calls to it, so that only one connection is made. All calls to the singleton will return a clone of the inner service once established. This fits the HTTP/2 case well.
Negotiate
The negotiate pool allows for a service that can decide between two service types based on an intermediate return value. It differs from typical routing since it doesn't depend on the request, but the response. The main use case is support ALPN upgrades to HTTP/2, with a fallback to HTTP/1.
It's possible we could make this type even more general purpose, but that's a potential feature for the future.
Expire
Most pools need a way to expire services/connections within them. Instead of making each type know how to spawn a watching task and evict connections, the expire
pool can wrap any other pool type with a single watcher concept. This has the additional benefit of allowing more customization on when and why to expire a connection.
The expire
pool wraps another, along with a timer and way to inspect the wrapped pool. The other pool types all will have a retain
method on them, which will allow passing a closure to determine which cached services should be retained or expired. Thus, an inner service could check both it's idle time and its lifetime, for instance.
Map
The map isn't a typical Service
, but rather stand-alone type that can map requests to a key and service factory. This is because the service is more of a router, and cannot determine which inner service to check for backpressure since it's not know until the request is made.
The map implementation allows customization of extracting a key, and how to construct a MakeService
for that key.
FAQ
Can I limit the number of connections being made at once?
let svc = ServiceBuilder::new()
.layer(pool::cache())
+ .concurrency_limit(5)
.layer(conn::http1())
.layer(connect:tcp())
.service(dns::hickory());
Can I limit the number of max connections?
A layer which shares a count and only drops once the connection is fully dropped could be implemented and added:
let svc = ServiceBuilder::new()
.layer(pool::cache())
+ .layer(conn_semaphore(5))
.layer(conn::http1())
.layer(connect:tcp())
.service(dns::hickory());
Can I evict a connection whenever I want?
pool.retain(|c| {
if should_evict(c) {
// bye
false
} else {
// keep it
true
}
})