
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).
149 lines
3.8 KiB
Go
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
|
|
}
|