Files
pmomusic/Blackboard/Report/fix-backend-mutex-poisoned.md
Eric Coissac 4f8f902f3f Fix backend mutex poisoned panic on OpenHome stop
Fix backend mutex poisoned panic when stopping playback on OpenHome renderer

This commit addresses a panic that occurred when stopping playback on OpenHome renderers, which was caused by:

1. Mutex poisoning from unhandled panics in backend operations
2. A regression revealed after initial fixes, where inconsistent renderer state caused index out of bounds errors

Changes include:
- Added defensive error handling for mutex operations to prevent poisoning
- Implemented bounds-checking for current_index in sync_queue to handle inconsistent renderer states
- Removed redundant clear_queue calls that were causing additional failures
- Improved error tolerance in queue operations

The fix ensures that the backend mutex remains healthy and that inconsistent renderer states are handled gracefully rather than causing panics.
2026-01-17 13:40:23 +01:00

6.0 KiB

Rapport: Fix Backend Mutex Poisoned

Résumé

Ce rapport documente la correction du bug "Backend mutex poisoned" qui se manifestait lors de l'arrêt de la lecture sur un renderer OpenHome, ainsi que la régression Round 2 découverte après les premiers correctifs.

Analyse de la cause racine

Round 1 : Mutex empoisonné

Le mutex backend était empoisonné par des panics non gérés lors d'opérations sur les renderers. Les appels .unwrap() et .expect() sur le mutex propageaient les panics au lieu de les gérer gracieusement.

Round 2 : Régression après Round 1

Les correctifs du Round 1 ont révélé un problème plus profond. La chaîne d'échecs était :

  1. DeleteAll échoue avec erreur 501 : Le renderer OpenHome rejette l'action DeleteAll pendant la lecture active
  2. Le code continue (grâce aux correctifs Round 1 qui tolèrent les erreurs)
  3. État incohérent du renderer : OpenHome retourne 5 IDs via IdArray mais une <TrackList> vide via ReadList
  4. Panic "index out of bounds" : sync_queue() accède à items[4] alors que items.len() == 0
  5. Mutex empoisonné : Le panic dans le thread empoisonne le mutex

Preuve dans les logs :

OpenHome Playlist IdArray returned ... id_count=5
OpenHome Playlist tracks read ... track_count=0 expected_count=5

Corrections apportées

1. Tolérance des erreurs clear_queue (musicrenderer.rs)

Fichier : pmocontrol/src/music_renderer/musicrenderer.rs

Modification : La méthode clear_for_playlist_attach() tolère maintenant les erreurs de clear_queue() au lieu de propager l'erreur.

pub fn clear_for_playlist_attach(&self) -> Result<(), ControlPointError> {
    let mut backend = self.lock_backend_for("clear_for_playlist_attach");

    // Clear the queue first (ignore errors - queue will be replaced anyway by sync_queue)
    // Some backends (OpenHome) may reject DeleteAll if currently playing
    if let Err(err) = backend.clear_queue() {
        warn!(
            renderer = self.id().0.as_str(),
            error = %err,
            "Clear queue failed when preparing for playlist attach (continuing anyway)"
        );
    }

    // Then stop playback (ignore errors if already stopped)
    backend.stop().or_else(|err| {
        warn!(
            renderer = self.id().0.as_str(),
            error = %err,
            "Stop failed when preparing for playlist attach (continuing anyway)"
        );
        Ok(())
    })
}

Justification : Le DeleteAll n'est pas critique car sync_queue() remplacera de toute façon le contenu de la queue.

2. Suppression du clear_queue redondant (control_point.rs)

Fichier : pmocontrol/src/control_point.rs

Modification : Suppression de l'appel renderer.clear_queue()? dans attach_queue_to_playlist_internal().

Avant :

// Clear the local queue (detach binding + clear runtime queue structure)
self.detach_playlist_binding(renderer_id, "attach_new_playlist");
renderer.clear_queue()?;

Après :

// Detach any existing binding (local queue will be replaced by sync_queue later)
self.detach_playlist_binding(renderer_id, "attach_new_playlist");

Justification : Ce clear_queue() était redondant car clear_for_playlist_attach() le fait déjà, et causait un second échec DeleteAll.

3. Bounds-check pour current_index (openhome.rs)

Fichier : pmocontrol/src/queue/openhome.rs

Modification : Ajout d'une vérification de bornes dans sync_queue() pour gérer l'état incohérent du renderer OpenHome.

let snapshot = self.queue_snapshot()?;
// Note: current_index may point to an index that doesn't exist in items
// if the OpenHome renderer is in an inconsistent state (e.g., IdArray returns
// IDs but ReadList returns empty TrackList). We must bounds-check here.
let playing_info = snapshot.current_index.and_then(|idx| {
    if idx < snapshot.items.len() {
        Some((
            idx,
            snapshot.items[idx].backend_id,
            snapshot.items[idx].uri.clone(),
            snapshot.items[idx].didl_id.clone(),
        ))
    } else {
        warn!(
            renderer = self.renderer_id.0.as_str(),
            current_index = idx,
            items_len = snapshot.items.len(),
            "OpenHome renderer in inconsistent state: current_index out of bounds, treating as no current track"
        );
        None
    }
});

Justification : Gère le cas où le renderer OpenHome retourne un état incohérent (IDs sans données de track correspondantes).

4. Ajout de l'import warn (openhome.rs)

Fichier : pmocontrol/src/queue/openhome.rs

Modification : Ajout de warn à l'import tracing.

use tracing::{debug, warn};

Fichiers modifiés

Fichier Modification
pmocontrol/src/music_renderer/musicrenderer.rs Tolérance des erreurs clear_queue() dans clear_for_playlist_attach()
pmocontrol/src/control_point.rs Suppression du clear_queue() redondant
pmocontrol/src/queue/openhome.rs Bounds-check + import warn

Comportement attendu après correction

  1. DeleteAll échoue : Warning loggé, le code continue
  2. État incohérent détecté : Warning loggé, traité comme "pas de track courante"
  3. sync_queue réussit : La playlist est correctement attachée au renderer
  4. Pas de panic : Le mutex reste sain

Tests effectués

  • L'utilisateur a confirmé que la correction fonctionne ("Ok ça marche")

Notes techniques

Les correctifs du Round 1 (gestion d'erreur sur mutex) ont révélé un bug préexistant : le code supposait que l'état du renderer OpenHome était toujours cohérent. En réalité, certains renderers peuvent retourner des IDs de tracks sans les données correspondantes, notamment lorsqu'une opération DeleteAll est rejetée pendant la lecture.

La solution adoptée est défensive : plutôt que de supposer un état cohérent, le code vérifie les bornes et traite les incohérences comme des cas dégradés (pas de track courante) plutôt que de paniquer.