Spaces:
Sleeping
Sleeping
| """Cliente asíncrono de MongoDB y gestión de la conexión. | |
| Se utiliza el driver oficial ``pymongo`` con su API asíncrona | |
| (:class:`pymongo.AsyncMongoClient`), disponible de forma estable a | |
| partir de PyMongo 4.13. | |
| El cliente se crea de forma perezosa: la conexión real se abre en la | |
| primera operación, por lo que la aplicación puede arrancar aunque la | |
| base de datos no esté disponible todavía (no se bloquea el arranque). | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import socket | |
| from pymongo import AsyncMongoClient | |
| from pymongo.asynchronous.collection import AsyncCollection | |
| from pymongo.asynchronous.database import AsyncDatabase | |
| from app.core.config import settings | |
| logger = logging.getLogger(__name__) | |
| class MongoDB: | |
| """Contenedor del cliente y la base de datos de MongoDB. | |
| Mantiene una única instancia del cliente durante el ciclo de vida de | |
| la aplicación. | |
| """ | |
| client: AsyncMongoClient | None = None | |
| database: AsyncDatabase | None = None | |
| async def connect(self) -> None: | |
| """Crea el cliente de MongoDB sin bloquear el arranque. | |
| No se hace ``ping`` aquí a propósito: el cliente es perezoso y la | |
| conexión se abre en la primera operación. Así la app arranca aunque | |
| Mongo no responda todavía. | |
| """ | |
| if self.client is not None: | |
| return | |
| self.client = AsyncMongoClient( | |
| settings.MONGODB_URI, | |
| tz_aware=True, | |
| serverSelectionTimeoutMS=settings.MONGODB_TIMEOUT_MS, | |
| ) | |
| self.database = self.client[settings.MONGODB_DB_NAME] | |
| async def ping(self) -> bool: | |
| """Comprueba si MongoDB responde. No lanza excepción.""" | |
| if self.client is None: | |
| return False | |
| try: | |
| await self.client.admin.command("ping") | |
| except Exception as exc: # noqa: BLE001 | |
| logger.warning("MongoDB no responde: %s", exc) | |
| self._diagnostico_red() | |
| return False | |
| logger.info("MongoDB conectado (db=%s).", settings.MONGODB_DB_NAME) | |
| return True | |
| def _diagnostico_red(self) -> None: | |
| """Registra un diagnóstico de red hacia los nodos de MongoDB. | |
| Resuelve el DNS y prueba un socket TCP crudo a cada nodo para | |
| distinguir un bloqueo de red de un problema del driver/credenciales. | |
| """ | |
| try: | |
| nodos = self.client.topology_description.server_descriptions() | |
| except Exception: # noqa: BLE001 | |
| return | |
| for host, port in nodos: | |
| try: | |
| ip = socket.gethostbyname(host) | |
| except OSError as exc: | |
| logger.warning("DIAG DNS %s -> FALLA (%s)", host, exc) | |
| continue | |
| sock = socket.socket() | |
| sock.settimeout(5) | |
| try: | |
| sock.connect((host, port)) | |
| logger.warning("DIAG TCP %s:%s (%s) -> OK", host, port, ip) | |
| except OSError as exc: | |
| logger.warning( | |
| "DIAG TCP %s:%s (%s) -> BLOQUEADO/timeout (%s)", | |
| host, | |
| port, | |
| ip, | |
| type(exc).__name__, | |
| ) | |
| finally: | |
| sock.close() | |
| async def close(self) -> None: | |
| """Cierra la conexión con MongoDB.""" | |
| if self.client is not None: | |
| await self.client.close() | |
| self.client = None | |
| self.database = None | |
| logger.info("Conexión a MongoDB cerrada.") | |
| mongodb = MongoDB() | |
| def get_database() -> AsyncDatabase: | |
| """Devuelve la base de datos activa. | |
| Pensada para usarse como dependencia de FastAPI. | |
| """ | |
| if mongodb.database is None: | |
| raise RuntimeError( | |
| "La base de datos no está inicializada. " | |
| "¿Se ejecutó el lifespan de la aplicación?" | |
| ) | |
| return mongodb.database | |
| def get_collection(name: str) -> AsyncCollection: | |
| """Devuelve una colección por su nombre.""" | |
| return get_database()[name] | |