Skip to content

Commit

Permalink
fix: retry db transaction when db is busy
Browse files Browse the repository at this point in the history
* Add 5 seconds busy_timeout pragma
* Wait 5 seconds before a transaction get canceled and rolled back
  • Loading branch information
aymanbagabas committed Feb 17, 2022
1 parent 58505c0 commit 04c5d48
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 16 deletions.
34 changes: 22 additions & 12 deletions server/db/sqlite/storage.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sqlite

import (
"context"
"database/sql"
"fmt"
"log"
Expand All @@ -23,7 +24,7 @@ type DB struct {
func NewDB(path string) *DB {
var err error
log.Printf("Opening SQLite db: %s\n", path)
db, err := sql.Open("sqlite", filepath.Join(path, "charm_sqlite.db"))
db, err := sql.Open("sqlite", filepath.Join(path, "charm_sqlite.db")+"?_pragma=busy_timeout=5000")
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -619,20 +620,29 @@ func (me *DB) execOrPanic(tx *sql.Tx, s string) {
}

func (me *DB) wrapTransaction(f func(tx *sql.Tx) error) error {
tx, err := me.db.Begin()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer func() { cancel() }()
tx, err := me.db.BeginTx(ctx, nil)
if err != nil {
log.Printf("Transaction error: %s", err)
if rerr := tx.Rollback(); rerr != nil {
log.Printf("Rollback error: %s", rerr)
}
log.Printf("error starting transaction: %s", err)
return err
}
err = f(tx)
if err != nil {
if rerr := tx.Rollback(); rerr != nil {
log.Printf("Rollback error: %s", rerr)
for {
err = f(tx)
if err != nil {
serr, ok := err.(*sqlite.Error)
if ok && serr.Code() == sqlitelib.SQLITE_BUSY {
continue
}
log.Printf("error in transaction: %s", err)
return err
}
return err
err = tx.Commit()
if err != nil {
log.Printf("error committing transaction: %s", err)
return err
}
break
}
return tx.Commit()
return nil
}
45 changes: 41 additions & 4 deletions server/stats/sqlite/sqlite.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package sqlite

import (
"context"
"database/sql"
"fmt"
"log"
"time"

"modernc.org/sqlite"
_ "modernc.org/sqlite"
sqlitelib "modernc.org/sqlite/lib"
)

const (
Expand Down Expand Up @@ -36,7 +42,7 @@ type Stats struct {

// NewStats returns a *Stats with the default configuration.
func NewStats(path string) (*Stats, error) {
db, err := sql.Open("sqlite", path)
db, err := sql.Open("sqlite", path+"?_pragma=busy_timeout=5000")
if err != nil {
return nil, err
}
Expand All @@ -58,9 +64,12 @@ func (s *Stats) createDB() error {
}

func (s *Stats) increment(field string) {
// SQLite doesn't use placeholders for table or field names
stmt := fmt.Sprintf("UPDATE stats SET %s = %s+1 WHERE id = (SELECT MAX(id) from stats)", field, field)
_, err := s.db.Exec(stmt)
err := s.wrapTransaction(func(tx *sql.Tx) error {
// SQLite doesn't use placeholders for table or field names
stmt := fmt.Sprintf("UPDATE stats SET %s = %s+1 WHERE id = (SELECT MAX(id) from stats)", field, field)
_, err := s.db.Exec(stmt)
return err
})
if err != nil {
log.Printf("error updating stat '%s': %s", field, err)
}
Expand Down Expand Up @@ -145,3 +154,31 @@ func (s *Stats) PostNews() {
func (s *Stats) GetNewsList() {
s.increment("get_news_list")
}

func (s *Stats) wrapTransaction(f func(tx *sql.Tx) error) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer func() { cancel() }()
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Printf("error starting transaction: %s", err)
return err
}
for {
err = f(tx)
if err != nil {
serr, ok := err.(*sqlite.Error)
if ok && serr.Code() == sqlitelib.SQLITE_BUSY {
continue
}
log.Printf("error in transaction: %s", err)
return err
}
err = tx.Commit()
if err != nil {
log.Printf("error committing transaction: %s", err)
return err
}
break
}
return nil
}

0 comments on commit 04c5d48

Please sign in to comment.