Lógica de reintento y resiliencia de la conexión con mssql-django

Las conexiones a SQL Server y Azure SQL pueden producir errores transitorios por motivos que no tienen nada que ver con el código:

  • Se produce una conmutación por error en un grupo de disponibilidad Always On.
  • La red pierde un paquete durante el establecimiento de la conexión.
  • Resource Governor limita el rendimiento de la base de datos.
  • Una réplica de Azure SQL se recicla durante una escala o actualización.

La mayoría de estos errores se borran en cuestión de segundos. En este artículo se muestra cómo reintentar una operación ante errores transitorios en una aplicación de Django que usa el backend mssql-django, y cómo configurar Django y el controlador ODBC para recuperarse automáticamente de las desconexiones por inactividad.

Errores transitorios

Los errores transitorios son errores temporales que se resuelven por sí mismos. Reintentar la operación tras una breve espera suele funcionar.

Los errores siguientes son transitorios cuando se producen durante el establecimiento de la conexión o al enviar una solicitud al servidor. Reintentar con un retroceso breve y limitado. Los errores que persisten más allá de algunos reintentos suelen indicar un problema de configuración (servidor incorrecto, permisos que faltan, cuota agotada) que el reintento no corregirá.

Error Message Troubleshooting
64 A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 - The specified network name is no longer available.) La conexión TCP se interrumpe en medio del protocolo de enlace. No es un fallo de credenciales. Si persiste, compruebe si hay inestabilidad en la red del cliente o algún dispositivo intermedio que interrumpa conexiones establecidas solo parcialmente.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Error de transporte antes del inicio de sesión o de TLS. Normalmente, el servidor lo devuelve cuando no puede aceptar la conexión (agotamiento de recursos, conexiones máximas alcanzadas o un cliente no admitido). No es un fallo de credenciales. Compruebe el estado del servidor y, a continuación, compruebe el tiempo de espera de inicio de sesión del cliente, la configuración de TLS y la compatibilidad de la versión de TLS de cliente/servidor.
4060 Cannot open database "%.*ls" requested by the login. The login failed. El inicio de sesión se autentica, pero no puede abrir la base de datos solicitada. Las causas transitorias incluyen que la base de datos está en transición (conmutación por error, restauración, escalado) o pausada automáticamente. Las causas persistentes (la base de datos no existe, el inicio de sesión carece de acceso) no se solucionarán con un nuevo intento; comprueba el nombre de la base de datos, la asignación de inicio de sesión y el estado de la base de datos.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. La réplica no está disponible para el inicio de sesión porque faltan versiones de fila para las transacciones que estaban en curso cuando se recicló la réplica. Revierte o confirma las transacciones activas en el servidor principal para resolver el problema. Mitíguelo evitando transacciones de escritura prolongadas en la base de datos principal.
10053 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An established connection was aborted by the software in your host machine.) El extremo local interrumpe la conexión. Compruebe el estado de la red del lado cliente y cualquier firewall local o cliente VPN.
10054 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) El extremo remoto envía un reinicio de TCP. Causas comunes: el proceso remoto se bloqueó, un firewall forzó un restablecimiento de la conexión o la puerta de enlace de Azure SQL cerró una conexión inactiva. Para los patrones de reinicio por inactividad, habilite TCP keepalive en el cliente o reduzca el tiempo de espera de inactividad del pool de conexiones.
10928 Resource ID: %d. The %s limit for the database is %d and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance. La base de datos supera un límite de gobernanza de recursos Azure SQL. El identificador de recurso 1 indica el límite de trabajo; El identificador de recurso 2 indica el límite de sesión. Identifique el tipo de límite a partir del mensaje y reduzca la concurrencia, amplíe la base de datos o acorte las operaciones de larga duración que mantienen el recurso.
10929 Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d, and the current usage for the database is %d. However, the server is currently too busy to support requests greater than %d for this database. La base de datos ha superado su garantía mínima y el servidor subyacente está limitando el rendimiento. El reintento suele tener éxito cuando disminuye la carga de los servidores vecinos. Las apariciones sostenidas indican que necesita un nivel de servicio superior o un entorno menos ruidoso.
40020, 40143, , 40166, 40540 Notificado en el Error code %d campo del error 40197 durante la conmutación por error. Subcódigos incrustados en un mensaje de conmutación por error 40197 que, en algunas rutas, aparecen como número de error de primer nivel. Tratarlos igual que 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Una actualización de software, un error de hardware u otro evento de conmutación por error en Azure SQL. Al volver a conectarse, se le redirige a una réplica en buen estado. El código de error incluido identifica el tipo de conmutación por error. Si el error persiste, capture el identificador de seguimiento de sesión y póngase en contacto con el soporte técnico.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Limitación del motor de Azure SQL. El tiempo de espera mínimo recomendado es de 10 segundos. Una limitación prolongada indica que la carga de trabajo ha superado la asignación de recursos de la base de datos; amplíe el nivel de servicio o reduzca la concurrencia.
40613 Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them with the session tracing ID of '%.*ls'. La base de datos no está disponible, normalmente en medio de una conmutación por error o brevemente durante una operación de escalado. Vuelve a intentarlo con un retardo; si el problema persiste más allá de unos minutos, anota el ID de seguimiento de la sesión y abre un caso de soporte técnico.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. El grupo de SQL dedicado (Synapse) está en un estado en pausa. El reintento solo tendrá éxito una vez que se haya reanudado el grupo. Reanuda el grupo de forma explícita o programa la carga de trabajo para que se ejecute una vez que el grupo se haya reanudado.
42109 The SQL pool is warming up. Please try again. El grupo de SQL dedicado se está reactivando. Vuelve a intentarlo con un retardo hasta que el grupo esté en línea; el tiempo de calentamiento suele durar unos minutos.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. El servidor no puede asignar recursos suficientes actualmente para satisfacer la solicitud. Vuelve a intentarlo con un retardo. Si el error persiste, aumente la capacidad de la base de datos o del grupo elástico.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Límite de concurrencia a nivel de suscripción en las operaciones de administración. Reduzca las llamadas de creación y actualización paralelas o escalonelas.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Límite de concurrencia a nivel de suscripción en operaciones en curso. Reduce el paralelismo o espera a que finalicen las operaciones en curso.

