Retry, Backoff e Circuit Breaker

Retry simples, exponencial e backoff

Implementando retry com backoff exponencial e jitter no payment-service.

Avançado 30 min 25 pontos Leitura 0%

Nesta aula você vai

  • Implementar retry exponencial com jitter em Go
  • Aplicar somente em erros transitórios
  • Registrar tentativas com observabilidade

Retry simples, exponencial e backoff

Objetivos

  • Melhorar taxa de sucesso em falhas transitórias.
  • Evitar rajada de tentativas simultâneas.
  • Preparar base para política "3 tentativas e DLQ".

Pré-requisitos

  • payment-service em Go/Gin.
  • Classificação de erro disponível (transitório vs irrecuperável).
  • Kafka consumidor já funcional.

Conceito

Retry exponencial aumenta o intervalo a cada tentativa (base * 2^n). O jitter adiciona aleatoriedade para evitar efeito manada quando várias instâncias falham ao mesmo tempo.

Estrutura de arquivos

services/payment-service/internal/
├── retry/policy.go
├── retry/runner.go
└── consumer/payment_consumer.go

Passo a passo

  1. Crie política em retry/policy.go:
package retry

import (
  "math"
  "math/rand"
  "time"
)

type Policy struct {
  MaxAttempts int
  BaseDelay   time.Duration
  MaxDelay    time.Duration
}

func (p Policy) NextDelay(attempt int) time.Duration {
  exp := float64(p.BaseDelay) * math.Pow(2, float64(attempt-1))
  delay := time.Duration(exp)
  if delay > p.MaxDelay {
    delay = p.MaxDelay
  }
  jitter := time.Duration(rand.Int63n(int64(delay / 4)))
  return delay + jitter
}
  1. Runner genérico em retry/runner.go:
func Run(ctx context.Context, policy Policy, fn func() error, isRetryable func(error) bool) error {
  var lastErr error
  for attempt := 1; attempt <= policy.MaxAttempts; attempt++ {
    if err := fn(); err != nil {
      lastErr = err
      if !isRetryable(err) {
        return err
      }
      if attempt == policy.MaxAttempts {
        return err
      }
      delay := policy.NextDelay(attempt)
      timer := time.NewTimer(delay)
      select {
      case <-ctx.Done():
        timer.Stop()
        return ctx.Err()
      case <-timer.C:
      }
      continue
    }
    return nil
  }
  return lastErr
}
  1. Use no consumidor:
err := retry.Run(ctx, retry.Policy{
  MaxAttempts: 3,
  BaseDelay:   200 * time.Millisecond,
  MaxDelay:    2 * time.Second,
}, func() error {
  return c.gateway.Authorize(ctx, event.OrderID, event.AmountCents)
}, errors.IsRetryable)

Como testar

docker compose up -d payment-service kafka
docker compose logs -f payment-service

Simule instabilidade temporária no gateway e observe no log:

attempt=1 delay_ms=200
attempt=2 delay_ms=400
attempt=3 delay_ms=800

Dicas de projeto

  • Sempre combine retry com timeout de contexto.
  • Defina MaxDelay para evitar latência infinita.
  • Métrica por tentativa ajuda a ajustar parâmetros.

Erros comuns

  • Retry em erro de validação de payload.
  • Backoff fixo em vez de exponencial.
  • Falta de jitter em ambiente com múltiplas réplicas.

Resumo

Você implementou retry exponencial com jitter no payment-service, aplicando apenas em erros transitórios.