Tratamento de erros e padrões de repetição de tentativas com go-mssqldb

As aplicações Production Go precisam de um tratamento estruturado de erros para distinguir entre falhas transitórias que pode tentar novamente e erros permanentes que requerem intervenção humana. Este artigo aborda a classificação de erros, os padrões de repetição de tentativas e as estratégias de resiliência para o go-mssqldb controlador.

Estrutura de erro do SQL Server

Quando o SQL Server devolve um erro, o go-mssqldb driver envolve-o numa mssql.Error struct. Use uma asserção de tipo para aceder aos campos de erro estruturados:

import (
    "database/sql"
    "errors"
    "fmt"

    mssql "github.com/microsoft/go-mssqldb"
)

func handleError(err error) {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        fmt.Printf("Number:  %d\n", mssqlErr.Number)
        fmt.Printf("State:   %d\n", mssqlErr.State)
        fmt.Printf("Class:   %d\n", mssqlErr.Class)
        fmt.Printf("Message: %s\n", mssqlErr.Message)
        fmt.Printf("Server:  %s\n", mssqlErr.ServerName)
        fmt.Printf("Proc:    %s\n", mssqlErr.ProcName)
        fmt.Printf("Line:    %d\n", mssqlErr.LineNo)
    }
}

Campos de erro

Campo Tipo Description
Number int32 Número de erro do SQL Server. Mapeia para sys.messages.
State uint8 Estado de erro. Fornece contexto adicional para o mesmo número de erro.
Class uint8 Nível de gravidade (0-25). As gravidades 11-16 podem ser corrigidas pelo utilizador. Gravidade 17+ indica problemas de recursos ou de sistema.
Message string Texto de erro percetível pelo utilizador do servidor.
ServerName string Nome da instância do SQL Server que gerou o erro.
ProcName string Procedimento armazenado ou nome da função onde ocorreu o erro. Vazio para consultas ad hoc.
LineNo int32 Número de linha no Transact-SQL batch (T-SQL) ou procedimento armazenado.

Níveis de severidade

Intervalo de gravidade Meaning Action
0-10 Mensagens informativas Sem erro. Regista se for útil.
11-16 Erros corrigíveis pelo utilizador Corrige a consulta, os parâmetros ou as permissões.
17-19 Erros de recursos Tente novamente. O servidor pode estar sob carga ou sem recursos.
20-25 Erros fatais A ligação está quebrada. Reconecta-se e tenta novamente.

Classificar erros como transitórios ou permanentes

Erros transitórios são condições temporárias que se resolvem sozinhas, como falhas de rede, limitação de ligação ou contenção breve de recursos. Erros permanentes requerem alterações no código ou na configuração.

Números de erro transitórios comuns

Use o seguinte catálogo partilhado como lista canónica de erros transitórios de estabelecimento de ligação e transporte de caminhos de pedido:

Os seguintes erros são transitórios quando ocorrem durante o estabelecimento da ligação ou ao enviar um pedido para o servidor. Tente novamente após um curto intervalo de espera limitado. Erros que persistem para além de algumas tentativas geralmente indicam um problema de configuração (servidor errado, permissões em falta, quota esgotada) que a tentativa novamente não resolve.

