Skip to content

Commit

Permalink
Use immediate rather than deferred transaction in sqlitemigration ens…
Browse files Browse the repository at this point in the history
…ureAppID (#98)

Co-authored-by: Ross Light <ross@zombiezen.com>
  • Loading branch information
bradenrayhorn and zombiezen committed May 4, 2024
1 parent c60a6c8 commit 59bb121
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Address low-frequency errors with concurrent use of `sqlitemigration`
([#99](https://github.com/zombiezen/go-sqlite/issues/99)).
- The error returned from `sqlitex.NewPool`
when trying to open an in-memory database
now gives correct advice
Expand Down
14 changes: 13 additions & 1 deletion sqlitemigration/sqlitemigration.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,19 @@ func rollback(conn *sqlite.Conn) {
}

func ensureAppID(conn *sqlite.Conn, wantAppID int32) (schemaVersion int32, err error) {
defer sqlitex.Save(conn)(&err)
// This transaction will later be upgraded to a write transaction. If at the point of upgrading
// to a write transaction, the database is locked, SQLite will fail immediately with
// SQLITE_BUSY and the busy timeout will have no effect, causing the pool to fail.
//
// If we use an immediate transaction, telling SQLite this is a write transaction, SQLite
// will attempt to lock the database immediately. If a lock cannot be acquired, the busy
// timeout is used allowing the transaction to wait until it can get a lock, thus allowing the
// pool to start successfully.
end, err := sqlitex.ImmediateTransaction(conn)
if err != nil {
return 0, err
}
defer end(&err)

var hasSchema bool
err = sqlitex.ExecuteTransient(conn, "VALUES ((SELECT COUNT(*) FROM sqlite_master) > 0);", &sqlitex.ExecOptions{
Expand Down
58 changes: 58 additions & 0 deletions sqlitemigration/sqlitemigration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"path/filepath"
"sync"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -917,6 +918,63 @@ func TestMigrate(t *testing.T) {
t.Error("Foreign keys were disabled after migration")
}
})

t.Run("Concurrent", func(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "concurrent.db")
schema := Schema{
AppID: 0xedbeef,
Migrations: []string{
`create table foo ( id integer primary key not null );`,
},
}

// Attempt to perform migrations while writing.
var wg sync.WaitGroup
defer wg.Wait()
const numConcurrent = 5
wg.Add(numConcurrent)
for i := 0; i < numConcurrent; i++ {
go func(i int) {
defer wg.Done()

conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
if err != nil {
t.Error(err)
return
}
defer func() {
if err := conn.Close(); err != nil {
t.Error(err)
}
}()
if err := Migrate(ctx, conn, schema); err != nil {
t.Error("Migrate:", err)
}
}(i)
}

// Migrate and issue writes on one connection.
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := conn.Close(); err != nil {
t.Error(err)
}
}()
if err := Migrate(ctx, conn, schema); err != nil {
t.Fatal("Migrate:", err)
}
for i := 0; i < 150; i++ {
if err := sqlitex.Execute(conn, "insert into foo values (?)", &sqlitex.ExecOptions{
Args: []any{i},
}); err != nil {
t.Error("insert query:", err)
}
}
})
}

// withTestConn makes an independent connection to the given database.
Expand Down

0 comments on commit 59bb121

Please sign in to comment.