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 <noreply@anthropic.com>

Executed-By: mayor
Role: mayor
This commit is contained in:
mayor
2026-01-25 17:00:58 -08:00
committed by gastown/crew/joe
parent 27d6a8c2da
commit 9f0a85d111

View File

@@ -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
}