Los errores a nivel de instrucción no figuran en esta lista porque se producen después de que se haya establecido la conexión y el error permite seguir utilizando la sesión. Los errores de instrucción más comunes que se pueden reintentar son el 1205 (víctima de interbloqueo) y el 1222 (tiempo de espera de solicitud de bloqueo agotado). Reintenta toda la transacción en lugar de la única instrucción que ha generado un error.

El texto del mensaje de error procede de Azure SQL errores de conexión transitorios. Los controladores individuales mantienen sus propias listas integradas de reintentos; este catálogo describe qué errores son susceptibles de reintento en SQL Server, Azure SQL Database, Azure SQL Managed Instance, la base de datos SQL de Microsoft Fabric y los grupos dedicados de SQL en Azure Synapse Analytics.

Resistencia de conexión inactiva del controlador ODBC

El controlador ODBC de Microsoft para SQL Server proporciona resistencia integrada a conexiones inactivas mediante las palabras clave ConnectRetryCount y ConnectRetryInterval de la cadena de conexión. Esta configuración administra las conexiones inactivas que se han cerrado a nivel del controlador, antes de que intervenga el código de tu aplicación.

Habilite la resistencia de conexión inactiva en extra_params:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "extra_params": "ConnectRetryCount=3;ConnectRetryInterval=10",
        },
    },
}
Keyword Predeterminado Description
ConnectRetryCount 1 Número de intentos de reconexión automática para conexiones inactivas.
ConnectRetryInterval 10 Segundos entre los intentos de reconexión.

Note

