Architecture remediation A/B/F: poison-tolerant locks, graceful backend init, doc fixes

Workstream A — poison-tolerant locking:
- Add utils/lock.rs with MutexSafe/RwLockSafe extension traits that recover a
  poisoned std::sync lock instead of panicking, plus unit tests.
- Replace all 153 .lock().unwrap() and 4 .read()/.write().unwrap() production
  sites with _safe variants across 14 files, eliminating the player
  crash-cascade class. Tokio async mutexes are unchanged.

Workstream B — graceful backend init:
- create_player_backend no longer panics when MPV/ExoPlayer fail to initialize;
  it falls back to NullBackend and emits a backend-init-failed event so the UI
  can show "playback unavailable" instead of the app crashing. Fatal DB-setup
  panics are kept.

Workstream F — doc reconciliation:
- Rewrite software-architecture.md's inaccurate "thin UI / ~800 lines" claims to
  reflect reality (~20.5k non-test frontend) and document the events+polling
  hybrid plus the new locking/backend-init behavior.
This commit is contained in:
2026-06-20 16:03:54 +02:00
parent 0738ef10ec
commit 6866f03c55
18 changed files with 345 additions and 178 deletions
+11 -9
View File
@@ -1,5 +1,7 @@
//! Tauri commands for download operations
#[cfg(test)]
use crate::utils::lock::MutexSafe;
use std::sync::{Arc, Mutex};
use tauri::State;
use log::{debug, error, info, warn};
@@ -1577,7 +1579,7 @@ mod tests {
fn setup_test_db() -> Database {
let db = Database::open_in_memory().unwrap();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// Create server
conn.execute(
@@ -1620,7 +1622,7 @@ mod tests {
fn test_download_item_returns_correct_id_on_insert() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// Insert a new download
conn.execute(
@@ -1658,7 +1660,7 @@ mod tests {
fn test_download_item_upsert_returns_correct_id() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// First insert
conn.execute(
@@ -1733,7 +1735,7 @@ mod tests {
// to get the correct download ID, regardless of whether INSERT or UPDATE occurred.
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// First insert
conn.execute(
@@ -1781,7 +1783,7 @@ mod tests {
fn test_download_album_returns_correct_ids() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// Insert downloads for multiple items (simulating album download)
let track_ids = vec!["item1", "item2", "item3"];
@@ -1827,7 +1829,7 @@ mod tests {
fn test_download_album_upsert_returns_correct_ids() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// First download attempt - insert all
let track_ids = vec!["item1", "item2", "item3"];
@@ -1887,7 +1889,7 @@ mod tests {
fn test_get_downloads_returns_correct_metadata() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// Insert a download
conn.execute(
@@ -1932,7 +1934,7 @@ mod tests {
fn test_download_status_transitions() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
// Insert pending download
conn.execute(
@@ -1978,7 +1980,7 @@ mod tests {
fn test_download_progress_updates() {
let db = setup_test_db();
let conn = db.connection();
let conn = conn.lock().unwrap();
let conn = conn.lock_safe();
conn.execute(
"INSERT INTO downloads (item_id, user_id, file_path, status, progress, bytes_downloaded, file_size)
+2 -1
View File
@@ -1,5 +1,6 @@
//! TRACES: UR-003, UR-004, UR-005, UR-010, UR-020, UR-021 | JA-022, JA-023, JA-024, JA-025, JA-026 | DR-001
use crate::utils::lock::MutexSafe;
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@@ -1291,7 +1292,7 @@ fn get_player_status(controller: &PlayerController) -> PlayerStatus {
fn get_queue_status(controller: &PlayerController) -> QueueStatus {
let queue = controller.queue();
let queue_lock = queue.lock().unwrap();
let queue_lock = queue.lock_safe();
QueueStatus {
items: queue_lock.items().to_vec(),
+4 -3
View File
@@ -3,6 +3,7 @@
//!
//! TRACES: UR-007, UR-035, UR-036 | JA-004, JA-005, JA-029, JA-030, JA-031
use crate::utils::lock::MutexSafe;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
@@ -26,17 +27,17 @@ impl RepositoryManager {
}
pub fn create(&self, handle: String, repository: HybridRepository) {
let mut repos = self.repositories.lock().unwrap();
let mut repos = self.repositories.lock_safe();
repos.insert(handle, Arc::new(repository));
}
pub fn get(&self, handle: &str) -> Option<Arc<HybridRepository>> {
let repos = self.repositories.lock().unwrap();
let repos = self.repositories.lock_safe();
repos.get(handle).cloned()
}
pub fn destroy(&self, handle: &str) {
let mut repos = self.repositories.lock().unwrap();
let mut repos = self.repositories.lock_safe();
repos.remove(handle);
}
}