From 9f0a85d11187bce06fe2d9c7fcf82393ee651e0c Mon Sep 17 00:00:00 2001 From: mayor Date: Sun, 25 Jan 2026 17:00:58 -0800 Subject: [PATCH] fix(dolt): clean up stats subdatabase LOCK files to prevent read-only errors The Dolt stats subdatabase at .dolt/stats/.dolt/noms/LOCK was causing "cannot update manifest: database is read only" errors after crashes. Changes: - cleanupStaleDoltLock now also cleans stats and oldgen subdatabase LOCKs - Add dolt_stats_stop() call to fully stop stats background worker - Set dolt_stats_auto_refresh_interval=0 to prevent stats restart Fixes bd-dolt.1 Co-Authored-By: Claude Opus 4.5 Executed-By: mayor Role: mayor --- internal/storage/dolt/store.go | 43 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/internal/storage/dolt/store.go b/internal/storage/dolt/store.go index 100769d4..5b72ef4a 100644 --- a/internal/storage/dolt/store.go +++ b/internal/storage/dolt/store.go @@ -264,8 +264,14 @@ func openEmbeddedConnection(ctx context.Context, cfg *Config) (*sql.DB, string, // Disable statistics collection to avoid stats subdatabase lock issues // The stats database can cause "cannot update manifest: database is read only" - // errors when multiple processes access the embedded Dolt database + // errors when multiple processes access the embedded Dolt database. + // We disable stats via multiple methods to ensure it's fully stopped: + // 1. Set the enabled flag to 0 + // 2. Call dolt_stats_stop() to stop the background stats worker + // 3. Set auto-refresh interval to 0 to prevent automatic restarts _, _ = db.ExecContext(ctx, "SET @@dolt_stats_enabled = 0") + _, _ = db.ExecContext(ctx, "SET @@dolt_stats_auto_refresh_interval = 0") + _, _ = db.ExecContext(ctx, "CALL dolt_stats_stop()") return db, connStr, nil } @@ -701,19 +707,35 @@ func isLockError(err error) bool { // cleanupStaleDoltLock removes stale LOCK files from the Dolt noms directory. // The embedded Dolt driver creates a LOCK file that persists after crashes, // causing subsequent opens to fail with "database is read only" errors. +// This also cleans up the stats subdatabase LOCK which causes similar issues. func cleanupStaleDoltLock(dbPath string, database string) error { - // The LOCK file is in the noms directory under .dolt - // For a database at /path/to/dolt with database name "beads", - // the lock is at /path/to/dolt/beads/.dolt/noms/LOCK - lockPath := filepath.Join(dbPath, database, ".dolt", "noms", "LOCK") + // Clean up main database LOCK and stats subdatabase LOCK + // The stats subdatabase at .dolt/stats/.dolt/noms/LOCK can cause + // "cannot update manifest: database is read only" errors + lockPaths := []string{ + filepath.Join(dbPath, database, ".dolt", "noms", "LOCK"), + filepath.Join(dbPath, database, ".dolt", "stats", ".dolt", "noms", "LOCK"), + filepath.Join(dbPath, database, ".dolt", "noms", "oldgen", "LOCK"), + } + for _, lockPath := range lockPaths { + if err := cleanupSingleLock(lockPath); err != nil { + return err + } + } + + return nil +} + +// cleanupSingleLock removes a single stale LOCK file if it appears to be orphaned +func cleanupSingleLock(lockPath string) error { info, err := os.Stat(lockPath) if os.IsNotExist(err) { // No lock file, nothing to do return nil } if err != nil { - return fmt.Errorf("stat lock file: %w", err) + return fmt.Errorf("stat lock file %s: %w", lockPath, err) } // Check if lock file is empty (Dolt creates empty LOCK files) @@ -723,16 +745,13 @@ func cleanupStaleDoltLock(dbPath string, database string) error { // it's likely stale from a crashed process age := time.Since(info.ModTime()) if age > 5*time.Second { - fmt.Fprintf(os.Stderr, "Removing stale Dolt LOCK file (age: %v)\n", age.Round(time.Second)) + fmt.Fprintf(os.Stderr, "Removing stale Dolt LOCK file: %s (age: %v)\n", lockPath, age.Round(time.Second)) if err := os.Remove(lockPath); err != nil { - return fmt.Errorf("remove stale lock: %w", err) + return fmt.Errorf("remove stale lock %s: %w", lockPath, err) } - return nil } - // Lock is recent, might be held by another process - return nil } - // Non-empty lock file - might contain PID info, don't touch it + // Non-empty or recent lock file - don't touch it return nil }