Spaces:
Sleeping
Sleeping
File size: 4,044 Bytes
67aa1ca 57090be 67aa1ca 57090be 67aa1ca 57090be 67aa1ca 57090be 67aa1ca 57090be 67aa1ca 57090be 67aa1ca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | """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]
|