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:
@@ -264,8 +264,14 @@ func openEmbeddedConnection(ctx context.Context, cfg *Config) (*sql.DB, string,
|
|||||||
|
|
||||||
// Disable statistics collection to avoid stats subdatabase lock issues
|
// Disable statistics collection to avoid stats subdatabase lock issues
|
||||||
// The stats database can cause "cannot update manifest: database is read only"
|
// 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_enabled = 0")
|
||||||
|
_, _ = db.ExecContext(ctx, "SET @@dolt_stats_auto_refresh_interval = 0")
|
||||||
|
_, _ = db.ExecContext(ctx, "CALL dolt_stats_stop()")
|
||||||
|
|
||||||
return db, connStr, nil
|
return db, connStr, nil
|
||||||
}
|
}
|
||||||
@@ -701,19 +707,35 @@ func isLockError(err error) bool {
|
|||||||
// cleanupStaleDoltLock removes stale LOCK files from the Dolt noms directory.
|
// cleanupStaleDoltLock removes stale LOCK files from the Dolt noms directory.
|
||||||
// The embedded Dolt driver creates a LOCK file that persists after crashes,
|
// The embedded Dolt driver creates a LOCK file that persists after crashes,
|
||||||
// causing subsequent opens to fail with "database is read only" errors.
|
// 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 {
|
func cleanupStaleDoltLock(dbPath string, database string) error {
|
||||||
// The LOCK file is in the noms directory under .dolt
|
// Clean up main database LOCK and stats subdatabase LOCK
|
||||||
// For a database at /path/to/dolt with database name "beads",
|
// The stats subdatabase at .dolt/stats/.dolt/noms/LOCK can cause
|
||||||
// the lock is at /path/to/dolt/beads/.dolt/noms/LOCK
|
// "cannot update manifest: database is read only" errors
|
||||||
lockPath := filepath.Join(dbPath, database, ".dolt", "noms", "LOCK")
|
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)
|
info, err := os.Stat(lockPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// No lock file, nothing to do
|
// No lock file, nothing to do
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != 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)
|
// 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
|
// it's likely stale from a crashed process
|
||||||
age := time.Since(info.ModTime())
|
age := time.Since(info.ModTime())
|
||||||
if age > 5*time.Second {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user