2

Excelente post.

O ponto brutal aqui é que nenhum desses bugs “quebrou” o sistema. Eles fizeram algo pior: ensinaram o sistema a mentir.

E num sistema de confiabilidade, mentira é corrupção de estado. Não é bug bonitinho, não é descuido inocente. É o tipo de falha que transforma um pagamento real em um webhook fantasma e ainda deixa o painel sorrindo com status entregue.

O primeiro bug é o mais Go possível pelo motivo errado:

resposta, err := clienteHTTP.Do(requisicao)
if err != nil {
    return "", nil
}

Isso aí é basicamente assassinar a semântica do error.

O Go é chato com err justamente porque ele te força a tomar uma decisão explícita. Só que, quando você retorna nil ali, você não está “tratando” o erro. Você está falsificando o resultado da operação.

Eu evitaria esse tipo de retorno frágil com string solta. Algo mais Go-like seria modelar a entrega como resultado explícito:

type DeliveryResult struct {
    StatusCode int
    Body       string
}

func Deliver(ctx context.Context, client *http.Client, req *http.Request) (DeliveryResult, error) {
    resp, err := client.Do(req)
    if err != nil {
        return DeliveryResult{}, fmt.Errorf("deliver webhook: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    result := DeliveryResult{
        StatusCode: resp.StatusCode,
        Body:       string(body),
    }

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return result, fmt.Errorf("webhook returned non-2xx status: %d", resp.StatusCode)
    }

    return result, nil
}

A regra fica simples: erro de rede é erro. HTTP não-2xx é erro de entrega. Só 2xx vira sucesso.

E principalmente: o worker não deveria decidir “entregue” porque recebeu uma string vazia e nil. Ele deveria marcar como entregue apenas quando a função de entrega devolve um resultado válido e sem erro. Estado de negócio não pode ser inferido de valor zero.

O segundo bug é outro clássico venenoso: log de sucesso fora do caminho de sucesso. Isso é pior que não ter log. Sem log você sabe que está cego. Com log mentiroso você acha que está enxergando e vai depurar na direção errada.

Esse trecho:

if erroBanco := repo.MarkDead(ctx, evento.ID, descricao); erroBanco != nil {
    log.Printf("erro ao marcar morto %d: %v", evento.ID, erroBanco)
}
log.Printf("evento %d esgotou as tentativas, marcado como MORTO", evento.ID)

deveria ser tratado quase como bug de integridade:

if err := repo.MarkDead(ctx, evento.ID, descricao); err != nil {
    log.Printf("erro ao marcar morto %d: %v", evento.ID, err)
    continue
}

log.Printf("evento %d esgotou as tentativas, marcado como MORTO", evento.ID)

Log de sucesso tem que estar depois da mutação bem-sucedida. Antes disso, é propaganda enganosa.

E o terceiro é o tapa clássico do time.Duration: 5 não é “cinco segundos”. É cinco nanossegundos. Em Go, duração sem unidade é cheiro de bug. Para configuração vinda de ambiente, eu prefiro ser chato: aceita 5s, 1m, 500ms; se vier 5, falha no boot e manda o operador corrigir. Default silencioso em configuração inválida é outro jeito educado de mentir.

A frase “software que quebra é chato; software que mente é caro” é perfeita.

Sistema confiável não é o que nunca falha. Isso não existe. Sistema confiável é o que falha de forma honesta, observável e recuperável.

Se o banco diz uma coisa, o log diz outra e o worker age como se uma terceira fosse verdade, você não tem confiabilidade. Você tem fanfic distribuída.

Carregando publicação patrocinada...