From b588d9fd0701a80ad318d79dcb14f77458545174 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 4 Aug 2022 18:37:32 +0200 Subject: [PATCH] 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 --- core/src/http_client.rs | 55 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index ed7eac6d..903894ad 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -2,7 +2,7 @@ use std::env::consts::OS; use bytes::Bytes; use futures_util::{future::IntoStream, FutureExt}; -use http::header::HeaderValue; +use http::{header::HeaderValue, Uri}; use hyper::{ client::{HttpConnector, ResponseFuture}, header::USER_AGENT, @@ -10,6 +10,7 @@ use hyper::{ }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use once_cell::sync::OnceCell; use thiserror::Error; use url::Url; @@ -70,15 +71,17 @@ impl From for Error { } } +type HyperClient = Client>, Body>; + #[derive(Clone)] pub struct HttpClient { user_agent: HeaderValue, - proxy: Option, - https_connector: HttpsConnector, + proxy_url: Option, + hyper_client: OnceCell, } impl HttpClient { - pub fn new(proxy: Option<&Url>) -> Self { + pub fn new(proxy_url: Option<&Url>) -> Self { let spotify_version = match OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), _ => SPOTIFY_VERSION.to_string(), @@ -102,6 +105,14 @@ impl HttpClient { 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 { // configuring TLS is expensive and should be done once per process let https_connector = HttpsConnectorBuilder::new() .with_native_roots() @@ -110,11 +121,23 @@ impl HttpClient { .enable_http2() .build(); - Self { - user_agent, - proxy: proxy.cloned(), - https_connector, - } + // When not using a proxy a dummy proxy is configured that will not intercept any traffic. + // This prevents needing to carry the Client Connector generics through the whole project + let proxy = match &proxy_url { + 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) -> Result, Error> { @@ -146,19 +169,7 @@ impl HttpClient { let headers_mut = req.headers_mut(); headers_mut.insert(USER_AGENT, self.user_agent.clone()); - let request = if let Some(url) = &self.proxy { - 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) - }; - + let request = self.hyper_client()?.request(req); Ok(request) } }