coder16
update
57090be
Raw
History Blame Contribute Delete
4.04 kB
"""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]