Skip to content

fix(analysis): serialize background analyzer behind scans + batch writes#320

Open
InstaZDLL wants to merge 1 commit into
mainfrom
fix/analysis-scan-contention
Open

fix(analysis): serialize background analyzer behind scans + batch writes#320
InstaZDLL wants to merge 1 commit into
mainfrom
fix/analysis-scan-contention

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Problème

L'analyseur audio (BPM / loudness, sweep bibliothèque) tournait comme second writer SQLite en concurrence du scanner, ce qui provoquait deux bugs réels sur les grosses bibliothèques (mesuré sur ~900 fichiers AAC) :

  1. Résultats perdus : un INSERT INTO track_analysis ligne-par-ligne en course avec un scan tombait sur database is locked après le busy-timeout de 5 s → rows_affected=0, le BPM/loudness fraîchement décodé était jeté silencieusement (décode Symphonia coûteux gaspillé).
  2. Wall-clock du scan gonflé : le décode CPU-intensif + les rafales d'écriture de l'analyseur se battaient pour chaque cœur et l'unique writer → un scan AAC propre de ~34 s passait à plus d'une minute.

Fix

  • Sérialisation : scan_folder_inner (chemin commun à scan_folder / rescan_library / import_paths / fs-watcher / rescan de démarrage) incrémente un compteur process-wide SCANS_IN_FLIGHT via un guard RAII. run_analyze_library le wait-out en tête de chaque itération, cancel-aware.
  • Batch + retry : écritures track_analysis groupées par 16 dans une transaction, avec retry du batch entier sur SQLITE_BUSY / SQLITE_LOCKED (octet bas 5/6) en backoff exponentiel → plus aucune perte sur lock transitoire. Le buffer est flush sur cancel + en fin de run → un run stoppé sauve quand même tout ce qu'il avait décodé.

Tests

  • Tests unitaires pour le classifieur code_is_busy (masquage des codes étendus SQLite).
  • cargo check + cargo clippy --all-targets verts. (Tests app-crate non exécutables localement sur Windows — DLL Tauri — valent via CI.)

Docs

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • L’analyse de bibliothèque en arrière-plan s’exécute désormais de façon plus fluide lorsqu’un scan est déjà en cours.
    • Les résultats d’analyse sont enregistrés par lots, ce qui améliore la fiabilité des traitements sur de grandes bibliothèques.
  • Corrections de bugs

    • Réduction des erreurs de verrouillage de base de données pendant les scans et analyses simultanés.
    • Les résultats d’analyse sont mieux conservés, limitant les pertes silencieuses de données comme le BPM ou le volume.

The library-wide audio analyzer was a second SQLite writer running
concurrently with the scanner, which caused two real bugs on large
libraries:

- Lost results: a per-row `INSERT INTO track_analysis` racing a
  concurrent scan hit `database is locked` after the 5 s busy-timeout
  and the freshly-decoded BPM / loudness was silently dropped
  (`rows_affected=0`), wasting the expensive Symphonia decode.
- Inflated scan wall-clock: the analyzer's CPU-heavy decode + write
  bursts contended for every core and the single writer, turning a
  clean ~34 s AAC scan into a minute-plus.

Fix:
- Park the analyzer while a scan is walking. `scan_folder_inner` (the
  one path every scan entry point routes through) bumps a process-wide
  `SCANS_IN_FLIGHT` counter via an RAII guard; `run_analyze_library`
  waits it out at the top of each iteration, cancel-aware.
- Batch `track_analysis` writes 16-at-a-time in one transaction and
  retry the whole batch on `SQLITE_BUSY` / `SQLITE_LOCKED` (low-byte
  5/6) with exponential backoff, so a transient lock never drops a
  decoded result. The buffer is flushed on cancel + at run end, so a
  stopped run still saves everything it had decoded.

Adds unit tests for the busy-code classifier. Docs synced
(library.md + the single-writer rule in CLAUDE.md).
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: fix Bug fix size: l 200-500 lines labels Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Le scan bibliothèque expose désormais un état partagé “en cours”, et l’analyse en arrière-plan attend la fin du scan avant de tamponner puis persister les résultats track_analysis par lots, avec retry sur verrous SQLite.

Changes

Synchronisation scan et analyse

