Lógica de nova tentativa e resiliência da conexão com mssql-django

As conexões com SQL Server e SQL do Azure podem falhar transitóriamente por motivos que não têm nada a ver com seu código:

  • Um grupo de disponibilidade Always On sofre failover.
  • A rede descarta um pacote durante a instalação da conexão.
  • O Resource Governor restringe o banco de dados.
  • Uma réplica do SQL do Azure é recriada durante um redimensionamento ou uma atualização.

A maioria dessas falhas se resolve em poucos segundos. Este artigo mostra como repetir erros transitórios em um aplicativo Django que usa o mssql-django back-end e como configurar o Django e o driver ODBC para se recuperar de quedas de conexão ociosa automaticamente.

Erros transitórios

Erros transitórios são falhas temporárias que são resolvidas por conta própria. Tentar novamente a operação após um pequeno atraso geralmente é bem-sucedido.

Os erros a seguir são transitórios quando ocorrem durante o estabelecimento da conexão ou ao enviar uma solicitação ao servidor. Tente novamente em uma retirada curta e limitada. Erros que persistem após algumas tentativas geralmente indicam um problema de configuração (servidor incorreto, permissões ausentes, cota esgotada) que a repetição não corrigirá.

Erro Mensagem 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.) A conexão TCP é interrompida durante o handshake. Não é uma falha de credenciais. Se persistir, verifique se há instabilidade de rede do lado do cliente ou um dispositivo intermediário que descarta conexões semi-estabelecidas.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Transporte de pré-logon ou falha do TLS. O servidor normalmente retorna quando não pode aceitar a conexão (esgotamento de recursos, conexões máximas alcançadas ou um cliente sem suporte). Não é uma falha de credenciais. Verifique a integridade do servidor e verifique o tempo limite de logon do cliente, as configurações do TLS e a compatibilidade de versão do TLS cliente/servidor.
4060 Cannot open database "%.*ls" requested by the login. The login failed. O logon é autenticado, mas não pode abrir o banco de dados solicitado. As causas transitórias incluem o fato de o banco de dados estar em transição (failover, restauração, redimensionamento) ou em pausa automática. As causas persistentes (o banco de dados não existe, o logon não tem acesso) não serão corrigidas por repetição; verifique o nome do banco de dados, o mapeamento de logon e o estado do banco de dados.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. A réplica não está disponível para login porque as versões das linhas estão ausentes para transações que ainda estavam em andamento quando a réplica foi reciclada. Reverta ou confirme as transações ativas no primário para resolver o problema. Mitigue evitando transações longas de gravação no servidor primário.
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.) O lado local aborta a conexão. Verifique a integridade da rede do lado do cliente e qualquer firewall local ou 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.) O lado remoto envia uma redefinição de TCP. Causas comuns: o processo par falhou, um firewall injetou uma redefinição ou o gateway de SQL do Azure fechou uma conexão ociosa. Para padrões de redefinição por inatividade, habilite o keepalive TCP no cliente ou reduza o tempo limite de inatividade do pool de conexões.
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. O banco de dados excede um limite de governança de recursos SQL do Azure. A ID do recurso 1 indica o limite de trabalho; A ID do recurso 2 indica o limite da sessão. Identifique o tipo de limite na mensagem e, em seguida, reduza a concorrência, aumente a capacidade do banco de dados ou encurte as operações de longa duração que mantêm o recurso ocupado.
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. O banco de dados está acima de sua garantia mínima e o servidor subjacente está limitando. Normalmente, a repetição é bem-sucedida quando a carga do vizinho cai. Ocorrências sustentadas indicam que você precisa de uma camada de serviço mais alta ou um ambiente menos barulhento.
40020, 40143, , 4016640540 Relatado no slot Error code %d do erro 40197 durante o failover. Subcódigos incorporados em uma mensagem de failover 40197 que alguns caminhos expõem como o número de erro de nível superior. Trate-os da mesma forma que 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Uma atualização de software, uma falha de hardware ou outro evento de failover no SQL do Azure. Ao reconectar, você é redirecionado para uma réplica saudável. O código de erro inserido identifica o tipo de failover. Se o erro persistir, capture a ID de rastreamento da sessão e contate o suporte.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Limitação do mecanismo do SQL do Azure. O mínimo recomendado é um intervalo de espera de 10 segundos. A limitação contínua indica que a carga de trabalho excedeu a alocação de recursos do banco de dados; aumente a camada de serviço ou reduza a concorrência.
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'. O banco de dados não está disponível, geralmente durante um failover ou por um breve período durante uma operação de escala. Tente novamente após um intervalo de espera; se o problema persistir por alguns minutos, capture o ID de rastreamento da sessão e abra um chamado de suporte.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. O pool de SQL dedicado (Synapse) está em pausa. A nova tentativa só é bem-sucedida depois que o pool é retomado. Retome o pool explicitamente ou agende a carga de trabalho a ser executada após a retomada do pool.
42109 The SQL pool is warming up. Please try again. O pool de SQL dedicado está sendo reiniciado. Tente novamente com backoff até que o pool fique online; a inicialização normalmente leva alguns minutos.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. No momento, o servidor não pode alocar recursos suficientes para atender à solicitação. Tente novamente após um intervalo de espera. Se o erro persistir, aumente a capacidade do banco de dados ou do pool elástico.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Limite de concorrência no nível da assinatura para operações de gerenciamento. Reduza chamadas paralelas de criação/atualização ou escalone-as.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Limite de concorrência de operações em andamento no nível da assinatura. Reduza o paralelismo ou aguarde até que as operações em andamento sejam concluídas.

