mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
fix(library-db): name slow-write ops for macOS stall diagnosis (#1043)
* 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.
This commit is contained in:
@@ -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)**
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -81,7 +81,7 @@ pub fn link_all_tracks_for_server(
|
||||
server_id: &str,
|
||||
now: i64,
|
||||
) -> Result<u32, String> {
|
||||
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 \
|
||||
|
||||
@@ -476,7 +476,7 @@ pub async fn library_get_offline_path(
|
||||
) -> Result<OfflinePathDto, String> {
|
||||
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))?;
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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<i64, String> {
|
||||
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<u32, String> {
|
||||
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<RemapEntry> = Vec::new();
|
||||
let mut upsert = tx.prepare_cached(UPSERT_SQL)?;
|
||||
|
||||
@@ -26,7 +26,7 @@ impl<'a> TrackIdHistoryRepository<'a> {
|
||||
server_id: &str,
|
||||
old_id: &str,
|
||||
) -> Result<Option<String>, 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<i64, String> {
|
||||
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],
|
||||
|
||||
@@ -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<R>(
|
||||
&self,
|
||||
op: &'static str,
|
||||
|
||||
@@ -284,7 +284,7 @@ impl<'a> DeltaSyncRunner<'a> {
|
||||
|
||||
fn local_track_updated_watermark(&self) -> Result<Option<i64>, 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<HashSet<String>, 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",
|
||||
|
||||
@@ -115,7 +115,7 @@ impl<'a> TombstoneReconciler<'a> {
|
||||
|
||||
fn next_candidates(&self, budget: u32) -> Result<Vec<String>, 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",
|
||||
|
||||
@@ -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)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user