La resistencia de conexión inactiva vuelve a conectar las conexiones que se quitaron mientras estaban inactivas. No reintenta las consultas con errores ni se recupera de errores que se producen durante las transacciones activas. En esos escenarios, use la lógica de reintento de nivel de aplicación.

Middleware de base de datos de Django para reintentos

Cree un middleware de Django que capture errores transitorios y vuelva a intentar la operación de base de datos. Este enfoque funciona para la gestión de solicitudes a nivel de vista:

# myproject/middleware.py
import random
import re
import time
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
    # It is usually a permanent error (wrong database name or missing permissions).
}

# Microsoft ODBC driver formats native error codes as "(<number>)" in the
# message. Extracting parenthesized codes avoids false positives that a plain
# substring match would produce for short codes like "64".
_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


class DatabaseRetryMiddleware:
    """Retry database operations on transient errors."""

    def __init__(self, get_response):
        self.get_response = get_response
        self.max_retries = 3
        self.base_delay = 1   # seconds; doubled each attempt
        self.max_delay = 30   # cap on a single sleep, regardless of attempt

    def __call__(self, request):
        for attempt in range(self.max_retries + 1):
            try:
                return self.get_response(request)
            except OperationalError as e:
                if attempt < self.max_retries and is_transient(e):
                    # Exponential backoff with full jitter, capped at max_delay.
                    # Jitter spreads simultaneous retries so many clients
                    # don't hammer the server in lock-step during an outage.
                    capped = min(self.max_delay, self.base_delay * (2 ** attempt))
                    delay = random.uniform(0, capped)
                    logger.warning(
                        "Transient DB error (attempt %d/%d), retrying in %.2fs: %s",
                        attempt + 1, self.max_retries, delay, e
                    )
                    connection.close()
                    time.sleep(delay)
                    continue
                raise

Registre el middleware en settings.py:

MIDDLEWARE = [
    "myproject.middleware.DatabaseRetryMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ... other middleware
]

Importante

Coloque DatabaseRetryMiddleware antes de otro middleware que tenga acceso a la base de datos para que pueda detectar y reintentar errores transitorios desde toda la canalización de solicitudes.

Decorador de reintento para operaciones específicas

Para un control más detallado, utilice un decorador aplicado a funciones individuales:

import random
import re
import time
import functools
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
}

_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


def retry_on_transient(max_retries=3, base_delay=1, max_delay=30):
    """Retry on transient database errors with exponential backoff and full jitter."""

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except OperationalError as e:
                    if attempt < max_retries and is_transient(e):
                        # Exponential cap doubled per attempt, then jittered
                        # within [0, cap] and limited by max_delay.
                        capped = min(max_delay, base_delay * (2 ** attempt))
                        delay = random.uniform(0, capped)
                        logger.warning(
                            "Transient error in %s (attempt %d/%d), retrying in %.2fs: %s",
                            func.__name__, attempt + 1, max_retries, delay, e
                        )
                        connection.close()
                        time.sleep(delay)
                        continue
                    raise
        return wrapper
    return decorator

Aplique el decorador a funciones con uso intensivo de la base de datos:

from myproject.retry import retry_on_transient

@retry_on_transient(max_retries=3, base_delay=2)
def process_order(order_id):
    """Process an order with automatic retry on transient failures."""
    order = Order.objects.select_for_update().get(id=order_id)
    order.status = "processing"
    order.save()
    return order

Reintentar con transacciones

Cuando se produce un error transitorio dentro de una transacción, el servidor revierte toda la transacción. Reintenta la transacción completa, no solo la instrucción que ha generado un error:

from django.db import transaction

@retry_on_transient(max_retries=3)
def transfer_funds(from_account_id, to_account_id, amount):
    """Transfer funds between accounts with retry."""
    with transaction.atomic():
        from_account = Account.objects.select_for_update().get(id=from_account_id)
        to_account = Account.objects.select_for_update().get(id=to_account_id)

        from_account.balance -= amount
        to_account.balance += amount

        from_account.save()
        to_account.save()

Caution