Os erros no nível da instrução não estão nessa lista porque são disparados depois que a conexão é estabelecida e a falha deixa a sessão utilizável. Os erros de instrução passíveis de repetição mais comuns são 1205 (vítima de deadlock) e 1222 (tempo limite da solicitação de bloqueio). Repita a transação inteira em vez da única instrução com falha.

O texto da mensagem de erro vem de erros transitórios de conexão do SQL do Azure. Os drivers individuais mantêm suas próprias listas internas de nova tentativa; este catálogo descreve quais erros são passíveis de nova tentativa no SQL Server, no Banco de Dados SQL do Azure, no Instância Gerenciada de SQL do Azure, no Banco de Dados SQL no Microsoft Fabric e nos pools de SQL dedicados no Azure Synapse Analytics.

Resiliência de conexão ociosa do driver ODBC

O Driver ODBC da Microsoft para SQL Server fornece resiliência interna para conexões ociosas por meio das palavras-chave ConnectRetryCount e ConnectRetryInterval da cadeia de conexão. Essas configurações manipulam conexões ociosas descartadas no nível do driver, antes que o código do aplicativo esteja envolvido.

Habilitar resiliência de conexão ociosa em 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 Default Descrição
ConnectRetryCount 1 Número de tentativas de reconexão automática para conexões ociosas.
ConnectRetryInterval 10 Segundos entre tentativas de reconexão.

Note

A resiliência de conexões ociosas reconecta conexões que foram interrompidas quando estão ociosas. Ele não repete consultas com falha nem se recupera de erros que ocorrem durante transações ativas. Para esses cenários, use a lógica de repetição no nível do aplicativo.

Middleware de banco de dados do Django para novas tentativas

Crie um middleware do Django que captura erros transitórios e tenta novamente a operação de banco de dados. Essa abordagem funciona para tratamento de solicitações no nível da exibição:

# 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 o middleware em settings.py:

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

Importante

Coloque DatabaseRetryMiddleware antes de outro middleware que acesse o banco de dados para que ele possa capturar e tentar novamente erros transitórios de todo o pipeline de solicitação.

Decorador de repetição para operações específicas

Para um controle mais refinado, use um decorador em funções individuais:

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 o decorador a funções com uso intensivo do banco de dados:

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