Erro 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.) A ligação TCP cai a meio do handshake. Não é uma falha de credenciais. Se persistir, verifique se há instabilidade na rede do lado do cliente ou um dispositivo intermédio que interrompa ligações semiestabelecidas.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Falha de transporte pré-login ou TLS. O servidor normalmente devolve-a quando não consegue aceitar a ligação (esgotamento de recursos, ligações máximas atingidas ou um cliente não suportado). Não é uma falha de credenciais. Verifica o estado do servidor e depois verifica o timeout de login do cliente, as definições do TLS e a compatibilidade da versão cliente/servidor TLS.
4060 Cannot open database "%.*ls" requested by the login. The login failed. O login autentica, mas não consegue abrir a base de dados solicitada. Causas transitórias incluem a base de dados estar em transição (failover, restauração, escalabilidade) ou em pausa automática. Causas persistentes (a base de dados não existe, o login não tem acesso) não serão corrigidas por uma nova tentativa; Verifique o nome da base de dados, o mapeamento de login e o estado da base 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 início de sessão porque as versões de linha estão em falta para transações que estavam em curso quando a réplica foi reciclada. Reverta ou compromete as transações ativas no principal para resolver o problema. Atenue evitando transações de escrita longas no 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 interrompe a ligação. Verifique a saúde 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 um reset TCP. Causas comuns: o processo peer crashava, um firewall injetava um reset, ou o gateway SQL do Azure fechava uma ligação inativa. Para casos de reposição da ligação por inatividade, ative o keepalive de TCP no cliente ou reduza o tempo limite de inatividade do conjunto de ligaçõ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. A base de dados ultrapassa um limite de governação de recursos do SQL do Azure. O ID de Recurso 1 indica o limite de trabalhadores; O ID do Recurso 2 indica o limite de sessão. Identifique o tipo de limite a partir da mensagem, depois reduza a concorrência, escale a base de dados ou encurta as operações de longa duração que detêm o 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. A base de dados ultrapassa a sua garantia mínima e o servidor subjacente está a limitar. Retry normalmente tem sucesso quando a carga do vizinho diminui. Ocorrências prolongadas indicam que precisa de um nível de serviço mais elevado ou de um ambiente menos ruidoso.
40020, 40143, 40166, 40540 Reportado na posição Error code %d do erro 40197 durante a comutação pós-falha. Subcódigos incorporados numa mensagem de failover 40197 que alguns caminhos apresentam como o número de erro de nível superior. Trata-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, falha de hardware ou outro evento de failover no SQL do Azure. Ao restabelecer a ligação, será encaminhado para uma réplica em bom estado. O código de erro incorporado identifica o tipo de failover. Se o erro persistir, regista o ID de rastreamento da sessão e contacta o suporte.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Limitação do motor do SQL do Azure. O intervalo mínimo recomendado é de 10 segundos. A limitação contínua indica que a carga de trabalho excedeu a alocação de recursos da base de dados; aumente o nível de serviço ou reduza o processamento simultâneo.
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'. A base de dados está indisponível, geralmente a meio do failover ou brevemente durante uma operação de escala. Tente novamente com intervalo progressivo; se o problema persistir durante mais do que alguns minutos, registe o ID de rastreio da sessão e abra um pedido de suporte.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. O pool dedicado de SQL (Synapse) está em estado de pausa. A nova tentativa só é bem-sucedida depois de o pool ser reativado. Retome explicitamente o pool ou programe a carga de trabalho para ser executada depois de o pool ser retomado.
42109 The SQL pool is warming up. Please try again. O pool dedicado de SQL está a recomeçar. Tente novamente um recuo até a piscina estar online; O aquecimento normalmente demora alguns minutos.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. O servidor não consegue atualmente alocar recursos suficientes para satisfazer o pedido. Tente novamente após um intervalo de espera. Se o erro persistir, amplie a base de dados ou o elastic pool.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Limite de concorrência a nível da subscrição para operações de gestão. Reduza as 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 ao nível de subscrição para operações em voo. Reduzir o paralelismo ou esperar que as operações em voo se esgotem.

Os erros ao nível da instrução não constam desta lista porque ocorrem depois de a ligação ter sido estabelecida e a falha não inutiliza a sessão. Os erros mais comuns das instruções passíveis de repetição são 1205 (vítima de impasse [deadlock]) e 1222 (tempo limite do pedido de bloqueio). Tente novamente toda a transação em vez do único extrato falhado.

O texto da mensagem de erro provém de erros de ligação transitória do SQL do Azure. Os controladores individuais mantêm as suas próprias listas de repetição integradas; este catálogo descreve os erros passíveis de repetição no SQL Server, no Base de Dados SQL do Azure, no Azure SQL Managed Instance, na Base de Dados SQL no Microsoft Fabric e em pools de SQL dedicados no Azure Synapse Analytics.

A função seguinte isTransient mostra um padrão de implementação em Go para a classificação de retentativas. Trate o catálogo partilhado acima como fonte de verdade e mantenha a sua pesquisa de código alinhada com ele.

// isTransient returns true if the error is a transient SQL Server error
// that is likely to succeed on retry.
func isTransient(err error) bool {
    var mssqlErr mssql.Error
    if !errors.As(err, &mssqlErr) {
        // Network errors, context deadlines, and connection resets
        // are also transient.
        return isNetworkError(err)
    }

    if isTransientSQLNumber(mssqlErr.Number) {
        return true
    }

    // Severity 17-19 indicates resource issues that are typically transient.
    return mssqlErr.Class >= 17 && mssqlErr.Class <= 19
}

