Skip to content

Commit 59bb121

Browse files
Use immediate rather than deferred transaction in sqlitemigration ensureAppID (#98)
Co-authored-by: Ross Light <ross@zombiezen.com>
1 parent c60a6c8 commit 59bb121

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Fixed
1313

14+
- Address low-frequency errors with concurrent use of `sqlitemigration`
15+
([#99](https://github.com/zombiezen/go-sqlite/issues/99)).
1416
- The error returned from `sqlitex.NewPool`
1517
when trying to open an in-memory database
1618
now gives correct advice

sqlitemigration/sqlitemigration.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,19 @@ func rollback(conn *sqlite.Conn) {
353353
}
354354

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

358370
var hasSchema bool
359371
err = sqlitex.ExecuteTransient(conn, "VALUES ((SELECT COUNT(*) FROM sqlite_master) > 0);", &sqlitex.ExecOptions{

sqlitemigration/sqlitemigration_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"path/filepath"
10+
"sync"
1011
"testing"
1112

1213
"github.com/google/go-cmp/cmp"
@@ -917,6 +918,63 @@ func TestMigrate(t *testing.T) {
917918
t.Error("Foreign keys were disabled after migration")
918919
}
919920
})
921+
922+
t.Run("Concurrent", func(t *testing.T) {
923+
ctx := context.Background()
924+
dbPath := filepath.Join(t.TempDir(), "concurrent.db")
925+
schema := Schema{
926+
AppID: 0xedbeef,
927+
Migrations: []string{
928+
`create table foo ( id integer primary key not null );`,
929+
},
930+
}
931+
932+
// Attempt to perform migrations while writing.
933+
var wg sync.WaitGroup
934+
defer wg.Wait()
935+
const numConcurrent = 5
936+
wg.Add(numConcurrent)
937+
for i := 0; i < numConcurrent; i++ {
938+
go func(i int) {
939+
defer wg.Done()
940+
941+
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
942+
if err != nil {
943+
t.Error(err)
944+
return
945+
}
946+
defer func() {
947+
if err := conn.Close(); err != nil {
948+
t.Error(err)
949+
}
950+
}()
951+
if err := Migrate(ctx, conn, schema); err != nil {
952+
t.Error("Migrate:", err)
953+
}
954+
}(i)
955+
}
956+
957+
// Migrate and issue writes on one connection.
958+
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
959+
if err != nil {
960+
t.Fatal(err)
961+
}
962+
defer func() {
963+
if err := conn.Close(); err != nil {
964+
t.Error(err)
965+
}
966+
}()
967+
if err := Migrate(ctx, conn, schema); err != nil {
968+
t.Fatal("Migrate:", err)
969+
}
970+
for i := 0; i < 150; i++ {
971+
if err := sqlitex.Execute(conn, "insert into foo values (?)", &sqlitex.ExecOptions{
972+
Args: []any{i},
973+
}); err != nil {
974+
t.Error("insert query:", err)
975+
}
976+
}
977+
})
920978
}
921979

922980
// withTestConn makes an independent connection to the given database.

0 commit comments

Comments
 (0)