
Instead of returning an error when getting the sqlc queries object, just panic. This'll make the calling code quite a bit simpler. The situation in which it might error out is so rare that I've never seen it, and I don't even know if it will ever be possible to happen with the SQLite implementation we use now. Furthermore, once we get rid of GORM, it should just always work anyway. Ref: #104305
183 lines
5.0 KiB
Go
183 lines
5.0 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 = 10 * time.Second
|
|
)
|
|
|
|
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):
|
|
case <-db.consistencyCheckRequests:
|
|
}
|
|
|
|
ok := db.performIntegrityCheck(ctx)
|
|
if !ok {
|
|
log.Error().Msg("database: periodic integrity check failed")
|
|
onErrorCallback()
|
|
}
|
|
}
|
|
}
|
|
|
|
// RequestIntegrityCheck triggers a check of the database persistency.
|
|
func (db *DB) RequestIntegrityCheck() {
|
|
select {
|
|
case db.consistencyCheckRequests <- struct{}{}:
|
|
// Don't do anything, the work is done.
|
|
default:
|
|
log.Debug().Msg("database: could not trigger integrity check, another check might already be queued.")
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
log.Debug().Msg("database: performing integrity check")
|
|
|
|
db.ensureForeignKeysEnabled()
|
|
|
|
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) {
|
|
queries := db.queries()
|
|
issues, err := queries.PragmaIntegrityCheck(ctx)
|
|
if err != nil {
|
|
log.Error().Err(err).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
|
|
}
|
|
|
|
// ensureForeignKeysEnabled checks whether foreign keys are enabled, and if not,
|
|
// tries to enable them.
|
|
//
|
|
// This is likely caused by either GORM or its embedded SQLite creating a new
|
|
// connection to the low-level SQLite driver. Unfortunately the GORM-embedded
|
|
// SQLite doesn't have an 'on-connect' callback function to always enable
|
|
// foreign keys.
|
|
func (db *DB) ensureForeignKeysEnabled() {
|
|
fkEnabled, err := db.areForeignKeysEnabled()
|
|
|
|
if err != nil {
|
|
log.Error().AnErr("cause", err).Msg("database: could not check whether foreign keys are enabled")
|
|
return
|
|
}
|
|
|
|
if fkEnabled {
|
|
return
|
|
}
|
|
|
|
log.Warn().Msg("database: foreign keys are disabled, re-enabling them")
|
|
if err := db.pragmaForeignKeys(true); err != nil {
|
|
log.Error().AnErr("cause", err).Msg("database: error re-enabling foreign keys")
|
|
return
|
|
}
|
|
}
|