Manager: perform database integrity check at startup

Perform these two SQL calls & check their result:

- `PRAGMA integrity_check`
- `PRAGMA foreign_key_check`:

See  https: //www.sqlite.org/pragma.html for more info on these.

This also removes the unused `PeriodicMaintenanceLoop()` function.
Periodic checking while Flamenco Manager is running might be introduced
in a future commit, after the startup-time checks have been shown to not
get in the way.
This commit is contained in:
Sybren A. Stüvel 2023-07-07 15:58:57 +02:00
parent 7f588e6dbc
commit 7a508c7e6b
2 changed files with 119 additions and 22 deletions

View File

@ -15,7 +15,7 @@ import (
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
) )
const vacuumPeriod = 1 * time.Hour const checkPeriod = 1 * time.Hour
// DB provides the database interface. // DB provides the database interface.
type DB struct { type DB struct {
@ -57,6 +57,10 @@ func OpenDB(ctx context.Context, dsn string) (*DB, error) {
} }
// Perfom some maintenance at startup. // Perfom some maintenance at startup.
if !db.performIntegrityCheck(ctx) {
return nil, ErrIntegrity
}
db.vacuum() db.vacuum()
if err := db.migrate(); err != nil { if err := db.migrate(); err != nil {
@ -147,27 +151,6 @@ func nowFunc() time.Time {
return time.Now().UTC() return time.Now().UTC()
} }
// PeriodicMaintenanceLoop periodically vacuums the database.
// This function only returns when the context is done.
func (db *DB) PeriodicMaintenanceLoop(ctx context.Context) {
log.Debug().Msg("periodic database maintenance loop starting")
defer log.Debug().Msg("periodic database maintenance loop stopping")
var waitTime time.Duration
for {
select {
case <-ctx.Done():
return
case <-time.After(waitTime):
waitTime = vacuumPeriod
}
log.Debug().Msg("vacuuming database")
db.vacuum()
}
}
// vacuum executes the SQL "VACUUM" command, and logs any errors. // vacuum executes the SQL "VACUUM" command, and logs any errors.
func (db *DB) vacuum() { func (db *DB) vacuum() {
tx := db.gormDB.Exec("vacuum") tx := db.gormDB.Exec("vacuum")

View File

@ -0,0 +1,114 @@
package persistence
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"context"
"errors"
"time"
"github.com/rs/zerolog/log"
)
var ErrIntegrity = errors.New("database integrity check failed")
const integrityCheckTimeout = 2 * time.Second
type PragmaIntegrityCheckResult struct {
Description string `gorm:"column:integrity_check"`
}
type PragmaForeignKeyCheckResult struct {
Table string `gorm:"column:table"`
RowID int `gorm:"column:rowid"`
Parent string `gorm:"column:parent"`
FKID int `gorm:"column:fkid"`
}
// performIntegrityCheck uses a few 'pragma' SQL statements to do some integrity checking.
// Returns true on OK, false if there was an issue. Issues are always logged.
func (db *DB) performIntegrityCheck(ctx context.Context) (ok bool) {
checkCtx, cancel := context.WithTimeout(ctx, integrityCheckTimeout)
defer cancel()
if !db.pragmaIntegrityCheck(checkCtx) {
return false
}
return db.pragmaForeignKeyCheck(checkCtx)
}
// pragmaIntegrityCheck checks database file integrity. This does not include
// foreign key checks.
//
// Returns true on OK, false if there was an issue. Issues are always logged.
//
// See https: //www.sqlite.org/pragma.html#pragma_integrity_check
func (db *DB) pragmaIntegrityCheck(ctx context.Context) (ok bool) {
var issues []PragmaIntegrityCheckResult
tx := db.gormDB.WithContext(ctx).
Raw("PRAGMA integrity_check").
Scan(&issues)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("database error checking integrity")
return false
}
switch len(issues) {
case 0:
log.Warn().Msg("database integrity check returned nothing, expected explicit 'ok'; treating as an implicit 'ok'")
return true
case 1:
if issues[0].Description == "ok" {
log.Debug().Msg("database integrity check ok")
return true
}
}
log.Error().Int("num_issues", len(issues)).Msg("database integrity check failed")
for _, issue := range issues {
log.Error().
Str("description", issue.Description).
Msg("database integrity check failure")
}
return false
}
// pragmaForeignKeyCheck checks whether all foreign key constraints are still valid.
//
// SQLite has optional foreign key relations, so even though Flamenco Manager
// always enables these on startup, at some point there could be some issue
// causing these checks to be skipped.
//
// Returns true on OK, false if there was an issue. Issues are always logged.
//
// See https: //www.sqlite.org/pragma.html#pragma_foreign_key_check
func (db *DB) pragmaForeignKeyCheck(ctx context.Context) (ok bool) {
var issues []PragmaForeignKeyCheckResult
tx := db.gormDB.WithContext(ctx).
Raw("PRAGMA foreign_key_check").
Scan(&issues)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("database error checking foreign keys")
return false
}
if len(issues) == 0 {
log.Debug().Msg("database foreign key check ok")
return true
}
log.Error().Int("num_issues", len(issues)).Msg("database foreign key check failed")
for _, issue := range issues {
log.Error().
Str("table", issue.Table).
Int("rowid", issue.RowID).
Str("parent", issue.Parent).
Int("fkid", issue.FKID).
Msg("database foreign key relation missing")
}
return false
}