MemPalace/mempalace · error · BackendClosedError

QdrantCollection has been closed

Error message

QdrantCollection has been closed

What it means

Raised by QdrantCollection._ensure_open() when an operation is attempted on a collection handle after close() was called on it (or on its parent QdrantBackend). It is a BackendClosedError (BackendError subclass) and marks a use-after-close programming error, not an environmental issue.

Source

Thrown at mempalace/backends/qdrant.py:704

        client: _QdrantRESTClient,
        config: _QdrantConfig,
        palace: PalaceRef,
        collection_name: str,
        remote_collection: str,
    ):
        self._backend = backend
        self._client = client
        self._config = config
        self._palace = palace
        self._collection_name = collection_name
        self._remote_collection = remote_collection
        self._lock = threading.RLock()
        self._closed = False
        self._known_dimension: Optional[int] = None

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

    def _remote_exists(self) -> bool:
        return self._client.collection_exists(self._remote_collection)

    def _marker_exists(self) -> bool:
        return self._backend._marker_exists(self._palace)

    def get_stored_embedder_identity(self):
        return self._backend._get_embedder_identity(self._palace, self._collection_name)

    def set_embedder_identity(self, identity) -> None:
        # Sidecar-backed (see QdrantBackend), so this records even on a
        # brand-new palace whose mismatch marker doesn't exist yet.
        self._backend._set_embedder_identity(self._palace, self._collection_name, identity)

    def _remote_dimension(self) -> Optional[int]:
        try:
            info = self._client.get_collection_info(self._remote_collection)

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Re-open the backend and re-fetch the collection via palace.get_collection() after close
  2. Audit lifecycle: ensure close() only runs when no concurrent work holds the handle
  3. In long-running apps, create the backend per request/job or use a manager that re-opens on demand
  4. In tests, use fresh fixtures per test instead of sharing a closable handle

Example fix

# before
 collection = palace.get_collection("notes")
 backend.close()
 collection.get(ids=["a"])  # BackendClosedError
# after
 backend.close()
 backend = qdrant_backend reopen (QdrantBackend(...))
 collection = palace.get_collection("notes")
 collection.get(ids=["a"])
Defensive patterns

Strategy: type-guard

Type guard

def collection_usable(collection) -> bool:
    try:
        collection._ensure_open()
        return True
    except Exception:
        return False

Try / catch

from mempalace.backends.base import BackendClosedError
try:
    collection.get(ids=ids)
except BackendClosedError:
    backend = reopen_backend()
    collection = palace.get_collection(name)

Prevention

When it happens

Trigger: Calling add/upsert/get/query/delete/facet_counts etc. on a collection handle obtained before backend.close() or collection.close(); a long-lived cached handle in a web app whose startup code closes the backend during shutdown while a request is still in flight.

Common situations: Module-level collection handle used after an atexit or context-manager exit closed the backend; test fixture closing the backend between tests while a helper still holds the old handle; background thread racing with shutdown.

Related errors


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