Skip to content

Commit

Permalink
fix: enhance transaction handling and error logging in Save method
Browse files Browse the repository at this point in the history
- Implemented a unique transaction ID for better traceability during database operations.
- Added retry logic for handling "database is locked" errors, with exponential backoff delays.
- Improved error logging to provide detailed information on transaction attempts and failures.
- Ensured that all database operations are wrapped in a transaction with proper rollback on panic or error.
  • Loading branch information
tphakala committed Jan 3, 2025
1 parent bf2a055 commit 6ff42f3
Showing 1 changed file with 67 additions and 25 deletions.
92 changes: 67 additions & 25 deletions internal/datastore/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"log"
"os"
"strconv"
"strings"
"time"

"github.com/google/uuid"
"github.com/tphakala/birdnet-go/internal/conf"
"gorm.io/gorm"
"gorm.io/gorm/logger"
Expand Down Expand Up @@ -66,39 +68,79 @@ func New(settings *conf.Settings) Interface {

// Save stores a note and its associated results as a single transaction in the database.
func (ds *DataStore) Save(note *Note, results []Results) error {
// Begin a transaction
tx := ds.DB.Begin()
if tx.Error != nil {
return fmt.Errorf("starting transaction: %w", tx.Error)
}
// Generate a unique transaction ID (first 8 chars of UUID)
txID := fmt.Sprintf("tx-%s", uuid.New().String()[:8])

// Retry configuration
maxRetries := 5
baseDelay := 500 * time.Millisecond

var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
// Begin a transaction
tx := ds.DB.Begin()
if tx.Error != nil {
lastErr = fmt.Errorf("starting transaction: %w", tx.Error)
continue
}

// Roll back the transaction if a panic occurs
defer func() {
if r := recover(); r != nil {
// Roll back the transaction if a panic occurs
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()

// Save the note and its associated results within the transaction
if err := tx.Create(note).Error; err != nil {
tx.Rollback()
if strings.Contains(strings.ToLower(err.Error()), "database is locked") {
delay := baseDelay * time.Duration(attempt+1)
log.Printf("[%s] Database locked, retrying in %v (attempt %d/%d)", txID, delay, attempt+1, maxRetries)
time.Sleep(delay)
continue
}
return fmt.Errorf("saving note: %w", err)
}
}()

// Save the note and its associated results within the transaction
if err := tx.Create(note).Error; err != nil {
tx.Rollback()
return fmt.Errorf("saving note: %w", err)
}
// Assign the note ID to each result and save them
for _, result := range results {
result.NoteID = note.ID
if err := tx.Create(&result).Error; err != nil {
tx.Rollback()
if strings.Contains(strings.ToLower(err.Error()), "database is locked") {
delay := baseDelay * time.Duration(attempt+1)
log.Printf("[%s] Database locked, retrying in %v (attempt %d/%d)", txID, delay, attempt+1, maxRetries)
time.Sleep(delay)
continue
}
lastErr = fmt.Errorf("saving result: %w", err)
return lastErr
}
}

// Assign the note ID to each result and save them
for _, result := range results {
result.NoteID = note.ID
if err := tx.Create(&result).Error; err != nil {
tx.Rollback()
return fmt.Errorf("saving result: %w", err)
// Commit the transaction
if err := tx.Commit().Error; err != nil {
if strings.Contains(strings.ToLower(err.Error()), "database is locked") {
delay := baseDelay * time.Duration(attempt+1)
log.Printf("[%s] Database locked, retrying in %v (attempt %d/%d)", txID, delay, attempt+1, maxRetries)
time.Sleep(delay)
continue
}
return fmt.Errorf("committing transaction: %w", err)
}

// Log if retry count is not 0 and transaction was successful
if attempt > 0 {
log.Printf("[%s] Database transaction successful after %d attempts", txID, attempt+1)
}
}

// Commit the transaction
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("committing transaction: %w", err)
// If we get here, the transaction was successful
return nil
}
return nil

// If we've exhausted all retries
return fmt.Errorf("[%s] failed after %d attempts: %w", txID, maxRetries, lastErr)
}

// Get retrieves a note by its ID from the database.
Expand Down

0 comments on commit 6ff42f3

Please sign in to comment.