From e4e3e91ecbc69d934938825c58c51c5e95b2185e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 08:05:41 +0000 Subject: [PATCH] =?UTF-8?q?Ajouter=20le=20pr=C3=A9buffering=20configurable?= =?UTF-8?q?=20au=20cache=20pour=20=C3=A9viter=20les=20erreurs=20de=20lectu?= =?UTF-8?q?re=20pr=C3=A9matur=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problème : Après la correction de la race condition précédente, les fichiers étaient créés sur disque mais la lecture commençait immédiatement, avant qu'il y ait suffisamment de données. Cela causait des erreurs FLAC "Expected one more byte" car le décodeur essayait de lire un fichier incomplet. Solution - Prébuffering : Attendre qu'une quantité minimale de données (512 KB par défaut, ~5 secondes de FLAC) soit téléchargée avant que add_from_url() et add_from_reader() retournent le pk. Cela permet au cache progressif de fonctionner correctement : le fichier a suffisamment de données pour commencer la lecture pendant que le téléchargement continue en arrière-plan. Changements : - Ajout d'un champ min_prebuffer_size dans Cache (défaut: 512 KB) - Ajout de méthodes set_prebuffer_size() et get_prebuffer_size() - Ajout de la constante DEFAULT_PREBUFFER_SIZE (512 KB) - Modification de add_from_url() : utilise wait_until_min_size() - Modification de add_from_reader() : utilise wait_until_min_size() - Cas où download déjà en cours : attend également le prébuffering Résultat testé : ✓ L'exemple play_and_cache fonctionne sans erreur FLAC ✓ Le prébuffering garantit suffisamment de données avant la lecture ✓ Le cache progressif fonctionne : lecture pendant le téléchargement ✓ Configurable : peut être ajusté selon les besoins (0 = désactivé) --- pmocache/src/cache.rs | 126 +++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/pmocache/src/cache.rs b/pmocache/src/cache.rs index 0057213d..89b5cb8a 100755 --- a/pmocache/src/cache.rs +++ b/pmocache/src/cache.rs @@ -17,6 +17,9 @@ use tokio::io::AsyncRead; use tokio::sync::RwLock; use tracing; +/// Taille minimale de prébuffering par défaut (512 KB = ~5 secondes de FLAC) +pub const DEFAULT_PREBUFFER_SIZE: u64 = 512 * 1024; + /// Paramètres statiques d'un cache spécialisé. pub trait CacheConfig: Send + Sync { /// Extension des fichiers générés (ex: `"webp"`, `"flac"`). @@ -58,6 +61,8 @@ pub struct Cache { downloads: Arc>>>, /// Factory pour créer des transformers (optionnel) transformer_factory: Option StreamTransformer + Send + Sync>>, + /// Taille minimale de prébuffering en octets (0 = désactivé) + min_prebuffer_size: u64, /// Phantom data pour le type de configuration _phantom: std::marker::PhantomData, } @@ -124,10 +129,39 @@ impl Cache { db: Arc::new(db), downloads: Arc::new(RwLock::new(HashMap::new())), transformer_factory, + min_prebuffer_size: DEFAULT_PREBUFFER_SIZE, _phantom: std::marker::PhantomData, }) } + /// Configure la taille minimale de prébuffering + /// + /// # Arguments + /// + /// * `size` - Taille minimale en octets (0 = désactivé) + /// + /// # Exemple + /// + /// ```rust,no_run + /// use pmocache::{Cache, CacheConfig}; + /// + /// struct MyConfig; + /// impl CacheConfig for MyConfig { + /// fn file_extension() -> &'static str { "dat" } + /// } + /// + /// let mut cache = Cache::::new("./cache", 1000).unwrap(); + /// cache.set_prebuffer_size(1024 * 1024); // 1 MB de prébuffering + /// ``` + pub fn set_prebuffer_size(&mut self, size: u64) { + self.min_prebuffer_size = size; + } + + /// Retourne la taille minimale de prébuffering configurée + pub fn get_prebuffer_size(&self) -> u64 { + self.min_prebuffer_size + } + /// Télécharge un fichier depuis une URL et l'ajoute au cache /// /// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL. @@ -178,23 +212,22 @@ impl Cache { } // 4. Vérifier si un download est déjà en cours pour ce pk - { + let download_handle = { let downloads = self.downloads.read().await; - if downloads.contains_key(&pk) { - // Download déjà en cours pour ce contenu, attendre que le fichier soit créé - tracing::debug!("Download already in progress for pk {}", pk); - drop(downloads); // Libérer le lock avant la boucle d'attente + downloads.get(&pk).cloned() + }; - // Attendre que le fichier soit créé (pour le cache progressif) - let file_path = self.get_file_path(&pk); - let mut attempts = 0; - while !file_path.exists() && attempts < 100 { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - attempts += 1; - } + if let Some(download) = download_handle { + // Download déjà en cours pour ce contenu, attendre le prébuffering + tracing::debug!("Download already in progress for pk {}, waiting for prebuffering", pk); - return Ok(pk); + if self.min_prebuffer_size > 0 { + download.wait_until_min_size(self.min_prebuffer_size).await + .map_err(|e| anyhow!("Prebuffering failed: {}", e))?; + tracing::debug!("Prebuffering complete for pk {}", pk); } + + return Ok(pk); } // 5. Lancer le téléchargement complet avec transformer @@ -217,6 +250,13 @@ impl Cache { tracing::warn!("Error enforcing cache limit: {}", e); } + // Attendre le prébuffering (pour le cache progressif) + if self.min_prebuffer_size > 0 { + download.wait_until_min_size(self.min_prebuffer_size).await + .map_err(|e| anyhow!("Prebuffering failed: {}", e))?; + tracing::debug!("Prebuffering complete for pk {} ({} bytes)", pk, self.min_prebuffer_size); + } + // Lancer une tâche de nettoyage en background let downloads_clone = self.downloads.clone(); let pk_clone = pk.clone(); @@ -225,23 +265,6 @@ impl Cache { downloads_clone.write().await.remove(&pk_clone); }); - // Attendre que le fichier soit créé sur disque (pour le cache progressif) - // On attend jusqu'à 5 secondes maximum - let file_path = self.get_file_path(&pk); - let mut attempts = 0; - while !file_path.exists() && attempts < 100 { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - attempts += 1; - } - - if !file_path.exists() { - tracing::warn!( - "File {} not created after waiting 5 seconds, pk={}", - file_path.display(), - pk - ); - } - Ok(pk) } @@ -304,12 +327,22 @@ impl Cache { } // 4. Vérifier si un download est déjà en cours pour ce pk - { + let download_handle = { let downloads = self.downloads.read().await; - if downloads.contains_key(&pk) { - tracing::debug!("Download already in progress for pk {}", pk); - return Ok(pk); + downloads.get(&pk).cloned() + }; + + if let Some(download) = download_handle { + // Download déjà en cours pour ce contenu, attendre le prébuffering + tracing::debug!("Download already in progress for pk {}, waiting for prebuffering", pk); + + if self.min_prebuffer_size > 0 { + download.wait_until_min_size(self.min_prebuffer_size).await + .map_err(|e| anyhow!("Prebuffering failed: {}", e))?; + tracing::debug!("Prebuffering complete for pk {}", pk); } + + return Ok(pk); } // 5. Reconstituer le reader complet (header + reste) @@ -339,6 +372,14 @@ impl Cache { tracing::warn!("Error enforcing cache limit: {}", e); } + // Attendre le prébuffering (pour le cache progressif) + if self.min_prebuffer_size > 0 { + download.wait_until_min_size(self.min_prebuffer_size).await + .map_err(|e| anyhow!("Prebuffering failed: {}", e))?; + tracing::debug!("Prebuffering complete for pk {} ({} bytes)", pk, self.min_prebuffer_size); + } + + // Lancer une tâche de nettoyage en background let downloads_clone = self.downloads.clone(); let pk_clone = pk.clone(); tokio::spawn(async move { @@ -346,23 +387,6 @@ impl Cache { downloads_clone.write().await.remove(&pk_clone); }); - // Attendre que le fichier soit créé sur disque (pour le cache progressif) - // On attend jusqu'à 5 secondes maximum - let file_path = self.get_file_path(&pk); - let mut attempts = 0; - while !file_path.exists() && attempts < 100 { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - attempts += 1; - } - - if !file_path.exists() { - tracing::warn!( - "File {} not created after waiting 5 seconds, pk={}", - file_path.display(), - pk - ); - } - Ok(pk) }