{"record":{"id":"e6d2c2e4e3e428e9","repo":"JuliusBrussee/caveman","slug":"ccr-recovery-store-is-closed","errorCode":null,"errorMessage":"ccr: recovery store is closed","messagePattern":"ccr: recovery store is closed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/store_generation.go","lineNumber":141,"sourceCode":"// before the first complete generation snapshot. Never close an opened disk\n// connection on that error path: SQLite close can checkpoint its stale WAL into\n// the current main file. The process must exit to release those descriptors.\nfunc closeSQLiteAfterOpenFailure(db *sql.DB, path string) {\n\tif path == \":memory:\" || db.Stats().OpenConnections == 0 {\n\t\t_ = db.Close()\n\t}\n}\n\n// checkGeneration runs with mu held. A detected generation change is terminal\n// for this Store. A fresh process must open the restored/current database;\n// pathname snapshots cannot establish which files a replacement connection\n// actually opened, so automatically adopting a replacement is unsafe.\nfunc (s *Store) checkGeneration() error {\n\tif s.quarantined != nil {\n\t\treturn s.quarantined\n\t}\n\tif s.closed {\n\t\treturn errors.New(\"ccr: recovery store is closed\")\n\t}\n\tfiles, err := inspectSQLiteGeneration(s.path)\n\tif errors.Is(err, errStorageUnverifiable) {\n\t\t// \"Could not look\" is not \"was replaced\". A momentary stat/permission\n\t\t// failure — an antivirus lock on Windows, EIO on a network home, a\n\t\t// parent directory whose mode was loose for one instant — fails THIS\n\t\t// operation closed, but must not latch the terminal state below and\n\t\t// make already-stored recoveries unreadable for the rest of the process.\n\t\treturn err\n\t}\n\tif err != nil {\n\t\ts.quarantined = err\n\t\treturn s.quarantined\n\t}\n\tif s.db != nil && s.files.same(files) {\n\t\treturn nil\n\t}\n\t// No SQLite calls, including Close, follow invalidation. The public driver","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/store_generation.go#L123-L159","documentation":"The CCR recovery store refuses all operations after Close() has been called. checkGeneration runs on every withStore/Close path and short-circuits with this sentinel when s.closed is set, preventing use of a store whose recovery files may already have been released.","triggerScenarios":"Any operation routed through withStore (reads/writes to the recovery store) or Close itself, executed after a prior call to Store.Close on the same *ccr.Store instance.","commonSituations":"Calling Close in a defer and then still using the store later in the function; a long-lived process that closed the store during shutdown but a lingering goroutine still writes decisions; double-Close; tests reusing a closed fixture store.","solutions":["Ensure Close is the last operation: reorder code so no withStore call happens after Close.","Audit goroutines holding a reference to the store and shut them down before Close (waitgroup/context cancellation).","If the store must be reopened, construct a new Store instead of reusing the closed instance.","Guard call sites with a closed check or run operations through a single owner that serializes Close."],"exampleFix":"// before\nstore.Close()\nrecord, err := store.WithStore(func(...) {...}) // panics-free but errors\n// after\nrecord, err := store.WithStore(func(...) {...})\nif err != nil { ... }\nstore.Close()","handlingStrategy":"try-catch","validationCode":"// track liveness yourself\nif store.IsClosed() { return errors.New(\"store already closed\") }","typeGuard":null,"tryCatchPattern":"rec, err := store.WithStore(fn)\nif err != nil {\n    if strings.Contains(err.Error(), \"store is closed\") {\n        // reopen or abort shutdown path\n    }\n    return err\n}","preventionTips":["Make Close the terminal operation in a defer placed last","Cancel worker goroutines before Close (context + WaitGroup)","Never reuse a Store after Close; construct a new one"],"tags":["sqlite","lifecycle","closed-resource"],"backgroundTag":"invalid-state-transition","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}