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:
parent
7f588e6dbc
commit
7a508c7e6b
@ -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")
|
||||||
|
114
internal/manager/persistence/integrity.go
Normal file
114
internal/manager/persistence/integrity.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user