Zackriya-Solutions/meetily · error · anyhow::Error

Failed to start transaction: {}

Error message

Failed to start transaction: {}

What it means

A pooled connection was acquired, but issuing BEGIN failed. On SQLite this is nearly always SQLITE_BUSY at BEGIN time - another connection already holds the single write lock - or the connection was left in a bad state by an earlier error on it.

Source

Thrown at frontend/src-tauri/src/audio/retranscription.rs:434

        return Err(anyhow!("Retranscription cancelled"));
    }

    emit_progress(&app, &meeting_id, "saving", 80, "Saving transcripts...");

    // Create transcript segments with proper timestamps from VAD
    let segments = create_transcript_segments(&all_transcripts);

    // Save to database
    let app_state = app
        .try_state::<AppState>()
        .ok_or_else(|| anyhow!("App state not available"))?;

    // Wrap delete+insert+update in a transaction to prevent data loss
    let pool = app_state.db_manager.pool();
    let mut conn = pool.acquire().await.map_err(|e| anyhow!("DB error: {}", e))?;
    let mut tx = sqlx::Connection::begin(&mut *conn)
        .await
        .map_err(|e| anyhow!("Failed to start transaction: {}", e))?;

    sqlx::query("DELETE FROM transcripts WHERE meeting_id = ?")
        .bind(&meeting_id)
        .execute(&mut *tx)
        .await
        .map_err(|e| anyhow!("Failed to delete existing transcripts: {}", e))?;

    for segment in &segments {
        sqlx::query(
            "INSERT INTO transcripts (id, meeting_id, transcript, timestamp, audio_start_time, audio_end_time, duration)
             VALUES (?, ?, ?, ?, ?, ?, ?)"
        )
        .bind(&segment.id)
        .bind(&meeting_id)
        .bind(&segment.text)
        .bind(&segment.timestamp)
        .bind(segment.audio_start_time)
        .bind(segment.audio_end_time)

View on GitHub (pinned to 0281737d87)

Solutions

  1. Read the inner error: 'database is locked' means retry after the concurrent writer finishes.
  2. Serialize database writers through a single writer queue or task so only one transaction is open at a time.
  3. Enable busy_timeout/WAL on the pool so BEGIN waits for the lock instead of erroring.
  4. Retry the retranscription once the other operation completes.
Defensive patterns

Strategy: retry

Try / catch

// Retry the whole short transaction once on BEGIN failure
let tx = match sqlx::Connection::begin(&mut *conn).await {
    Ok(t) => t,
    Err(e) => {
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        sqlx::Connection::begin(&mut *conn).await?
    }
};

Prevention

When it happens

Trigger: Another task opens a write transaction (import saving, live transcript writes, summary update) at the exact moment retranscription starts its delete+insert transaction; nested or abandoned transactions on the same connection.

Common situations: Simultaneous DB-heavy operations finishing at the same time; a background sync task that writes during retranscription save.

Related errors


AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16). Data as JSON: /api/errors/5e8d58ff260a954e. Report an issue: GitHub.