Fase 3 concluída — CircuitBreaker + Retry implementados em v3.1.0 e v3.2.0.
O SpeechToTextClient faz uma chamada bloqueante ao Whisper com .block(Duration.ofSeconds(30)).
Se o Speaches estiver lento, sobrecarregado ou offline:
Requisição do cliente
│
▼
SpeechToTextClient.transcribe()
│
│ .block(30s) ←── thread bloqueada por até 30 segundos
▼
Speaches (pode estar lento / offline)
│
│ timeout ou erro
▼
TranscriptionException("Falha na comunicação com Whisper")
│
▼
HTTP 400 ←── sem retry, sem fallback, sem circuit breaker
Sob carga, threads acumulam esperando o timeout → pool de threads esgotado → cascata de falhas.
Padrões aplicados em camadas sobre o SpeechToTextClient, na ordem de execução:
Requisição → TranscriptionService → SpeechToTextClient
│
┌────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ @Retry(name="whisper") ←── v3.2.0 │
│ maxAttempts=3 · backoff 500ms → 1s → 2s (exponencial, max 4s) │
│ retry apenas em TranscriptionException (erros de I/O) │
│ ignora ServiceUnavailableException + IllegalArgumentException │
└───────────────────────────────┬──────────────────────────────────┘
│ (se ainda falhar após 3 tentativas)
▼
┌──────────────────────────────────────────────────────────────────┐
│ @CircuitBreaker(name="whisper") ←── v3.1.0 │
│ │
│ CLOSED ──(≥50% falhas em 10 calls)──► OPEN │
│ ↑ │ │
│ └──(3 calls ok em HALF_OPEN)──◄── (30s) │
│ HALF_OPEN │
└───────────────────────────────┬──────────────────────────────────┘
│ (se OPEN → fallback imediato)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Fallback │
│ ServiceUnavailableException │
│ → GlobalExceptionHandler → HTTP 503 ProblemDetail │
└──────────────────────────────────────────────────────────────────┘
| Estado | Comportamento | Transição |
|---|---|---|
| CLOSED | Operação normal — chamadas passam para o Speaches | → OPEN se ≥ 50% das últimas 10 chamadas falharem (mín. 5) |
| OPEN | Circuito aberto — fallback imediato, sem chamar Speaches | → HALF_OPEN após 30s (prod) / 10s (dev) |
| HALF_OPEN | Sondagem — 3 chamadas de teste permitidas | → CLOSED se as 3 passarem; → OPEN se qualquer uma falhar |
resilience4j:
circuitbreaker:
instances:
whisper:
slidingWindowSize: 10 # prod | 5 em dev
failureRateThreshold: 50 # % de falhas para abrir
waitDurationInOpenState: 30s # prod | 10s em dev
permittedNumberOfCallsInHalfOpenState: 3
minimumNumberOfCalls: 5 # prod | 3 em dev
registerHealthIndicator: true # visível em /actuator/health
slowCallDurationThreshold: 10s # chamadas lentas = falha
slowCallRateThreshold: 80
retry:
instances:
whisper:
maxAttempts: 3 # prod | 2 em dev
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2 # 500ms → 1s → 2s
exponentialMaxWaitDuration: 4s
retryExceptions:
- TranscriptionException
ignoreExceptions:
- ServiceUnavailableException
- IllegalArgumentException
// Ordem importa: Retry executa primeiro, CircuitBreaker por baixo
@Retry(name = "whisper", fallbackMethod = "fallback")
@CircuitBreaker(name = "whisper", fallbackMethod = "fallback")
public WhisperResponse transcribe(MultipartFile file) { ... }
private WhisperResponse fallback(MultipartFile file, Exception ex) {
throw new ServiceUnavailableException(
"Serviço de transcrição temporariamente indisponível.");
}
Resilience4j integra com Micrometer automaticamente. Dashboard Grafana atualizado em v3.3.0 com a seção Resiliência.
| Painel | Métrica Prometheus |
|---|---|
| Estado do CB | resilience4j_circuitbreaker_state{name="whisper"} |
| Chamadas por tipo | resilience4j_circuitbreaker_calls_seconds_count{kind=...} |
| Retries | resilience4j_retry_calls_total{kind=...} |
# 1. Ver estado atual do CB
curl http://localhost:8080/actuator/health | jq .components.circuitBreakers
# 2. Forçar abertura do CB (derrubar Speaches e fazer requisições)
docker stop speaches
TOKEN=$(curl -s -X POST http://localhost:8080/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"user","password":"any"}' | jq -r .token)
# Fazer 5+ requisições — as primeiras retornam 400 (erro Whisper),
# depois do threshold retornam 503 (circuito aberto)
for i in {1..6}; do
echo -n "Req $i: "
curl -s -o /dev/null -w "%{http_code}" \
-X POST http://localhost:8080/api/transcriptions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@audio.wav;type=audio/wav"
echo ""
done
# 3. Restaurar Speaches — CB fecha após waitDurationInOpenState
docker start speaches