Retry, Backoff e Circuit Breaker

Circuit Breaker (introdução)

Protegendo o payment-service contra cascata de falhas em dependências externas.

Avançado 25 min 25 pontos Leitura 0%

Nesta aula você vai

  • Implementar estados closed open half-open em Go
  • Bloquear chamadas quando dependência está degradada
  • Expor estado do circuito para observabilidade

Circuit Breaker (introdução)

Objetivos

  • Evitar sobrecarga em dependência externa instável.
  • Reduzir latência de erro com falha rápida.
  • Melhorar diagnóstico operacional do fluxo de pagamento.

Pré-requisitos

  • Retry exponencial já implementado.
  • Gateway externo de pagamento disponível para simulação.
  • Métricas do payment-service habilitadas.

Conceito

Circuit breaker tem três estados:

  • closed: fluxo normal.
  • open: bloqueia chamadas por janela de resfriamento.
  • half-open: permite poucas chamadas de teste para decidir fechamento.

Estrutura de arquivos

services/payment-service/internal/
├── resilience/circuit_breaker.go
├── integrations/payment_gateway.go
└── consumer/payment_consumer.go

Passo a passo

  1. Implementação do breaker em resilience/circuit_breaker.go:
package resilience

import (
  "errors"
  "sync"
  "time"
)

var ErrCircuitOpen = errors.New("circuit breaker open")

type State string

const (
  Closed   State = "closed"
  Open     State = "open"
  HalfOpen State = "half_open"
)

type CircuitBreaker struct {
  mu               sync.Mutex
  state            State
  failures         int
  failureThreshold int
  openUntil        time.Time
  cooldown         time.Duration
}

func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker {
  return &CircuitBreaker{state: Closed, failureThreshold: threshold, cooldown: cooldown}
}

func (cb *CircuitBreaker) Execute(fn func() error) error {
  cb.mu.Lock()
  if cb.state == Open && time.Now().Before(cb.openUntil) {
    cb.mu.Unlock()
    return ErrCircuitOpen
  }
  if cb.state == Open && time.Now().After(cb.openUntil) {
    cb.state = HalfOpen
  }
  cb.mu.Unlock()

  err := fn()

  cb.mu.Lock()
  defer cb.mu.Unlock()
  if err != nil {
    cb.failures++
    if cb.failures >= cb.failureThreshold {
      cb.state = Open
      cb.openUntil = time.Now().Add(cb.cooldown)
    }
    return err
  }
  cb.failures = 0
  cb.state = Closed
  return nil
}
  1. Uso na integração com gateway:
err := c.breaker.Execute(func() error {
  return c.gateway.Authorize(ctx, event.OrderID, event.AmountCents)
})
if errors.Is(err, resilience.ErrCircuitOpen) {
  c.logger.Warn("gateway indisponível: circuito aberto")
  return c.dlqPublisher.Publish(ctx, msg, err, 0)
}
  1. Endpoint de estado:
router.GET("/internal/circuit-breaker", func(c *gin.Context) {
  c.JSON(200, gin.H{"state": paymentBreaker.State()})
})

Como testar

docker compose up -d payment-service kafka
curl -s http://localhost:8083/internal/circuit-breaker | jq

Simule falhas no gateway e consulte novamente:

curl -s http://localhost:8083/internal/circuit-breaker | jq
docker compose logs -f payment-service

Você deve observar transições closed -> open -> half_open -> closed.

Dicas de projeto

  • Combine circuit breaker com timeout e retry.
  • Configure threshold com base em erro real, não chute.
  • Exponha estado para alertas e dashboards.

Erros comuns

  • Não resetar estado após sucesso em half-open.
  • Abrir circuito sem cooldown.
  • Tratar circuito aberto como erro de domínio.

Resumo

Você adicionou circuit breaker ao payment-service, protegendo o sistema contra cascata de falhas em dependências externas.