Tentar novamente com transações

Quando ocorre um erro transitório dentro de uma transação, toda a transação é revertida pelo servidor. Tente novamente a transação completa, não apenas a instrução que falhou:

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()

Cuidado

Não tente novamente em transaction.atomic(). O decorador de nova tentativa deve envolver todo o bloco atomic() para que cada tentativa inicie uma nova transação.

Erros no nível da declaração

A lista de erros na seção anterior aborda falhas no nível da conexão. Dois outros erros geralmente são repetidos no nível da instrução:

  • 1205: A sessão foi escolhida como vítima de deadlock. Execute a transação novamente.
  • 1222: Um tempo limite de solicitação de bloqueio foi excedido. Execute novamente a transação ou aumente LOCK_TIMEOUT para a sessão se o valor padrão for muito agressivo.

ConnectRetryCount tenta novamente conexões interrompidas, portanto, ela não se aplica a esses erros no nível da instrução. Trate-os com o mesmo padrão decorator, adicionando "1205" e "1222" a TRANSIENT_ERROR_CODES para transações que podem ser reexecutadas com segurança.

CONN_MAX_AGE e conexões obsoletas

O Django reutiliza conexões de banco de dados entre solicitações quando CONN_MAX_AGE é definido. Uma conexão de longa duração poderá ficar obsoleta se o servidor a fechar (por exemplo, durante uma operação de escala SQL do Azure ou um tempo limite de firewall).

Defina CONN_MAX_AGE para equilibrar a reutilização com a obsolescência:

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 (padrão): feche a conexão no final de cada solicitação. Mais seguro, mas mais lento.
  • CONN_MAX_AGE=600: Reutilize conexões por 10 minutos. Bom equilíbrio para a maioria dos aplicativos Web.
  • CONN_MAX_AGE=None: mantenha as conexões abertas indefinidamente. Use apenas com um mecanismo de repetição para conexões obsoletas.

CONN_HEALTH_CHECKS (Django 4.1 e posterior)

O Django 4.1 introduziu CONN_HEALTH_CHECKS, que valida uma conexão reutilizada antes de cada requisição. Habilite-o juntamente com CONN_MAX_AGE, para detectar conexões obsoletas automaticamente:

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,
    },
}

Com as verificações de integridade habilitadas, o Django emite uma consulta de validação leve antes de reutilizar uma conexão. Se a conexão estiver interrompida, o Django abrirá de forma transparente uma nova em vez de gerar um erro.

Práticas recomendadas

  • Use a retirada exponencial com tremulação total. Dobre o limite a cada tentativa e, em seguida, aguarde um tempo aleatório dentro de [0, cap]. O jitter impede que muitos clientes tentem novamente de forma sincronizada durante uma indisponibilidade regional, o que, de outra forma, pode transformar uma breve falha em uma sobrecarga sustentada. Defina um limite máximo para o tempo de espera por tentativa (por exemplo, 30 segundos), para que o tempo total de recuperação permaneça dentro de um limite.
  • Defina um teto de repetição. Três repetições com retirada exponencial são um padrão razoável. Mais de cinco repetições geralmente indicam um problema nãotransiente.
  • Feche a conexão antes de tentar novamente. Chame connection.close() para que o Django abra uma nova conexão na próxima tentativa.
  • Registre cada repetição. Novas tentativas bem-sucedidas sem aviso podem ocultar problemas de desempenho. Faça logon no WARNING nível para que você possa acompanhar a frequência.
  • Não tente novamente em caso de erros não transitórios. Falhas de autenticação, erros de permissão e erros de sintaxe não se beneficiam de novas tentativas.
  • Tente novamente toda a transação. Envolva transaction.atomic() na lógica de nova tentativa, não o contrário.
  • Ativar CONN_HEALTH_CHECKS (Django 4.1 e versões posteriores) para aplicativos da web que usam CONN_MAX_AGE.