No lo intente de nuevo dentro de transaction.atomic(). El decorador de reintento debe envolver todo el bloque atomic() para que cada reintento inicie una nueva transacción.

Errores a nivel de instrucción

La lista de errores de la sección anterior trata los errores de nivel de conexión. Hay otros dos errores que suelen reintentarse a nivel de instrucción:

  • 1205: La sesión fue elegida como víctima de interbloqueo. Vuelva a ejecutar la transacción.
  • 1222: Se superó el tiempo de espera de una solicitud de bloqueo. Vuelva a ejecutar la transacción o aumente LOCK_TIMEOUT para la sesión si el valor predeterminado es demasiado agresivo.

ConnectRetryCount reintenta las conexiones interrumpidas, por lo que no se aplica a estos errores a nivel de instrucción. Gestiónalos con el mismo patrón de decorador añadiendo "1205" y "1222" a TRANSIENT_ERROR_CODES para las transacciones que se puedan volver a ejecutar de forma segura.

CONN_MAX_AGE y conexiones obsoletas

Django reutiliza las conexiones de base de datos entre las solicitudes cuando CONN_MAX_AGE se establece. Una conexión de larga duración podría quedar obsoleta si el servidor lo cierra (por ejemplo, durante una operación de escalado de Azure SQL o un tiempo de espera del firewall).

Establezca CONN_MAX_AGE para equilibrar la reutilización y la obsolescencia:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,  # Close and reopen connections after 10 minutes
    },
}
  • CONN_MAX_AGE=0 (valor predeterminado): cierre la conexión al final de cada solicitud. Más seguro pero más lento.
  • CONN_MAX_AGE=600: reutiliza las conexiones durante 10 minutos. Buen equilibrio para la mayoría de las aplicaciones web.
  • CONN_MAX_AGE=None: mantenga las conexiones abiertas indefinidamente. Use solo con un mecanismo de reintento para conexiones obsoletas.

CONN_HEALTH_CHECKS (Django 4.1 y versiones posteriores)

Django 4.1 introdujo CONN_HEALTH_CHECKS, que valida una conexión reutilizada antes de cada solicitud. Habilite junto con CONN_MAX_AGE para detectar conexiones obsoletas automáticamente:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,
        "CONN_HEALTH_CHECKS": True,
    },
}

Con las comprobaciones de estado habilitadas, Django emite una consulta de validación ligera antes de reutilizar una conexión. Si se interrumpe la conexión, Django abre de forma transparente uno nuevo en lugar de generar un error.

procedimientos recomendados

  • Utiliza un retroceso exponencial con fluctuación completa. Duplica el límite en cada intento y, a continuación, espera un tiempo aleatorio dentro de [0, cap]. La fluctuación impide que muchos clientes vuelvan a intentarlo de forma sincronizada durante una interrupción regional, lo que, de otro modo, podría convertir un error breve en una sobrecarga prolongada. Establezca un límite máximo al tiempo de espera por intento (por ejemplo, 30 segundos) para que el tiempo total de recuperación permanezca dentro de unos límites.
  • Establezca un límite máximo de reintentos. Tres reintentos con retroceso exponencial es un valor predeterminado razonable. Más de cinco reintentos suelen indicar un problema no transitorio.
  • Cierre la conexión antes de reintentar. Llame connection.close() a para que Django abra una nueva conexión en el siguiente intento.
  • Registre cada reintento. Los reintentos que se completan con éxito de forma silenciosa pueden ocultar problemas de rendimiento. Registra en el nivel WARNING para poder realizar un seguimiento de la frecuencia.
  • No reintentes los errores no transitorios. Los errores de autenticación, los errores de permiso y los errores de sintaxis no se benefician de los reintentos.
  • Vuelva a intentar toda la transacción. Incluye transaction.atomic() dentro de la lógica de reintento, y no al revés.
  • Habilitar CONN_HEALTH_CHECKS (Django 4.1 y versiones posteriores) para aplicaciones web que usan CONN_MAX_AGE.