fix(images): reuse a shared HTTP client to stop fd exhaustion
Discover and other image-heavy views created a fresh reqwest::blocking::Client per image (and per notification artwork), exhausting the process file-descriptor limit and crashing the app with "Too many open files". Reuse a single shared client across downloads. cc
This commit is contained in:
2 files changed
+37
-3
No files matched your search
@@ -1039,6 +1039,21 @@ fn v2_resolve_local_artwork(url: &str) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Shared blocking HTTP client for notification artwork downloads. Reused across
|
||||
/// track changes so a long listening session doesn't leak a file descriptor per
|
||||
/// track via a fresh `reqwest::blocking::Client::new()` (EMFILE — see the image
|
||||
/// cache for the same fix on the Discover grid).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn notification_http_client() -> &'static reqwest::blocking::Client {
|
||||
static CLIENT: std::sync::OnceLock<reqwest::blocking::Client> = std::sync::OnceLock::new();
|
||||
CLIENT.get_or_init(|| {
|
||||
reqwest::blocking::Client::builder()
|
||||
.pool_max_idle_per_host(2)
|
||||
.build()
|
||||
.expect("failed to build shared notification HTTP client")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn v2_cache_notification_artwork(url: &str) -> Result<PathBuf, String> {
|
||||
if let Some(local_path) = v2_resolve_local_artwork(url) {
|
||||
@@ -1056,7 +1071,7 @@ pub fn v2_cache_notification_artwork(url: &str) -> Result<PathBuf, String> {
|
||||
return Ok(cache_path);
|
||||
}
|
||||
|
||||
let response = reqwest::blocking::Client::new()
|
||||
let response = notification_http_client()
|
||||
.get(url)
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
use std::sync::OnceLock;
|
||||
use tauri::State;
|
||||
|
||||
/// Shared blocking HTTP client for image downloads.
|
||||
///
|
||||
/// Discover (and any grid view) loads dozens of images at once, each through
|
||||
/// `v2_get_cached_image`. Building a fresh `reqwest::blocking::Client` per image
|
||||
/// opens a new connection pool + file descriptors every time and exhausts the
|
||||
/// process fd limit — the `Os { code: 24, "Too many open files" }` (EMFILE)
|
||||
/// panic seen in `Client::new()`. One reused client fixes the leak and enables
|
||||
/// connection keep-alive across downloads.
|
||||
fn image_http_client() -> &'static reqwest::blocking::Client {
|
||||
static CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(|| {
|
||||
reqwest::blocking::Client::builder()
|
||||
.pool_max_idle_per_host(4)
|
||||
.build()
|
||||
.expect("failed to build shared image HTTP client")
|
||||
})
|
||||
}
|
||||
|
||||
/// Download an image via reqwest (rustls) and write to a temp file.
|
||||
/// Returns a file:// URL that WebKit can load without needing system TLS.
|
||||
/// Used as fallback when the image cache service is unavailable.
|
||||
async fn download_image_to_temp(url: &str) -> Result<String, String> {
|
||||
let url_owned = url.to_string();
|
||||
let bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> {
|
||||
let response = reqwest::blocking::Client::new()
|
||||
let response = image_http_client()
|
||||
.get(&url_owned)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
@@ -110,7 +129,7 @@ pub async fn v2_get_cached_image(
|
||||
// Download the image via reqwest (uses rustls — own CA bundle)
|
||||
let url_clone = url.clone();
|
||||
let bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> {
|
||||
let response = reqwest::blocking::Client::new()
|
||||
let response = image_http_client()
|
||||
.get(&url_clone)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
|
||||
Reference in new issue
Block a user