Run database maintenance under the database lock (closes #94)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The database maintainer ran Vacuum and Analyze without the database lock every batch write holds, so on the live feed every incremental vacuum failed with "database is locked", and an hourly ANALYZE made a batch write fail and lose its updates. Vacuum, Analyze and Checkpoint now take that lock, so maintenance and batch writes wait for each other. Analyze runs PRAGMA optimize with an analysis limit, which analyzes only tables whose size changed a lot, so it holds the lock briefly however large the file. Model: opus-5-5
This commit was merged in pull request #97.
This commit is contained in:
@@ -68,6 +68,18 @@ const (
|
||||
sqliteHardHeapLimitBytes = 1610612736
|
||||
)
|
||||
|
||||
// Settings of the ANALYZE that Analyze runs.
|
||||
const (
|
||||
// analysisLimit is about how many rows of each index ANALYZE reads
|
||||
// (PRAGMA analysis_limit); SQLite estimates the rest.
|
||||
analysisLimit = 1000
|
||||
// optimizeAllTables is the PRAGMA optimize mask that runs ANALYZE (0x2)
|
||||
// on every table that has an index without statistics or whose size has
|
||||
// changed tenfold since it was last analyzed, not only on the tables this
|
||||
// connection has queried (0x10000).
|
||||
optimizeAllTables = 0x10002
|
||||
)
|
||||
|
||||
// Bounds on the work GetRandomPrefixesByLength does to pick its sample.
|
||||
const (
|
||||
// maxPrefixesToShuffle is the most prefixes of one mask length that are
|
||||
@@ -182,7 +194,6 @@ func (d *Database) Initialize() error {
|
||||
// enough for the whole pool.
|
||||
pragmas := []string{
|
||||
"PRAGMA journal_mode=WAL", // Write-Ahead Logging
|
||||
"PRAGMA analysis_limit=0", // Disable automatic ANALYZE
|
||||
fmt.Sprintf("PRAGMA soft_heap_limit=%d", sqliteSoftHeapLimitBytes),
|
||||
fmt.Sprintf("PRAGMA hard_heap_limit=%d", sqliteHardHeapLimitBytes),
|
||||
}
|
||||
@@ -2151,14 +2162,18 @@ func (d *Database) GetIPInfoContext(
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Vacuum runs incremental vacuum to reclaim unused pages without blocking
|
||||
// writes. It frees up to the specified number of pages per call (0 = all
|
||||
// freeable pages).
|
||||
// Vacuum runs incremental vacuum to return up to 1000 unused pages to the
|
||||
// filesystem. Like Analyze and Checkpoint, it holds the database lock, so it
|
||||
// and the batch writes wait for each other instead of failing on SQLite's
|
||||
// write lock.
|
||||
func (d *Database) Vacuum(ctx context.Context) error {
|
||||
// Free up to 1000 pages per call (~4MB with default 4KB page size)
|
||||
// This keeps each vacuum operation quick and non-blocking
|
||||
// This keeps each vacuum operation quick
|
||||
const pagesToFree = 1000
|
||||
|
||||
d.lock("Vacuum")
|
||||
defer d.unlock()
|
||||
|
||||
rows, err := d.db.QueryContext(ctx,
|
||||
fmt.Sprintf("PRAGMA incremental_vacuum(%d)", pagesToFree))
|
||||
if err != nil {
|
||||
@@ -2180,9 +2195,20 @@ func (d *Database) Vacuum(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Analyze runs the SQLite ANALYZE command to update query planner statistics.
|
||||
// Analyze updates the query planner statistics. It runs ANALYZE, through
|
||||
// PRAGMA optimize, only on the tables that have an index without statistics or
|
||||
// whose size has changed tenfold since they were last analyzed: even with
|
||||
// analysisLimit, ANALYZE counts every entry of each index, so it takes longer
|
||||
// as the tables grow. It holds the database lock.
|
||||
func (d *Database) Analyze(ctx context.Context) error {
|
||||
_, err := d.db.ExecContext(ctx, "ANALYZE")
|
||||
d.lock("Analyze")
|
||||
defer d.unlock()
|
||||
|
||||
// analysis_limit is a setting of the connection, so it is set in the same
|
||||
// call as PRAGMA optimize, which runs both statements on one connection.
|
||||
_, err := d.db.ExecContext(ctx, fmt.Sprintf(
|
||||
"PRAGMA analysis_limit=%d; PRAGMA optimize=%#x",
|
||||
analysisLimit, optimizeAllTables))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze database: %w", err)
|
||||
}
|
||||
@@ -2192,8 +2218,11 @@ func (d *Database) Analyze(ctx context.Context) error {
|
||||
|
||||
// Checkpoint runs a WAL checkpoint to transfer data from the WAL to the main database.
|
||||
// Uses TRUNCATE mode which blocks writers briefly but ensures complete checkpoint,
|
||||
// keeping the WAL small for fast read performance.
|
||||
// keeping the WAL small for fast read performance. It holds the database lock.
|
||||
func (d *Database) Checkpoint(ctx context.Context) error {
|
||||
d.lock("Checkpoint")
|
||||
defer d.unlock()
|
||||
|
||||
_, err := d.db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to checkpoint WAL: %w", err)
|
||||
@@ -2214,21 +2243,26 @@ func (d *Database) Ping(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The debug messages lock logs, with the operation's name, when it starts to
|
||||
// wait for the database mutex and when it holds it.
|
||||
const (
|
||||
lockWaitMessage = "Acquiring database lock"
|
||||
lockHeldMessage = "Database lock acquired"
|
||||
)
|
||||
|
||||
// lock acquires the database mutex and logs debug information
|
||||
func (d *Database) lock(operation string) {
|
||||
// Get caller information
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
caller := fmt.Sprintf("%s:%d", filepath.Base(file), line)
|
||||
|
||||
d.logger.Debug("Acquiring database lock",
|
||||
"operation", operation, "caller", caller)
|
||||
d.logger.Debug(lockWaitMessage, "operation", operation, "caller", caller)
|
||||
|
||||
d.mu.Lock()
|
||||
d.lockedAt = time.Now()
|
||||
d.lockedBy = fmt.Sprintf("%s (%s)", operation, caller)
|
||||
|
||||
d.logger.Debug("Database lock acquired",
|
||||
"operation", operation, "caller", caller)
|
||||
d.logger.Debug(lockHeldMessage, "operation", operation, "caller", caller)
|
||||
}
|
||||
|
||||
// unlock releases the database mutex and logs debug information including
|
||||
|
||||
Reference in New Issue
Block a user