// Keep this lookup synchronized with the shared transient catalog above.
var transientSQLNumbers = map[int32]struct{}{
    64:    {}, // Transport/connection error.
    1205:  {}, // Deadlock victim.
    40197: {}, // Service error processing request.
    40501: {}, // Service is currently busy.
    40613: {}, // Database is currently unavailable.
    49918: {}, // Cannot process request: not enough resources.
    49919: {}, // Cannot process create/update request.
    49920: {}, // Cannot process request: too many operations.
}

func isTransientSQLNumber(number int32) bool {
    _, ok := transientSQLNumbers[number]
    return ok
}

Se experienciar erros de configuração e de quota, corrija a capacidade subjacente, a base de dados ou a configuração da rede antes de tentar novamente. Os exemplos incluem:

  • 40544 (quota de tamanho da base de dados)
  • 4060 (não é possível abrir a base de dados)
  • 40615 (regra do firewall)

Detetar erros de rede

Os erros ao nível da rede não produzem valores mssql.Error. Verifique os tipos comuns de erro de rede do Go:

import (
    "context"
    "errors"
    "net"
    "io"
)

func isNetworkError(err error) bool {
    if err == nil {
        return false
    }

    // Context deadline exceeded or canceled
    if errors.Is(err, context.DeadlineExceeded) {
        return true
    }

    // Connection reset or broken pipe
    var netErr *net.OpError
    if errors.As(err, &netErr) {
        return true
    }

    // Unexpected EOF (server dropped the connection)
    if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
        return true
    }

    return false
}

Implementar nova tentativa com intervalo exponencial

Retente erros transitórios com atrasos crescentes entre tentativas. Esta abordagem dá tempo ao servidor para recuperar e evita sobrecarregá-lo com repetições rápidas.

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "log"
    "math"
    "math/rand"
    "time"

    mssql "github.com/microsoft/go-mssqldb"
)

// RetryConfig controls retry behavior.
type RetryConfig struct {
    MaxAttempts int           // Maximum number of attempts (including the first).
    BaseDelay   time.Duration // Initial delay before the first retry.
    MaxDelay    time.Duration // Upper bound on delay between retries.
}

// DefaultRetryConfig provides sensible defaults for SQL Server workloads.
var DefaultRetryConfig = RetryConfig{
    MaxAttempts: 5,
    BaseDelay:   100 * time.Millisecond,
    MaxDelay:    10 * time.Second,
}

// RetryFunc executes fn with retries for transient errors.
func RetryFunc(ctx context.Context, cfg RetryConfig, fn func(ctx context.Context) error) error {
    var lastErr error
    for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
        lastErr = fn(ctx)
        if lastErr == nil {
            return nil
        }

        if !isTransient(lastErr) {
            return lastErr // Permanent error, don't retry.
        }

        if attempt == cfg.MaxAttempts-1 {
            break // Last attempt, don't sleep.
        }

        delay := calculateDelay(attempt, cfg.BaseDelay, cfg.MaxDelay)

        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(delay):
        }
    }
    return lastErr
}

func calculateDelay(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
    // Exponential backoff: base * 2^attempt
    delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(attempt)))
    if delay > maxDelay {
        delay = maxDelay
    }
    // Add jitter: +/- 25% to avoid thundering herd
    jitter := time.Duration(rand.Int63n(int64(delay) / 2))
    return delay/2 + jitter
}

// isTransient classifies retryable SQL Server errors.
// For a fuller example, see "Classify errors as transient or permanent" earlier in this article.
func isTransient(err error) bool {
    var mssqlErr mssql.Error
    if !errors.As(err, &mssqlErr) {
        return errors.Is(err, context.DeadlineExceeded)
    }

    switch mssqlErr.Number {
    case 1205, 40197, 40501, 40613, 49918, 49919, 49920:
        return true
    }

    return mssqlErr.Class >= 17 && mssqlErr.Class <= 19
}

// getEmployeeCount wraps a query with automatic retry.
func getEmployeeCount(ctx context.Context, db *sql.DB) (int, error) {
    var count int
    err := RetryFunc(ctx, DefaultRetryConfig, func(ctx context.Context) error {
        // This query executes on every retry attempt until success or exhaustion.
        return db.QueryRowContext(ctx, "SELECT COUNT(*) FROM HumanResources.Employee").Scan(&count)
    })
    return count, err
}

