{"record":{"id":"021860a1f8df1ad4","repo":"MemPalace/mempalace","slug":"sqliteexactcollection-has-been-closed","errorCode":null,"errorMessage":"SQLiteExactCollection has been closed","messagePattern":"SQLiteExactCollection has been closed","errorType":"exception","errorClass":"BackendClosedError","httpStatus":null,"severity":"error","filePath":"mempalace/backends/sqlite_exact.py","lineNumber":280,"sourceCode":"        self.lock = lock\n        self.palace_path = palace_path\n        self.read_only = read_only\n        # True when opened with ``immutable=1`` because no WAL existed at connect\n        # time. A later writer can create WAL sidecars that this connection will\n        # never see, so the backend must reopen once those files appear.\n        self.immutable = immutable\n        self.closed = False\n\n\nclass SQLiteExactCollection(BaseCollection):\n    def __init__(self, handle: _SQLiteExactHandle, collection_name: str):\n        self._handle = handle\n        self._collection_name = collection_name\n        self._closed = False\n\n    def _ensure_open(self) -> None:\n        if self._closed or self._handle.closed:\n            raise BackendClosedError(\"SQLiteExactCollection has been closed\")\n\n    @contextlib.contextmanager\n    def _write_lock(self):\n        \"\"\"Serialize this handle before taking process-wide writer ownership.\n\n        ``mine_palace_lock`` grants cross-thread re-entrant access whenever\n        this process already owns the palace. Taking it before ``handle.lock``\n        lets a waiting thread consume that re-entrant credit, outlive the\n        thread that owns the OS lease, and then mutate after the lease has been\n        released. The handle mutex must therefore be the outer context.\n        \"\"\"\n        # Late import avoids a palace.py -> backend -> palace.py cycle.\n        from ..palace import mine_palace_lock\n\n        with self._handle.lock:\n            self._ensure_open()\n            with mine_palace_lock(self._handle.palace_path):\n                yield","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/backends/sqlite_exact.py#L262-L298","documentation":"Raised by `SQLiteExactCollection._ensure_open`, which guards every collection operation. It fires when the collection object was explicitly closed, or when the underlying shared handle was closed (e.g. `backend.close()` or palace close reaped the connection out of the registry). This is a use-after-close guard: all later reads/writes on that collection object fail fast instead of touching a dead sqlite connection.","triggerScenarios":"Calling any method on a SQLiteExactCollection after `col.close()`; calling after `backend.close()` closed the shared handle; holding a collection reference across a palace repair/reopen cycle; using a collection in a background hook thread after the main thread shut the backend down.","commonSituations":"Long-lived module-level collection handles in a server or hook process that gets torn down; test fixtures that close the backend in teardown while an assertion afterwards still touches the collection; a shutdown race where a save hook runs during interpreter exit after close.","solutions":["Re-fetch the collection after reopening: `backend = SQLiteExactBackend(); col = backend.get_collection(...)` — do not cache collections across close cycles.","Scope collection usage inside a `with`/try block that owns the backend lifetime, and close only after all work (including background threads) is joined.","In multi-threaded code, signal background threads to stop before calling `backend.close()`.","Catch BackendClosedError at the top level of hooks to treat late work as a no-op and log it."],"exampleFix":"# before\nbackend.close()\ncol.count()  # BackendClosedError\n\n# after\nbackend.close()\nbackend = SQLiteExactBackend()\ncol = backend.get_collection(palace, \"drawers\")\ncol.count()","handlingStrategy":"try-catch","validationCode":"def collection_usable(col) -> bool:\n    return not getattr(col, \"_closed\", True) and not getattr(getattr(col, \"_handle\", None), \"closed\", True)","typeGuard":null,"tryCatchPattern":"from mempalace.backends.base import BackendClosedError\n\ntry:\n    col.count()\nexcept BackendClosedError:\n    backend = SQLiteExactBackend()\n    col = backend.get_collection(palace, name)  # reacquire after reopen\n    col.count()","preventionTips":["Scope collection usage to the backend's lifetime; close backends only after joining background threads.","Never cache collection objects across module reloads or shutdown hooks.","In hook scripts, wrap the whole memory operation and treat BackendClosedError as a clean no-op."],"tags":["sqlite-exact","lifecycle","use-after-close","backend"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}