JuliusBrussee/caveman · error
file changed while securing
Error message
file changed while securing
What it means
After chmodding, secureSQLiteFile re-stats the file and requires os.SameFile(info, secured) to hold, i.e. the same inode/device as when it was inspected. This error means the file at the path was replaced (different inode) between the initial Lstat and the post-chmod Stat, so the permission change may have been applied to a different inode than the one that will be used.
Solutions
- Ensure only one process opens the CCR store at a time; serialize startup or use a lock file
- Retry the Open once the conflicting writer finishes
- Stop sync/backup jobs from touching the store directory while the engine starts
- If the error is transient and rare, wrap Open in a short retry loop
Example fix
// before
store, err := ccr.Open(dbPath) // races with backup restore
// after
for i := 0; i < 3; i++ {
store, err = ccr.Open(dbPath)
if err == nil || !strings.Contains(fmt.Sprint(err), "file changed while securing") { break }
time.Sleep(200 * time.Millisecond)
} Defensive patterns
Strategy: retry
Validate before calling
// best-effort pre-check: ensure no other process holds the db
if f, err := os.OpenFile(dbPath, os.O_EXCL|os.O_CREATE, 0o600); err == nil {
defer os.Remove(dbPath + ".lock") // external lock convention
f.Close()
} Try / catch
var store *ccr.Store
var err error
for i := 0; i < 3; i++ {
store, err = ccr.Open(dbPath)
if err == nil || !strings.Contains(err.Error(), "file changed while securing") { break }
time.Sleep(250 * time.Millisecond)
} Prevention
- Only one process should open a given CCR store
- Pause backup/sync jobs during engine startup
- Use atomic-rename writers carefully around the db path
When it happens
Trigger: Two processes opening/securing the same CCR store concurrently while one recreates the db file; an external script (vacuum, backup-restore, temp-file-rename) replacing the db file during startup; antivirus or sync tools swapping the file mid-operation.
Common situations: Multiple engine instances pointed at the same recovery.db started simultaneously; a deploy script restoring a db backup while the engine boots; Dropbox/rsync-style atomic replace (write temp + rename) colliding with startup.
Related errors
- file changed while opening
- file changed while securing
- budget is below CCR storage minimum
- cave_ccr_budget_exceeded
- ccr storage budget is below minimum
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c14fb24535b048fc.
Report an issue: GitHub.
Appendix: source
Thrown at engine/ccr/store_sqlite.go:371
}
err = chmodSQLiteFile(path, info)
if errors.Is(err, os.ErrNotExist) && !create {
// The sidecar vanished while securing it — a concurrent process
// checkpointed the WAL and removed it. Nothing left to secure.
return nil
}
if err != nil {
return err
}
secured, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) && !create {
return nil
}
if err != nil {
return err
}
if !os.SameFile(info, secured) {
return fmt.Errorf("file changed while securing")
}
return nil
}
func configureStorageBudget(db *sql.DB, maxBytes int64) error {
var pageSize int64
if err := db.QueryRow(`PRAGMA page_size`).Scan(&pageSize); err != nil {
return fmt.Errorf("read page size: %w", err)
}
if pageSize <= 0 {
return errors.New("invalid sqlite page size")
}
maxPages := maxBytes / pageSize
if maxPages < minimumStoragePages {
return fmt.Errorf("budget %d is below CCR storage minimum %d", maxBytes, minimumStoragePages*pageSize)
}
var applied int64
if err := db.QueryRow(fmt.Sprintf(`PRAGMA max_page_count=%d`, maxPages)).Scan(&applied); err != nil {View on GitHub (pinned to 3ee70a1026)