// Example call site (assumes db is already initialized).
func example(ctx context.Context, db *sql.DB) {
    queryCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
    defer cancel()

    count, err := getEmployeeCount(queryCtx, db)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Employee count: %d\n", count)
}

Lidar com bloqueios

Os deadlocks (erro 1205) são o erro transitório mais comum em aplicações multiutilizador. O SQL Server termina automaticamente uma das sessões concorrentes e devolve o erro 1205 à vítima.

Detetar um interbloqueio

Verifique se um erro do SQL Server é um bloqueio (erro 1205).

func isDeadlock(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 1205
    }
    return false
}

Tentar novamente as transações após interbloqueios

Quando ocorre um deadlock dentro de uma transação, o servidor reverte toda a transação. Deve tentar novamente a transação completa, não apenas a declaração falhada:

func transferInventory(ctx context.Context, db *sql.DB, productID, fromLocationID, toLocationID int, qty int) error {
    return RetryFunc(ctx, DefaultRetryConfig, func(ctx context.Context) error {
        tx, err := db.BeginTx(ctx, &sql.TxOptions{
            Isolation: sql.LevelReadCommitted,
        })
        if err != nil {
            return err
        }
        defer tx.Rollback()

        _, err = tx.ExecContext(ctx,
            "UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @lid",
            sql.Named("qty", qty),
            sql.Named("pid", productID),
            sql.Named("lid", fromLocationID))
        if err != nil {
            return err
        }

        _, err = tx.ExecContext(ctx,
            "UPDATE Production.ProductInventory SET Quantity = Quantity + @qty WHERE ProductID = @pid AND LocationID = @lid",
            sql.Named("qty", qty),
            sql.Named("pid", productID),
            sql.Named("lid", toLocationID))
        if err != nil {
            return err
        }

        return tx.Commit()
    })
}

Tip

Reduza os interbloqueios acedendo às tabelas numa ordem consistente em todas as transações e mantendo-as curtas.

Voltar a tentar é a abordagem correta no código da aplicação, mas interbloqueios repetidos na mesma consulta indicam um problema de conceção. Use o grafo de deadlock do SQL Server (capturado através de Eventos Estendidos ou da sessão de saúde do sistema) para identificar as instruções concorrentes e os tipos de bloqueio. Para uma explicação detalhada da análise e prevenção de interbloqueios, consulte o guia sobre interbloqueios. Para estratégias de gestão de deadlocks específicas para transações, veja Deadlock handling.

Lidar com a exaustão do pool de ligações

Quando todas as conexões no pool estão em uso e se atinge MaxOpenConns, as novas chamadas ficam bloqueadas até que uma conexão fique disponível ou o prazo limite do contexto expire. Esta situação manifesta-se como pedidos lentos ou erros de prazo de contexto, não como erros explícitos de esgotamento de pool.

Detetar pressão da piscina

Monitorize as estatísticas do pool e alerte quando o número de espera aumenta.

func monitorPool(ctx context.Context, db *sql.DB) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    var lastWaitCount int64
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            stats := db.Stats()
            newWaits := stats.WaitCount - lastWaitCount
            lastWaitCount = stats.WaitCount

            if newWaits > 0 {
                log.Printf("Pool pressure: open=%d inUse=%d idle=%d newWaits=%d waitDuration=%v",
                    stats.OpenConnections, stats.InUse, stats.Idle,
                    newWaits, stats.WaitDuration)
            }
        }
    }
}

Causas comuns e soluções

Symptom Motivo Solução
WaitCount aumenta de forma constante MaxOpenConns é demasiado baixo Aumenta MaxOpenConns para corresponder à tua concorrência.
InUse é igual a MaxOpenConns por períodos prolongados As conexões não são devolvidas ao pool Feche *sql.Rows, confirme ou reverta *sql.Tx e feche *sql.Conn de imediato.
OpenConnections continua a crescer As ligações sofrem fugas mais depressa do que MaxIdleConns consegue reciclar Defina ConnMaxLifetime e ConnMaxIdleTime para limitar a idade da ligação.
Prazo limite do contexto excedido durante as consultas A piscina está saturada e os chamadores esperam demasiado tempo Aumentar o tamanho do pool, reduzir o tempo de execução das consultas ou adicionar tempos de espera para as consultas.

Lidar com erros específicos do SQL Server

Violações de restrições

As violações de chave única e de chave estrangeira são erros permanentes que indicam um problema lógico na aplicação:

