From b2a5baa48de3e05754cbe560eb2a3508cfaf2f38 Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:03:22 +0300 Subject: [PATCH] fix(library-db): name slow-write ops for macOS stall diagnosis (#1043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(library-db): name slow-write ops for macOS stall diagnosis (#1040) Replace generic op=misc labels on production library-db write paths with stable module.action names (sync_state.*, track.*, tombstone.*, cmd.*, …) so SLOW write logs pinpoint the call site. Document the naming convention on LibraryStore::with_conn. Diagnostic step for #1040 — no behaviour change. * docs: add CHANGELOG and credits for PR #1043 Library-db slow-write op naming diagnostic for issue #1040. --- CHANGELOG.md | 8 +++++ .../psysonic-library/src/browse_support.rs | 2 +- .../crates/psysonic-library/src/canonical.rs | 2 +- .../crates/psysonic-library/src/commands.rs | 8 ++--- .../psysonic-library/src/repos/sync_state.rs | 30 +++++++++---------- .../psysonic-library/src/repos/track.rs | 6 ++-- .../src/repos/track_id_history.rs | 4 +-- .../crates/psysonic-library/src/store.rs | 5 ++++ .../crates/psysonic-library/src/sync/delta.rs | 4 +-- .../psysonic-library/src/sync/tombstone.rs | 6 ++-- src/config/settingsCredits.ts | 1 + 11 files changed, 45 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e17f6906..eb21416e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Fixed +### Library DB — named slow-write ops for stall diagnosis + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#1043](https://github.com/Psychotoxical/psysonic/pull/1043)** + +* Production `library-db` write paths now log stable `module.action` op names instead of the generic `misc`, so the next `[library-db] SLOW write` line on macOS (or elsewhere) identifies the call site — diagnostic step for playback stalls under long write-lock holds ([#1040](https://github.com/Psychotoxical/psysonic/issues/1040)). + + + ### Servers — complete border on the active server card **By [@Psychotoxical](https://github.com/Psychotoxical), PR [#998](https://github.com/Psychotoxical/psysonic/pull/998)** diff --git a/src-tauri/crates/psysonic-library/src/browse_support.rs b/src-tauri/crates/psysonic-library/src/browse_support.rs index 877332a1..70d34d29 100644 --- a/src-tauri/crates/psysonic-library/src/browse_support.rs +++ b/src-tauri/crates/psysonic-library/src/browse_support.rs @@ -34,7 +34,7 @@ pub(crate) fn reconcile_album_stars( ) -> Result<(), String> { runtime .store - .with_conn("misc", |conn| { + .with_conn("browse.reconcile_album_stars", |conn| { if starred.is_empty() { conn.execute( "UPDATE album SET starred_at = NULL \ diff --git a/src-tauri/crates/psysonic-library/src/canonical.rs b/src-tauri/crates/psysonic-library/src/canonical.rs index 762a0405..8fe20967 100644 --- a/src-tauri/crates/psysonic-library/src/canonical.rs +++ b/src-tauri/crates/psysonic-library/src/canonical.rs @@ -81,7 +81,7 @@ pub fn link_all_tracks_for_server( server_id: &str, now: i64, ) -> Result { - store.with_conn_mut("misc", |conn| { + store.with_conn_mut("canonical.link_all_tracks", |conn| { let tx = conn.transaction()?; let mut stmt = tx.prepare( "SELECT id, isrc, mbid_recording FROM track \ diff --git a/src-tauri/crates/psysonic-library/src/commands.rs b/src-tauri/crates/psysonic-library/src/commands.rs index e466a1b1..0b955d61 100644 --- a/src-tauri/crates/psysonic-library/src/commands.rs +++ b/src-tauri/crates/psysonic-library/src/commands.rs @@ -476,7 +476,7 @@ pub async fn library_get_offline_path( ) -> Result { let path = runtime .store - .with_conn("misc", |conn| { + .with_conn("cmd.get_offline_path", |conn| { conn.query_row( "SELECT local_path FROM track_offline \ WHERE server_id = ?1 AND track_id = ?2", @@ -1028,7 +1028,7 @@ pub fn patch_content_hash( } runtime .store - .with_conn("misc", |conn| { + .with_conn("cmd.patch_content_hash", |conn| { conn.execute( "UPDATE track SET content_hash = ?3 \ WHERE server_id = ?1 AND id = ?2", @@ -1075,7 +1075,7 @@ pub(crate) fn apply_track_patch( runtime .store - .with_conn("misc", |conn| { + .with_conn("cmd.patch_track", |conn| { // One UPDATE per field present — keeps SQL simple and // matches the spec's per-field patch semantics. if let Some(v) = starred_at { @@ -1211,7 +1211,7 @@ pub fn library_purge_server( let mut report = PurgeReportDto::default(); runtime .store - .with_conn_mut("misc", |conn| { + .with_conn_mut("cmd.purge_server", |conn| { let tx = conn.transaction()?; let track_count: i64 = tx.query_row("SELECT COUNT(*) FROM track WHERE server_id = ?1", params![server_id], |r| r.get(0))?; diff --git a/src-tauri/crates/psysonic-library/src/repos/sync_state.rs b/src-tauri/crates/psysonic-library/src/repos/sync_state.rs index 7e9dbedf..f5297f37 100644 --- a/src-tauri/crates/psysonic-library/src/repos/sync_state.rs +++ b/src-tauri/crates/psysonic-library/src/repos/sync_state.rs @@ -29,7 +29,7 @@ impl<'a> SyncStateRepository<'a> { /// if none exists. All non-PK columns fall back to their schema DEFAULTs /// (`sync_phase='idle'`, `initial_sync_cursor_json='{}'`, …). pub fn ensure(&self, server_id: &str, library_scope: &str) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.ensure", |conn| { conn.execute( "INSERT OR IGNORE INTO sync_state (server_id, library_scope) VALUES (?1, ?2)", params![server_id, library_scope], @@ -73,7 +73,7 @@ impl<'a> SyncStateRepository<'a> { cursor: &Value, ) -> Result<(), String> { let json = serde_json::to_string(cursor).map_err(|e| e.to_string())?; - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_initial_sync_cursor", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, initial_sync_cursor_json) \ VALUES (?1, ?2, ?3) \ @@ -134,7 +134,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, flags: u32, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_capability_flags", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, capability_flags) \ VALUES (?1, ?2, ?3) \ @@ -192,7 +192,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, phase: &str, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_sync_phase", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, sync_phase) \ VALUES (?1, ?2, ?3) \ @@ -212,7 +212,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, last_scan_iso: Option<&str>, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_server_last_scan_iso", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, server_last_scan_iso) \ VALUES (?1, ?2, ?3) \ @@ -232,7 +232,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, last_modified_ms: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_indexes_last_modified_ms", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, indexes_last_modified_ms) \ VALUES (?1, ?2, ?3) \ @@ -330,7 +330,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, epoch_ms: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_next_poll_at", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, next_poll_at) \ VALUES (?1, ?2, ?3) \ @@ -375,7 +375,7 @@ impl<'a> SyncStateRepository<'a> { stats: &Value, ) -> Result<(), String> { let json = serde_json::to_string(stats).map_err(|e| e.to_string())?; - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_poll_stats_json", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, poll_stats_json) \ VALUES (?1, ?2, ?3) \ @@ -412,7 +412,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, count: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_local_track_count", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, local_track_count) \ VALUES (?1, ?2, ?3) \ @@ -447,7 +447,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, count: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_server_track_count", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, server_track_count) \ VALUES (?1, ?2, ?3) \ @@ -488,7 +488,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, unreliable: bool, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_n1_bulk_unreliable", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, n1_bulk_unreliable) \ VALUES (?1, ?2, ?3) \ @@ -508,7 +508,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, epoch_ms: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_last_full_sync_at", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, last_full_sync_at) \ VALUES (?1, ?2, ?3) \ @@ -528,7 +528,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, epoch_ms: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_last_delta_sync_at", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, last_delta_sync_at) \ VALUES (?1, ?2, ?3) \ @@ -549,7 +549,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, last_modified_ms: i64, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_artists_last_modified_ms", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, artists_last_modified_ms) \ VALUES (?1, ?2, ?3) \ @@ -570,7 +570,7 @@ impl<'a> SyncStateRepository<'a> { library_scope: &str, tier: &str, ) -> Result<(), String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("sync_state.set_library_tier", |conn| { conn.execute( "INSERT INTO sync_state (server_id, library_scope, library_tier) \ VALUES (?1, ?2, ?3) \ diff --git a/src-tauri/crates/psysonic-library/src/repos/track.rs b/src-tauri/crates/psysonic-library/src/repos/track.rs index 3fdf7eb4..b9056acc 100644 --- a/src-tauri/crates/psysonic-library/src/repos/track.rs +++ b/src-tauri/crates/psysonic-library/src/repos/track.rs @@ -191,7 +191,7 @@ impl<'a> TrackRepository<'a> { /// Next generation stamp for a full-resync orphan sweep on this server. pub fn next_resync_gen(&self, server_id: &str) -> Result { - self.store.with_conn("misc", |c| { + self.store.with_conn("track.next_resync_gen", |c| { c.query_row( "SELECT COALESCE(MAX(resync_gen), 0) + 1 FROM track WHERE server_id = ?1", params![server_id], @@ -203,7 +203,7 @@ impl<'a> TrackRepository<'a> { /// IS-7 — soft-delete live rows not re-stamped during the active resync. pub fn sweep_resync_orphans(&self, server_id: &str, resync_gen: i64) -> Result { let now = now_unix_ms(); - let changed = self.store.with_conn_mut("misc", |c| { + let changed = self.store.with_conn_mut("track.sweep_resync_orphans", |c| { c.execute( "UPDATE track SET deleted = 1, synced_at = ?3 \ WHERE server_id = ?1 AND deleted = 0 AND resync_gen != ?2", @@ -418,7 +418,7 @@ impl<'a> TrackRepository<'a> { if rows.is_empty() { return Ok(RemapStats::default()); } - self.store.with_conn_mut("misc", |conn| { + self.store.with_conn_mut("track.upsert_batch_remap", |conn| { let tx = conn.transaction()?; let mut remapped: Vec = Vec::new(); let mut upsert = tx.prepare_cached(UPSERT_SQL)?; diff --git a/src-tauri/crates/psysonic-library/src/repos/track_id_history.rs b/src-tauri/crates/psysonic-library/src/repos/track_id_history.rs index cf4f1904..39621b46 100644 --- a/src-tauri/crates/psysonic-library/src/repos/track_id_history.rs +++ b/src-tauri/crates/psysonic-library/src/repos/track_id_history.rs @@ -26,7 +26,7 @@ impl<'a> TrackIdHistoryRepository<'a> { server_id: &str, old_id: &str, ) -> Result, String> { - self.store.with_conn("misc", |conn| { + self.store.with_conn("track_id_history.lookup", |conn| { conn.query_row( "SELECT new_id FROM track_id_history \ WHERE server_id = ?1 AND old_id = ?2", @@ -40,7 +40,7 @@ impl<'a> TrackIdHistoryRepository<'a> { /// Count the rows recorded for this server — used by tests and by /// post-sync diagnostics (Settings „Library index" panel later). pub fn count_for_server(&self, server_id: &str) -> Result { - self.store.with_conn("misc", |conn| { + self.store.with_conn("track_id_history.count", |conn| { conn.query_row( "SELECT COUNT(*) FROM track_id_history WHERE server_id = ?1", params![server_id], diff --git a/src-tauri/crates/psysonic-library/src/store.rs b/src-tauri/crates/psysonic-library/src/store.rs index 27335b32..35c798eb 100644 --- a/src-tauri/crates/psysonic-library/src/store.rs +++ b/src-tauri/crates/psysonic-library/src/store.rs @@ -103,6 +103,11 @@ impl LibraryStore { } /// Writer connection — sync ingest, migrations, mutations. + /// + /// `op` is logged on slow writes (`[library-db] SLOW write op=…`) — use a + /// stable `module.action` label (e.g. `sync_state.set_sync_phase`, + /// `track.upsert_batch_remap`), not the generic `"misc"`, so production + /// stalls can be attributed to a specific call site. pub(crate) fn with_conn( &self, op: &'static str, diff --git a/src-tauri/crates/psysonic-library/src/sync/delta.rs b/src-tauri/crates/psysonic-library/src/sync/delta.rs index 2a0b2be2..3ba36605 100644 --- a/src-tauri/crates/psysonic-library/src/sync/delta.rs +++ b/src-tauri/crates/psysonic-library/src/sync/delta.rs @@ -284,7 +284,7 @@ impl<'a> DeltaSyncRunner<'a> { fn local_track_updated_watermark(&self) -> Result, SyncError> { self.store - .with_conn("misc", |c| { + .with_conn("delta.local_track_watermark", |c| { c.query_row( "SELECT MAX(server_updated_at) FROM track \ WHERE server_id = ?1 AND deleted = 0", @@ -297,7 +297,7 @@ impl<'a> DeltaSyncRunner<'a> { fn local_album_ids(&self) -> Result, SyncError> { self.store - .with_conn("misc", |c| { + .with_conn("delta.local_album_ids", |c| { let mut stmt = c.prepare( "SELECT DISTINCT album_id FROM track \ WHERE server_id = ?1 AND deleted = 0 AND album_id IS NOT NULL", diff --git a/src-tauri/crates/psysonic-library/src/sync/tombstone.rs b/src-tauri/crates/psysonic-library/src/sync/tombstone.rs index 6c5008f5..7ae8ba8e 100644 --- a/src-tauri/crates/psysonic-library/src/sync/tombstone.rs +++ b/src-tauri/crates/psysonic-library/src/sync/tombstone.rs @@ -115,7 +115,7 @@ impl<'a> TombstoneReconciler<'a> { fn next_candidates(&self, budget: u32) -> Result, SyncError> { self.store - .with_conn("misc", |c| { + .with_conn("tombstone.next_candidates", |c| { let mut stmt = c.prepare( "SELECT id FROM track \ WHERE server_id = ?1 AND deleted = 0 \ @@ -133,7 +133,7 @@ impl<'a> TombstoneReconciler<'a> { fn mark_deleted(&self, id: &str) -> Result<(), SyncError> { self.store - .with_conn("misc", |c| { + .with_conn("tombstone.mark_deleted", |c| { c.execute( "UPDATE track SET deleted = 1, synced_at = ?3 \ WHERE server_id = ?1 AND id = ?2", @@ -146,7 +146,7 @@ impl<'a> TombstoneReconciler<'a> { fn mark_synced(&self, id: &str) -> Result<(), SyncError> { self.store - .with_conn("misc", |c| { + .with_conn("tombstone.mark_synced", |c| { c.execute( "UPDATE track SET synced_at = ?3 \ WHERE server_id = ?1 AND id = ?2", diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index 3445ffc0..4a3b1446 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -157,6 +157,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Themed startup splash before Vite loads — deferred window show, per-theme logo gradient (PR #1030)', 'Artist page: Top Tracks play when album list is empty on the page (PR #1031)', 'PsyLab Connections tab with server capability readout, Navidrome admin-role probe, and tab-bar layout fix; server-capability framework with AudioMuse detection via OpenSubsonic sonicSimilarity and sonic Instant Mix routing on Navidrome ≥0.62 (PR #1033)', + 'Library DB: named slow-write op labels for macOS playback-stall diagnosis (PR #1043)', ], }, {