MemPalace/mempalace · error · BackendClosedError

SQLiteExactCollection has been closed

Error message

SQLiteExactCollection has been closed

What it means

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.

Source

Thrown at mempalace/backends/sqlite_exact.py:280

        self.lock = lock
        self.palace_path = palace_path
        self.read_only = read_only
        # True when opened with ``immutable=1`` because no WAL existed at connect
        # time. A later writer can create WAL sidecars that this connection will
        # never see, so the backend must reopen once those files appear.
        self.immutable = immutable
        self.closed = False


class SQLiteExactCollection(BaseCollection):
    def __init__(self, handle: _SQLiteExactHandle, collection_name: str):
        self._handle = handle
        self._collection_name = collection_name
        self._closed = False

    def _ensure_open(self) -> None:
        if self._closed or self._handle.closed:
            raise BackendClosedError("SQLiteExactCollection has been closed")

    @contextlib.contextmanager
    def _write_lock(self):
        """Serialize this handle before taking process-wide writer ownership.

        ``mine_palace_lock`` grants cross-thread re-entrant access whenever
        this process already owns the palace. Taking it before ``handle.lock``
        lets a waiting thread consume that re-entrant credit, outlive the
        thread that owns the OS lease, and then mutate after the lease has been
        released. The handle mutex must therefore be the outer context.
        """
        # Late import avoids a palace.py -> backend -> palace.py cycle.
        from ..palace import mine_palace_lock

        with self._handle.lock:
            self._ensure_open()
            with mine_palace_lock(self._handle.palace_path):
                yield

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Re-fetch the collection after reopening: `backend = SQLiteExactBackend(); col = backend.get_collection(...)` — do not cache collections across close cycles.
  2. Scope collection usage inside a `with`/try block that owns the backend lifetime, and close only after all work (including background threads) is joined.
  3. In multi-threaded code, signal background threads to stop before calling `backend.close()`.
  4. Catch BackendClosedError at the top level of hooks to treat late work as a no-op and log it.

Example fix

# before
backend.close()
col.count()  # BackendClosedError

# after
backend.close()
backend = SQLiteExactBackend()
col = backend.get_collection(palace, "drawers")
col.count()
Defensive patterns

Strategy: try-catch

Validate before calling

def collection_usable(col) -> bool:
    return not getattr(col, "_closed", True) and not getattr(getattr(col, "_handle", None), "closed", True)

Try / catch

from mempalace.backends.base import BackendClosedError

try:
    col.count()
except BackendClosedError:
    backend = SQLiteExactBackend()
    col = backend.get_collection(palace, name)  # reacquire after reopen
    col.count()

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/021860a1f8df1ad4. Report an issue: GitHub.