func isUniqueViolation(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 2627 || // Unique constraint violation
            mssqlErr.Number == 2601       // Unique index violation
    }
    return false
}

func isForeignKeyViolation(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 547 // FK constraint violation
    }
    return false
}

Padrão Upsert com deteção de conflito

Utilize uma instrução MERGE para inserir ou atualizar uma linha de forma atómica:

func upsertDepartment(ctx context.Context, db *sql.DB, id int, name, groupName string) error {
    _, err := db.ExecContext(ctx, `
        MERGE INTO HumanResources.Department AS target
        USING (SELECT @id AS DepartmentID, @name AS Name, @grp AS GroupName) AS source
        ON target.DepartmentID = source.DepartmentID
        WHEN MATCHED THEN
            UPDATE SET Name = source.Name, GroupName = source.GroupName
        WHEN NOT MATCHED THEN
            INSERT (Name, GroupName) VALUES (source.Name, source.GroupName);`,
        sql.Named("id", id),
        sql.Named("name", name),
        sql.Named("grp", groupName))
    return err
}

Erros de permissões

Detetar códigos de erro comuns de “permissão negada” para fornecer uma mensagem clara ao autor da chamada:

func isPermissionError(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 229 ||   // SELECT permission denied
            mssqlErr.Number == 230 ||       // Column permission denied
            mssqlErr.Number == 262 ||       // CREATE permission denied
            mssqlErr.Number == 300 ||       // VIEW permission denied
            mssqlErr.Number == 15247        // User doesn't have permission
    }
    return false
}

Processar sql.ErrNoRows

sql.ErrNoRowsnão é um erro do SQL Server. O QueryRowContext.Scan método devolve-o quando a consulta não devolve linhas. Manuse-o explicitamente para distinguir "não encontrado" de erros reais:

func getEmployee(ctx context.Context, db *sql.DB, id int) (*Employee, error) {
    var emp Employee
    err := db.QueryRowContext(ctx,
        "SELECT TOP (1) BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location FROM Sales.vSalesPerson WHERE BusinessEntityID = @p1",
        sql.Named("p1", id)).Scan(&emp.Id, &emp.Name, &emp.Location)

    if errors.Is(err, sql.ErrNoRows) {
        return nil, nil // Not found, not an error.
    }
    if err != nil {
        return nil, fmt.Errorf("query employee %d: %w", id, err)
    }
    return &emp, nil
}

Adicionar contexto às mensagens de erro

Adicione contexto aos erros para que os chamadores possam perceber onde ocorreu a falha:

func getEmployeesByLocation(ctx context.Context, db *sql.DB, location string) ([]Employee, error) {
    rows, err := db.QueryContext(ctx,
        "SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
        sql.Named("p1", location))
    if err != nil {
        return nil, fmt.Errorf("query employees by location %q: %w", location, err)
    }
    defer rows.Close()

    var employees []Employee
    for rows.Next() {
        var emp Employee
        if err := rows.Scan(&emp.Id, &emp.Name, &emp.Location); err != nil {
            return nil, fmt.Errorf("scan employee row: %w", err)
        }
        employees = append(employees, emp)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("iterate employee rows: %w", err)
    }
    return employees, nil
}

A utilização %w preserva a cadeia de erros para que os chamadores possam continuar a usar errors.As e errors.Is inspecionar o erro subjacente.

Lista de verificação para o tratamento de erros

Area Recommendation
Asserção de tipo Declare var mssqlErr mssql.Error, depois use errors.As(err, &mssqlErr) para aceder a campos de erro do SQL Server.
Deteção de transientes Classificar os erros por número e gravidade antes de decidir se deve tentar novamente.
Lógica de nova tentativa Usa um retrocesso exponencial com jitter. Defina um número máximo de tentativas e um tempo final de espera com base no contexto.
Deadlocks Repita a transação completa, não instruções individuais. Reduza os interbloqueios acedendo às tabelas de forma consistente.
Exaustão na piscina Monitorize db.Stats() e defina prazos contextuais para todas as chamadas à base de dados.
ErrNoRows Processar sql.ErrNoRows explicitamente em relação a QueryRowContext. Não é um erro do servidor.
Encapsulamento de erros Use fmt.Errorf com %w para adicionar contexto, preservando a cadeia de erros.
Violações de restrições Verifique se existem os números de erro 2627, 2601 (unicidade) e 547 (chave externa), para tratar conflitos de forma elegante.