Skip to content

Commit bbe9d61

Browse files
committed
sqlite: move first read into a transaction
According to an old upstream issue [1]: "If the first statement after BEGIN DEFERRED is a SELECT, then a read transaction is started. Subsequent write statements will upgrade the transaction to a write transaction if possible, or return SQLITE_BUSY." So let's move the first SELECT under the same transaction as the table initialization. [NO NEW TESTS NEEDED] as it's a hard to cause race. [1] mattn/go-sqlite3#274 (comment) Fixes: containers#17859 Signed-off-by: Valentin Rothberg <[email protected]>
1 parent 1918924 commit bbe9d61

File tree

2 files changed

+53
-44
lines changed

2 files changed

+53
-44
lines changed

libpod/sqlite_state.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,11 @@ func NewSqliteState(runtime *Runtime) (_ State, defErr error) {
7878
}
7979
}()
8080

81-
state.conn = conn
82-
83-
// Migrate schema (if necessary)
84-
if err := state.migrateSchemaIfNecessary(); err != nil {
81+
if err := initSQLiteDB(conn); err != nil {
8582
return nil, err
8683
}
8784

88-
// Set up tables
89-
if err := sqliteInitTables(state.conn); err != nil {
90-
return nil, fmt.Errorf("creating tables: %w", err)
91-
}
92-
85+
state.conn = conn
9386
state.valid = true
9487
state.runtime = runtime
9588

libpod/sqlite_state_internal.go

+51-35
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,85 @@ import (
1515
_ "github.com/mattn/go-sqlite3"
1616
)
1717

18-
func (s *SQLiteState) migrateSchemaIfNecessary() (defErr error) {
18+
func initSQLiteDB(conn *sql.DB) (defErr error) {
19+
// Start with a transaction to avoid "database locked" errors.
20+
// See https://github.com/mattn/go-sqlite3/issues/274#issuecomment-1429054597
21+
tx, err := conn.Begin()
22+
if err != nil {
23+
return fmt.Errorf("beginning transaction: %w", err)
24+
}
25+
defer func() {
26+
if defErr != nil {
27+
if err := tx.Rollback(); err != nil {
28+
logrus.Errorf("Rolling back transaction to create tables: %v", err)
29+
}
30+
}
31+
}()
32+
33+
sameSchema, err := migrateSchemaIfNecessary(tx)
34+
if err != nil {
35+
return err
36+
}
37+
if !sameSchema {
38+
if err := createSQLiteTables(tx); err != nil {
39+
return err
40+
}
41+
}
42+
if err := tx.Commit(); err != nil {
43+
return fmt.Errorf("committing transaction: %w", err)
44+
}
45+
return nil
46+
}
47+
48+
func migrateSchemaIfNecessary(tx *sql.Tx) (bool, error) {
1949
// First, check if the DBConfig table exists
20-
checkRow := s.conn.QueryRow("SELECT 1 FROM sqlite_master WHERE type='table' AND name='DBConfig';")
50+
checkRow := tx.QueryRow("SELECT 1 FROM sqlite_master WHERE type='table' AND name='DBConfig';")
2151
var check int
2252
if err := checkRow.Scan(&check); err != nil {
2353
if errors.Is(err, sql.ErrNoRows) {
24-
return nil
54+
return false, nil
2555
}
26-
return fmt.Errorf("checking if DB config table exists: %w", err)
56+
return false, fmt.Errorf("checking if DB config table exists: %w", err)
2757
}
2858
if check != 1 {
2959
// Table does not exist, fresh database, no need to migrate.
30-
return nil
60+
return false, nil
3161
}
3262

33-
row := s.conn.QueryRow("SELECT SchemaVersion FROM DBConfig;")
63+
row := tx.QueryRow("SELECT SchemaVersion FROM DBConfig;")
3464
var schemaVer int
3565
if err := row.Scan(&schemaVer); err != nil {
3666
if errors.Is(err, sql.ErrNoRows) {
3767
// Brand-new, unpopulated DB.
3868
// Schema was just created, so it has to be the latest.
39-
return nil
69+
return false, nil
4070
}
41-
return fmt.Errorf("scanning schema version from DB config: %w", err)
71+
return false, fmt.Errorf("scanning schema version from DB config: %w", err)
4272
}
4373

4474
// If the schema version 0 or less, it's invalid
4575
if schemaVer <= 0 {
46-
return fmt.Errorf("database schema version %d is invalid: %w", schemaVer, define.ErrInternal)
76+
return false, fmt.Errorf("database schema version %d is invalid: %w", schemaVer, define.ErrInternal)
4777
}
4878

49-
if schemaVer != schemaVersion {
50-
// If the DB is a later schema than we support, we have to error
51-
if schemaVer > schemaVersion {
52-
return fmt.Errorf("database has schema version %d while this libpod version only supports version %d: %w",
53-
schemaVer, schemaVersion, define.ErrInternal)
54-
}
79+
// Same schema -> nothing do to.
80+
if schemaVer == schemaVersion {
81+
return true, nil
82+
}
5583

56-
// Perform schema migration here, one version at a time.
84+
// If the DB is a later schema than we support, we have to error
85+
if schemaVer > schemaVersion {
86+
return false, fmt.Errorf("database has schema version %d while this libpod version only supports version %d: %w",
87+
schemaVer, schemaVersion, define.ErrInternal)
5788
}
5889

59-
return nil
90+
// Perform schema migration here, one version at a time.
91+
92+
return false, nil
6093
}
6194

6295
// Initialize all required tables for the SQLite state
63-
func sqliteInitTables(conn *sql.DB) (defErr error) {
96+
func createSQLiteTables(tx *sql.Tx) error {
6497
// Technically we could split the "CREATE TABLE IF NOT EXISTS" and ");"
6598
// bits off each command and add them in the for loop where we actually
6699
// run the SQL, but that seems unnecessary.
@@ -186,28 +219,11 @@ func sqliteInitTables(conn *sql.DB) (defErr error) {
186219
"VolumeState": volumeState,
187220
}
188221

189-
tx, err := conn.Begin()
190-
if err != nil {
191-
return fmt.Errorf("beginning transaction: %w", err)
192-
}
193-
defer func() {
194-
if defErr != nil {
195-
if err := tx.Rollback(); err != nil {
196-
logrus.Errorf("Rolling back transaction to create tables: %v", err)
197-
}
198-
}
199-
}()
200-
201222
for tblName, cmd := range tables {
202223
if _, err := tx.Exec(cmd); err != nil {
203224
return fmt.Errorf("creating table %s: %w", tblName, err)
204225
}
205226
}
206-
207-
if err := tx.Commit(); err != nil {
208-
return fmt.Errorf("committing transaction: %w", err)
209-
}
210-
211227
return nil
212228
}
213229

0 commit comments

Comments
 (0)