diff --git a/methods/EverCore/src/core/lifespan/milvus_lifespan.py b/methods/EverCore/src/core/lifespan/milvus_lifespan.py index 6d6f916d..034569cf 100644 --- a/methods/EverCore/src/core/lifespan/milvus_lifespan.py +++ b/methods/EverCore/src/core/lifespan/milvus_lifespan.py @@ -2,6 +2,7 @@ Milvus lifespan provider implementation """ +import os from collections import defaultdict from fastapi import FastAPI from typing import Any @@ -41,6 +42,18 @@ async def startup(self, app: FastAPI) -> Any: Returns: Any: Milvus client information """ + # Symmetric env-gate (mirrors QdrantLifespanProvider): when running with + # the Qdrant adapter as the active vector backend, the Milvus lifespan + # must be a no-op. Without this gate it always tries to connect to + # Milvus and crashes the service when Milvus is offline (e.g. after + # cutover or during outage), preventing the Qdrant adapter — which + # runs at order=19, AFTER milvus order=18 — from ever starting. + if os.getenv("VECTOR_STORE_BACKEND", "milvus").strip().lower() == "qdrant": + logger.info( + "VECTOR_STORE_BACKEND=qdrant — Milvus lifespan startup no-op" + ) + return None + logger.info("Initializing Milvus connection...") try: diff --git a/methods/EverCore/src/core/oxm/milvus/base_repository.py b/methods/EverCore/src/core/oxm/milvus/base_repository.py index cfa49e67..025e866f 100644 --- a/methods/EverCore/src/core/oxm/milvus/base_repository.py +++ b/methods/EverCore/src/core/oxm/milvus/base_repository.py @@ -40,10 +40,23 @@ def __init__(self, model: Type[T]): """ self.model = model self.model_name = model.__name__ - self.collection: Optional[AsyncCollection] = model.async_collection() + # Lazy collection resolution: the underlying Milvus collection is only + # available once `MilvusLifespanProvider` has run `ensure_loaded()`. + # When VECTOR_STORE_BACKEND=qdrant the Milvus lifespan is a no-op, so + # eager resolution here would crash the entire app startup at every + # repository `__init__`. We defer to first access of `self.collection` + # so the app boots and qdrant-mode never touches the Milvus pathway. + self._collection_cache: Optional[AsyncCollection] = None self.schema = model._SCHEMA self.all_output_fields = [field.name for field in self.schema.fields] + @property + def collection(self) -> AsyncCollection: + """Lazy accessor for the underlying async Milvus collection.""" + if self._collection_cache is None: + self._collection_cache = self.model.async_collection() + return self._collection_cache + # ==================== Basic CRUD Operations ==================== async def insert(self, entity: T, flush: bool = False) -> str: diff --git a/methods/EverCore/src/core/tenants/tenantize/oxm/qdrant/config_utils.py b/methods/EverCore/src/core/tenants/tenantize/oxm/qdrant/config_utils.py index 4e857b57..2f806c9f 100644 --- a/methods/EverCore/src/core/tenants/tenantize/oxm/qdrant/config_utils.py +++ b/methods/EverCore/src/core/tenants/tenantize/oxm/qdrant/config_utils.py @@ -35,7 +35,7 @@ def get_tenant_qdrant_config() -> Optional[Dict[str, Any]]: oder ``None`` falls kein Tenant aktiv. """ # Lazy import vermeidet Circular-Dependency bei Adapter-Discovery-Time. - from core.tenants.tenantize.tenant_context import get_current_tenant + from core.tenants.tenant_contextvar import get_current_tenant # Fail-closed: an unexpected error during tenant resolution must not # degrade silently to the shared base-prefix path — that would route a