Refactor FlacCacheSink for parallel write tasks to prevent file truncation
Problem: When TrackBoundary arrived, the pump was awaited before continuing, causing file truncation when pcm_tx was dropped while data was still buffering. Solution: Allow multiple pump tasks to run in parallel: - Create dedicated channel (track_tx/track_rx) for each track's pump - Main loop reads from rx and dispatches segments to current pump via track_tx - When TrackBoundary arrives: drop track_tx (signals pump to finish) and immediately start new pump - Old pump continues writing in background until all data is flushed This prevents truncation in progressive cache scenario (radio streaming). Changes in flac_cache_sink.rs: - Replace pump_track_segments_owned() with pump_track_segments_from_channel() - Remove rx ownership passing - each pump gets its own channel - Dispatcher loop reads rx and forwards to active pump - No await on pump completion - let it finish in background
This commit is contained in:
@@ -154,17 +154,21 @@ impl NodeLogic for FlacCacheSinkLogic {
|
|||||||
collection_ref,
|
collection_ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawner pump_future avec ownership de rx
|
// Créer un channel dédié pour dispatcher les chunks vers ce pump
|
||||||
// Cela permet d'attendre cache_future séparément et de pusher à la playlist immédiatement
|
let (track_tx, track_rx) = mpsc::channel::<Arc<AudioSegment>>(16);
|
||||||
let pump_handle = tokio::spawn(pump_track_segments_owned(
|
|
||||||
|
// Lancer le pump en arrière-plan avec son channel dédié
|
||||||
|
// Cela permet à plusieurs pumps de tourner simultanément (écriture parallèle)
|
||||||
|
let pump_handle = tokio::spawn(pump_track_segments_from_channel(
|
||||||
first_segment,
|
first_segment,
|
||||||
rx, // move ownership!
|
track_rx,
|
||||||
pcm_tx,
|
pcm_tx,
|
||||||
bits_per_sample,
|
bits_per_sample,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
stop_token.clone(),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Envoyer le first_segment déjà vers le track_tx est inutile car on l'a passé directement
|
||||||
|
|
||||||
// Attendre SEULEMENT le prebuffer (cache retourne après 512KB)
|
// Attendre SEULEMENT le prebuffer (cache retourne après 512KB)
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
tracing::debug!("FlacCacheSink: Waiting for cache prebuffer to complete");
|
tracing::debug!("FlacCacheSink: Waiting for cache prebuffer to complete");
|
||||||
@@ -230,36 +234,70 @@ impl NodeLogic for FlacCacheSinkLogic {
|
|||||||
tracing::info!("FlacCacheSink: Successfully pushed to playlist in {:?}", push_start.elapsed());
|
tracing::info!("FlacCacheSink: Successfully pushed to playlist in {:?}", push_start.elapsed());
|
||||||
}
|
}
|
||||||
|
|
||||||
// MAINTENANT attendre que pump finisse (il continue en arrière-plan)
|
// NE PAS attendre le pump - le laisser finir en arrière-plan
|
||||||
tracing::debug!("FlacCacheSink: Waiting for pump to complete");
|
// Cela permet d'avoir plusieurs pumps en parallèle et évite la troncature
|
||||||
let pump_result = pump_handle.await.map_err(|e| {
|
tracing::debug!("FlacCacheSink: Pump running in background, dispatching segments");
|
||||||
AudioError::ProcessingError(format!("Pump task panicked: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let (_chunks, _samples, _duration_sec, stop_reason, rx_returned) = pump_result?;
|
// Dispatcher les segments depuis rx vers track_tx jusqu'au prochain TrackBoundary
|
||||||
rx = rx_returned; // récupérer rx pour la prochaine track
|
loop {
|
||||||
tracing::debug!("FlacCacheSink: Pump completed");
|
let segment = tokio::select! {
|
||||||
|
result = rx.recv() => {
|
||||||
|
match result {
|
||||||
|
Some(seg) => seg,
|
||||||
|
None => {
|
||||||
|
// EOF sur rx - fin du stream, fermer le pump
|
||||||
|
drop(track_tx);
|
||||||
|
drop(pump_handle);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = stop_token.cancelled() => {
|
||||||
|
drop(track_tx);
|
||||||
|
drop(pump_handle);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Si le fichier était déjà en cache (ChannelClosed), drainer les segments restants
|
match &segment.segment {
|
||||||
// jusqu'au prochain TrackBoundary ou EndOfStream
|
_AudioSegment::Chunk(_) => {
|
||||||
// IMPORTANT: Faire ceci APRÈS l'ajout à la playlist pour ne pas bloquer la lecture
|
// Dispatcher vers le pump actuel
|
||||||
let stop_reason = if matches!(stop_reason, StopReason::ChannelClosed) {
|
if track_tx.send(segment).await.is_err() {
|
||||||
tracing::debug!("File was already in cache, draining remaining segments");
|
// Le pump est mort (channel fermé) - drainer jusqu'au TrackBoundary
|
||||||
drain_until_track_boundary(&mut rx, &stop_token).await?
|
tracing::warn!("FlacCacheSink: pump died, draining until TrackBoundary");
|
||||||
} else {
|
loop {
|
||||||
stop_reason
|
let seg = rx.recv().await;
|
||||||
};
|
match seg {
|
||||||
|
Some(s) if matches!(s.segment, _AudioSegment::Sync(ref m) if matches!(**m, SyncMarker::TrackBoundary { .. })) => {
|
||||||
// Vérifier le stop_reason pour savoir si on continue
|
track_number += 1;
|
||||||
match stop_reason {
|
break;
|
||||||
StopReason::TrackBoundary(_metadata) => {
|
}
|
||||||
// Continuer avec la prochaine track
|
None => return Ok(()),
|
||||||
track_number += 1;
|
_ => continue,
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
StopReason::EndOfStream | StopReason::ChannelClosed => {
|
break;
|
||||||
// Fin de l'encodage
|
}
|
||||||
return Ok(());
|
}
|
||||||
|
_AudioSegment::Sync(marker) => match &**marker {
|
||||||
|
SyncMarker::TrackBoundary { .. } => {
|
||||||
|
// Nouveau morceau - fermer le pump actuel et passer au suivant
|
||||||
|
drop(track_tx); // Ferme le channel, le pump va se terminer proprement
|
||||||
|
tracing::debug!("FlacCacheSink: TrackBoundary detected, pump will finish in background");
|
||||||
|
track_number += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SyncMarker::EndOfStream => {
|
||||||
|
tracing::debug!("FlacCacheSink: EndOfStream received");
|
||||||
|
drop(track_tx);
|
||||||
|
drop(pump_handle);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Transmettre les autres syncmarkers au pump
|
||||||
|
let _ = track_tx.send(segment).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,18 +557,17 @@ async fn pump_track_segments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pompe les segments pour une seule track (s'arrête au TrackBoundary).
|
/// Pompe les segments pour une seule track depuis un channel dédié.
|
||||||
///
|
///
|
||||||
/// Version qui prend ownership de rx pour permettre un await séparé du cache.
|
/// Cette version permet d'avoir plusieurs pumps en parallèle (pour cache progressif),
|
||||||
/// Retourne rx à la fin pour permettre le traitement des tracks suivantes.
|
/// car chaque pump a son propre channel et ne bloque pas le traitement des tracks suivantes.
|
||||||
async fn pump_track_segments_owned(
|
async fn pump_track_segments_from_channel(
|
||||||
first_segment: Arc<AudioSegment>,
|
first_segment: Arc<AudioSegment>,
|
||||||
mut rx: mpsc::Receiver<Arc<AudioSegment>>,
|
mut track_rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||||
pcm_tx: mpsc::Sender<Vec<u8>>,
|
pcm_tx: mpsc::Sender<Vec<u8>>,
|
||||||
bits_per_sample: u8,
|
bits_per_sample: u8,
|
||||||
expected_rate: u32,
|
expected_rate: u32,
|
||||||
stop_token: CancellationToken,
|
) -> Result<(u64, u64, f64), AudioError> {
|
||||||
) -> Result<(u64, u64, f64, StopReason, mpsc::Receiver<Arc<AudioSegment>>), AudioError> {
|
|
||||||
let mut chunks = 0u64;
|
let mut chunks = 0u64;
|
||||||
let mut samples = 0u64;
|
let mut samples = 0u64;
|
||||||
let mut duration_sec = 0.0f64;
|
let mut duration_sec = 0.0f64;
|
||||||
@@ -541,7 +578,8 @@ async fn pump_track_segments_owned(
|
|||||||
if !pcm_bytes.is_empty() {
|
if !pcm_bytes.is_empty() {
|
||||||
if pcm_tx.send(pcm_bytes).await.is_err() {
|
if pcm_tx.send(pcm_bytes).await.is_err() {
|
||||||
drop(pcm_tx);
|
drop(pcm_tx);
|
||||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed, rx));
|
tracing::debug!("pump_track_segments_from_channel: pcm_tx closed on first segment");
|
||||||
|
return Ok((chunks, samples, duration_sec));
|
||||||
}
|
}
|
||||||
chunks += 1;
|
chunks += 1;
|
||||||
samples += chunk.len() as u64;
|
samples += chunk.len() as u64;
|
||||||
@@ -549,21 +587,15 @@ async fn pump_track_segments_owned(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boucle sur les segments suivants
|
// Boucle sur les segments depuis le channel dédié
|
||||||
loop {
|
loop {
|
||||||
let segment = tokio::select! {
|
let segment = match track_rx.recv().await {
|
||||||
result = rx.recv() => {
|
Some(seg) => seg,
|
||||||
match result {
|
None => {
|
||||||
Some(seg) => seg,
|
// Channel fermé - la track est terminée (TrackBoundary a été reçu en amont)
|
||||||
None => {
|
|
||||||
drop(pcm_tx);
|
|
||||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed, rx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = stop_token.cancelled() => {
|
|
||||||
drop(pcm_tx);
|
drop(pcm_tx);
|
||||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed, rx));
|
tracing::debug!("pump_track_segments_from_channel: channel closed, track finished");
|
||||||
|
return Ok((chunks, samples, duration_sec));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -583,31 +615,20 @@ async fn pump_track_segments_owned(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pcm_tx.send(pcm_bytes).await.is_err() {
|
if pcm_tx.send(pcm_bytes).await.is_err() {
|
||||||
|
// Le cache a fermé le channel (erreur ou déjà en cache)
|
||||||
drop(pcm_tx);
|
drop(pcm_tx);
|
||||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed, rx));
|
tracing::debug!("pump_track_segments_from_channel: pcm_tx closed");
|
||||||
|
return Ok((chunks, samples, duration_sec));
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks += 1;
|
chunks += 1;
|
||||||
samples += chunk.len() as u64;
|
samples += chunk.len() as u64;
|
||||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||||
}
|
}
|
||||||
_AudioSegment::Sync(marker) => match &**marker {
|
_AudioSegment::Sync(_marker) => {
|
||||||
SyncMarker::TrackBoundary { metadata, .. } => {
|
// Ignorer les syncmarkers - le TrackBoundary est géré en amont
|
||||||
drop(pcm_tx);
|
// Le channel sera fermé quand le TrackBoundary est détecté
|
||||||
return Ok((
|
}
|
||||||
chunks,
|
|
||||||
samples,
|
|
||||||
duration_sec,
|
|
||||||
StopReason::TrackBoundary(metadata.clone()),
|
|
||||||
rx,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
SyncMarker::EndOfStream => {
|
|
||||||
drop(pcm_tx);
|
|
||||||
return Ok((chunks, samples, duration_sec, StopReason::EndOfStream, rx));
|
|
||||||
}
|
|
||||||
_ => {} // Ignorer les autres syncmarkers
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user