Retry, Backoff e Circuit Breaker
Circuit Breaker (introdução)
Protegendo o payment-service contra cascata de falhas em dependências externas.
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-servicehabilitadas.
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
- 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
}
- 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)
}
- 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.