Layer / File(s) Summary
Compteur de scans en vol
src-tauri/crates/app/src/commands/scan.rs
scan_folder_inner incrémente un compteur process-wide des scans actifs via un garde RAII, et scan_in_flight() expose cet état.
Boucle d’analyse et tampon
src-tauri/crates/app/src/commands/analysis.rs
run_analyze_library attend la fin d’un scan actif, accumule les résultats décodés par lots de 16, puis effectue un flush final, même sur annulation.
Retry SQLite et documentation
src-tauri/crates/app/src/commands/analysis.rs, docs/features/library.md, CLAUDE.md
Les helpers de persistance classent SQLITE_BUSY/SQLITE_LOCKED, rejouent un lot en transaction avec backoff exponentiel, et les notes/documents reflètent ce flux.

Sequence Diagram(s)

sequenceDiagram
  participant scan_folder_inner
  participant run_analyze_library
  participant scan_in_flight
  participant SQLite

  scan_folder_inner->>scan_folder_inner: incrémente SCANS_IN_FLIGHT
  run_analyze_library->>scan_in_flight: lit l’état du scan
  loop tant que scan_in_flight() est vrai
    run_analyze_library->>run_analyze_library: attend `wait_out_active_scan()`
  end
  run_analyze_library->>SQLite: flush `track_analysis` par lots
  alt SQLITE_BUSY / SQLITE_LOCKED
    run_analyze_library->>run_analyze_library: backoff exponentiel
    run_analyze_library->>SQLite: réessaie le lot
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#206 : modifie aussi src-tauri/crates/app/src/commands/scan.rs et touche la coordination du scan avec des écritures liées à la base.
  • InstaZDLL/WaveFlow#288 : change aussi la boucle de run_analyze_library dans src-tauri/crates/app/src/commands/analysis.rs, avec des ajustements proches sur l’arrêt et le flux d’exécution.

Poem

Le scan lève son drapeau,
l’analyse fait une pause au bord du flot,
puis 16 notes glissent vers SQLite,
sans se heurter aux verrous du soir. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit le format Conventional Commits et résume bien la sérialisation de l’analyseur derrière les scans et les écritures par lot.
Description check ✅ Passed La description couvre le problème, le correctif, les tests et la documentation, même si la structure du template est partiellement différente.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analysis-scan-contention

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/analysis.rs`:
- Around line 329-335: Le flux de persistance dans persist_batch_once et ses
flush via flush_analysis_batch peut encore écrire pendant un scan, car l’attente
n’est faite qu’avant le décodage. Ajoute une vérification d’attente du scan
juste avant chaque flush DB, y compris dans les chemins qui déclenchent le flush
par taille de batch et en fin de traitement, afin que batch,
flush_analysis_batch et persist_batch_once ne continuent pas tant qu’un scan est
actif.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23196ae2-478f-4cc0-bdc1-6b97cb217960

📥 Commits

Reviewing files that changed from the base of the PR and between 53801bd and 24bd7aa.

📒 Files selected for processing (4)
  • CLAUDE.md
  • docs/features/library.md
  • src-tauri/crates/app/src/commands/analysis.rs
  • src-tauri/crates/app/src/commands/scan.rs

Comment on lines +329 to +335
batch.push(PendingAnalysis {
track_id,
result,
analyzed_at: Utc::now().timestamp_millis(),
});
if batch.len() >= ANALYSIS_BATCH_SIZE {
failed += flush_analysis_batch(pool, &mut batch).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Attends aussi le scan avant chaque flush DB.

L’attente actuelle est seulement avant le décodage. Si un scan démarre entre la ligne 308 et un flush ligne 335 ou 363, persist_batch_once() peut encore écrire pendant le scan et finir par dropper tout le batch après retries.

Correctif proposé
 async fn flush_analysis_batch(pool: &SqlitePool, batch: &mut Vec<PendingAnalysis>) -> u32 {
     if batch.is_empty() {
         return 0;
     }
     const MAX_ATTEMPTS: usize = 6;
     let mut backoff = Duration::from_millis(50);
     for attempt in 1..=MAX_ATTEMPTS {
+        while crate::commands::scan::scan_in_flight() {
+            tokio::time::sleep(SCAN_WAIT_POLL).await;
+        }
         match persist_batch_once(pool, batch).await {
             Ok(()) => {
                 batch.clear();
                 return 0;
             }

Also applies to: 360-363, 473-489

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/app/src/commands/analysis.rs` around lines 329 - 335, Le
flux de persistance dans persist_batch_once et ses flush via
flush_analysis_batch peut encore écrire pendant un scan, car l’attente n’est
faite qu’avant le décodage. Ajoute une vérification d’attente du scan juste
avant chaque flush DB, y compris dans les chemins qui déclenchent le flush par
taille de batch et en fin de traitement, afin que batch, flush_analysis_batch et
persist_batch_once ne continuent pas tant qu’un scan est actif.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant