Sybren A. Stüvel 63634361ce Manager: make periodic database integrity check configurable
Instead of always performing the periodic integrity check, make it possible
to disable it or run it at different intervals.

Currently for the Blender Studio it's crunch time, so the check should
really only run when there is someone looking at the system (i.e. at
restarts for upgrade purposes).
2023-07-18 16:33:01 +02:00

149 lines
3.8 KiB
Go

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"`
}
// PeriodicIntegrityCheck periodically checks the database integrity.
// This function only returns when the context is done.
func (db *DB) PeriodicIntegrityCheck(
ctx context.Context,
period time.Duration,
onErrorCallback func(),
) {
if period == 0 {
log.Info().Msg("database: periodic integrity check disabled")
return
}
log.Info().
Stringer("period", period).
Msg("database: periodic integrity check starting")
defer log.Debug().Msg("database: periodic integrity check stopping")
for {
select {
case <-ctx.Done():
return
case <-time.After(period):
}
ok := db.performIntegrityCheck(ctx)
if !ok {
log.Error().Msg("database: periodic integrity check failed")
onErrorCallback()
}
}
}
// 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
}