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]