Keep using the same hyper client

- Keep using the same hyper client instead of building a new one for
  each request
- This allows the client to reuse connections and improves the
  performance of multiple requests by almost 2x.
- The playlist_tracks example takes 38 secs before and 20 secs after the
  change to enumerate a 180 track playlist
- To avoid carrying the hyper Client generics through the whole project,
  `ProxyConnector` is always used as the Connector, but disabled when
  not using a proxy.
- The client creation is done lazily to keep the `HttpClient::new`
  without a `Result` return type
This commit is contained in:
Daniel M 2022-08-04 18:37:32 +02:00
parent 35f633c93a
commit b588d9fd07

View file

@ -2,7 +2,7 @@ use std::env::consts::OS;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{future::IntoStream, FutureExt}; use futures_util::{future::IntoStream, FutureExt};
use http::header::HeaderValue; use http::{header::HeaderValue, Uri};
use hyper::{ use hyper::{
client::{HttpConnector, ResponseFuture}, client::{HttpConnector, ResponseFuture},
header::USER_AGENT, header::USER_AGENT,
@ -10,6 +10,7 @@ use hyper::{
}; };
use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use once_cell::sync::OnceCell;
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
@ -70,15 +71,17 @@ impl From<HttpClientError> for Error {
} }
} }
type HyperClient = Client<ProxyConnector<HttpsConnector<HttpConnector>>, Body>;
#[derive(Clone)] #[derive(Clone)]
pub struct HttpClient { pub struct HttpClient {
user_agent: HeaderValue, user_agent: HeaderValue,
proxy: Option<Url>, proxy_url: Option<Url>,
https_connector: HttpsConnector<HttpConnector>, hyper_client: OnceCell<HyperClient>,
} }
impl HttpClient { impl HttpClient {
pub fn new(proxy: Option<&Url>) -> Self { pub fn new(proxy_url: Option<&Url>) -> Self {
let spotify_version = match OS { let spotify_version = match OS {
"android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(),
_ => SPOTIFY_VERSION.to_string(), _ => SPOTIFY_VERSION.to_string(),
@ -102,6 +105,14 @@ impl HttpClient {
HeaderValue::from_static(FALLBACK_USER_AGENT) HeaderValue::from_static(FALLBACK_USER_AGENT)
}); });
Self {
user_agent,
proxy_url: proxy_url.cloned(),
hyper_client: OnceCell::new(),
}
}
fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result<HyperClient, Error> {
// configuring TLS is expensive and should be done once per process // configuring TLS is expensive and should be done once per process
let https_connector = HttpsConnectorBuilder::new() let https_connector = HttpsConnectorBuilder::new()
.with_native_roots() .with_native_roots()
@ -110,11 +121,23 @@ impl HttpClient {
.enable_http2() .enable_http2()
.build(); .build();
Self { // When not using a proxy a dummy proxy is configured that will not intercept any traffic.
user_agent, // This prevents needing to carry the Client Connector generics through the whole project
proxy: proxy.cloned(), let proxy = match &proxy_url {
https_connector, Some(proxy_url) => Proxy::new(Intercept::All, proxy_url.to_string().parse()?),
None => Proxy::new(Intercept::None, Uri::from_static("0.0.0.0")),
};
let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?;
let client = Client::builder()
.http2_adaptive_window(true)
.build(proxy_connector);
Ok(client)
} }
fn hyper_client(&self) -> Result<&HyperClient, Error> {
self.hyper_client
.get_or_try_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref()))
} }
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> { pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
@ -146,19 +169,7 @@ impl HttpClient {
let headers_mut = req.headers_mut(); let headers_mut = req.headers_mut();
headers_mut.insert(USER_AGENT, self.user_agent.clone()); headers_mut.insert(USER_AGENT, self.user_agent.clone());
let request = if let Some(url) = &self.proxy { let request = self.hyper_client()?.request(req);
let proxy_uri = url.to_string().parse()?;
let proxy = Proxy::new(Intercept::All, proxy_uri);
let proxy_connector = ProxyConnector::from_proxy(self.https_connector.clone(), proxy)?;
Client::builder().build(proxy_connector).request(req)
} else {
Client::builder()
.http2_adaptive_window(true)
.build(self.https_connector.clone())
.request(req)
};
Ok(request) Ok(request)
} }
} }