Quiz
Clique em "Resposta" para revelar a resposta correta e a explicação.
Falhas em cascata e timeouts
Q1. O order tem 50 threads. Os pagamentos chegam a 20 por segundo e o payment de repente leva 3 s para responder. O GET /orders/{id} não chama o payment. O que acontece com ele, e por quê?
- A. Nada: ele não depende do
payment - B. Ele também fica lento e estoura o timeout: seriam necessárias 20/s × 3 s = 60 threads só para esperar o
payment, mais que as 50 disponíveis, então toda requisição vai para a fila - C. Ele fica mais rápido, porque os pagamentos são lentos e deixam a CPU ociosa
- D. Ele só falha se o banco de dados também estiver lento
Resposta
B — esgotamento de threads: uma falha em cascata.
Pela Lei de Little, concorrência = taxa de chegada × tempo no sistema. Chamadas que esperam uma dependência lenta seguram threads; quando o pool se esgota, requisições que nunca tocam a dependência ficam na fila atrás delas. O cenário browse do k6 existe para tornar esse dano colateral visível.
Q2. Por que um timeout é a primeira proteção a adicionar?
- A. Ele deixa a dependência mais rápida
- B. Ele transforma uma falha lenta numa falha rápida: cada chamada segura uma thread por no máximo o timeout, o que limita a concorrência gasta naquela dependência
- C. Ele faz retry automaticamente
- D. Ele é exigido pelo HTTP/1.1
Resposta
B — espera limitada.
Com um timeout de leitura de 1 s, 20 pagamentos/s seguram no máximo cerca de 20 threads, por mais lento que o payment fique. O preço: chamadas que teriam dado certo em 1,5 s agora falham. Os clientes HTTP Java esperam para sempre por padrão, então a falta de um timeout é a causa raiz mais comum das cascatas.
Q3. A borda estoura o timeout depois de 5 s, o gateway depois de 10 s, e o order espera o payment por até 3 retries × 4 s. O que está errado?
- A. Nada; timeouts internos mais longos são mais seguros
- B. Os orçamentos internos excedem os externos: a borda desiste enquanto o
ordere o gateway continuam trabalhando em requisições que ninguém está esperando. Os orçamentos precisam encolher a cada salto - C. O timeout do gateway deveria ser 0
- D. Os retries deveriam ser infinitos para garantir o sucesso
Resposta
B — os orçamentos de timeout precisam diminuir de fora para dentro.
Trabalho feito depois que quem chamou desistiu é puro desperdício — e segura threads e conexões. Um orçamento consistente é algo como borda 10 s > gateway 5 s > order→payment (1 s × 3 tentativas + backoff).
Retries e idempotência
Q4. Uma tentativa de chamar o payment falha com probabilidade 0,2, de forma independente. Com max-attempts: 3, qual é a probabilidade de um pagamento falhar?
- A. 0,6
- B. 0,2
- C. 0,008
- D. 0,04
Resposta
C — 0,2³ = 0,008 (0,8%).
Uma chamada só falha se as três tentativas falharem. Retries são muito eficazes contra erros independentes e transitórios — e inúteis (ou prejudiciais) contra erros persistentes: se o payment está fora do ar, três tentativas só triplicam a carga e a latência.
Q5. Por que o PaymentClient envia Idempotency-Key: order-<id> em toda tentativa?
- A. Para autenticar o
orderjunto aopayment - B. Para que o retry de uma chamada cuja resposta se perdeu (mas que deu certo) não cobre o fã duas vezes: o
paymente o provedor devolvem o primeiro resultado para uma chave conhecida - C. Para fazer o circuit breaker contar chamadas por pedido
- D. Porque o HTTP a exige para
POST
Resposta
B — retries só são seguros em operações idempotentes.
Um timeout não significa que a chamada falhou — a cobrança pode ter dado certo e só a resposta se perdeu. Sem uma chave de idempotência, o retry cria uma segunda cobrança. Com ela, a segunda requisição encontra a chave (UNIQUE em payments, repassada ao provedor) e recebe o resultado original.
Q6. Que problema o jitter (espera aleatória) resolve no backoff dos retries?
- A. Ele reduz o número de retries
- B. Ele impede que clientes que falharam no mesmo momento tentem de novo no mesmo momento, o que criaria ondas sincronizadas de carga sobre uma dependência em recuperação
- C. Ele deixa os timeouts mais curtos
- D. Ele criptografa as requisições de retry
Resposta
B — dessincronizar os retries.
O backoff exponencial sozinho espalha as tentativas no tempo, mas todos os clientes seguem o mesmo cronograma: uma queda que atinge 1.000 clientes de uma vez produz ondas de 1.000 retries em +200 ms, +400 ms… Adicionar ±50% de aleatoriedade transforma as ondas num fluxo mais suave. O cenário retry-storm do playground mostra a amplificação.
Circuit breaker e bulkhead
Q7. O circuit breaker tem minimum-number-of-calls: 10, failure-rate-threshold: 50 e wait-duration-in-open-state: 10s. O payment cai. O que o breaker faz?
- A. Tenta de novo para sempre até o
paymentvoltar - B. Depois de pelo menos 10 chamadas registradas com ≥ 50% de falhas, ele abre: por 10 s as chamadas falham imediatamente com
CallNotPermittedException; depois ele passa a half-open e deixa passar algumas chamadas de teste para decidir se fecha - C. Reinicia o
payment - D. Envia todas as chamadas para uma instância reserva do
payment
Resposta
B — fechado → aberto → half-open → fechado (ou aberto de novo).
O estado aberto poupa as threads e o tempo de quem chama, e dá espaço para a dependência se recuperar em vez de uma enxurrada de chamadas condenadas. As sondas do half-open detectam a recuperação automaticamente. Um breaker não conserta nada — ele limita o estrago.
Q8. Por que a CallNotPermittedException está nas ignore-exceptions do retry?
- A. Porque o Resilience4j tecnicamente não consegue fazer retry dela
- B. Porque, quando o circuito está aberto, tentar de novo imediatamente é inútil e só adiciona latência; a chamada deve falhar rápido para o fallback
- C. Porque ela é uma checked exception
- D. Porque o bulkhead cuida dela
Resposta
B — um circuito aberto é uma decisão, não um erro transitório.
Com a ordem padrão dos aspectos, Retry(CircuitBreaker(Bulkhead(call))), o retry vê a rejeição do breaker. Repeti-la só juntaria três rejeições e os atrasos do backoff. Ignorá-la manda a requisição direto para o fallback — rápido e honesto.
Q9. As chamadas ao payment dão certo, mas levam 700 ms, abaixo tanto do timeout de leitura quanto do limite de chamada lenta. A 70 pagamentos/s, as 50 threads do order ficam quase todas ocupadas e o GET /orders/{id} fica lento. Que padrão resolve isso?
- A. Um timeout menor
- B. Mais retries
- C. Um bulkhead: limitar as chamadas concorrentes ao
payment(por exemplo, 10), rejeitando o excedente imediatamente para que o resto das threads continue servindo os outros endpoints - D. Uma janela maior no circuit breaker
Resposta
C — um bulkhead limita a concorrência que uma dependência pode usar.
Nada está falhando, então nem o timeout nem o breaker reagem. 70/s × 0,7 s ≈ 49 chamadas concorrentes enchem o pool. O bulkhead reserva a capacidade restante para todo o resto; alguns pagamentos recebem um 503 rápido, o que é melhor que o serviço inteiro ficar lento.
Q10. No PaymentGateway, por que o fallbackMethod está no @Retry e não no @CircuitBreaker?
- A. Porque o
@CircuitBreakernão suporta fallbacks - B. Porque o Retry é o aspecto mais externo: um fallback no circuit breaker interno converteria toda falha na exceção do fallback antes que o retry pudesse ver — e repetir — o erro transitório original
- C. Porque os fallbacks precisam estar na primeira anotação do arquivo-fonte
- D. Não há diferença
Resposta
B — o fallback pertence à camada mais externa.
A ordem padrão do Resilience4j é Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(call))))). Um fallback substitui a exceção da camada que ele decora. No breaker interno, uma ResourceAccessException viraria PaymentUnavailableException, que não está em retry-exceptions: o retry pararia de funcionar silenciosamente.
Caos e game days
Q11. Por que o OrderService.pay chama o payment fora de uma transação de banco?
- A. Porque o JDBC não permite chamadas HTTP dentro de transações
- B. Porque uma transação segura uma conexão do banco: esperar 3 s pelo
paymentdentro dela levaria o esgotamento das threads do Tomcat para o pool de conexões, afetando toda consulta doorder - C. Para deixar o pagamento mais rápido
- D. Porque o Resilience4j exige isso
Resposta
B — nunca segure recursos escassos enquanto espera uma chamada remota.
O pool tem ~10 conexões; alguns pagamentos lentos dentro de transações deixariam todas as outras consultas sem conexão. A chamada remota roda primeiro, depois uma transação curta atualiza o status (o WHERE status = 'PENDING_PAYMENT' a mantém idempotente) e grava o evento do outbox.
Q12. Qual é o propósito da hipótese de estado estável num game day?
- A. Provar que o sistema nunca falha
- B. Declarar, antes de injetar uma falha, o comportamento mensurável que o sistema deve manter (por exemplo, p95 do
browse< 200 ms enquanto opaymentestá lento), para que o resultado seja uma confirmação ou refutação clara - C. Agendar o game day para quando o tráfego estiver estável
- D. Decidir quem é o culpado pelas falhas
Resposta
B — sem hipótese, não há experimento.
Engenharia do caos é um experimento: linha de base, hipótese, injeção, observação, conclusão. Uma hipótese refutada (o sistema não se comportou como projetado) é o resultado mais valioso — encontrado numa terça à tarde em vez de durante a próxima venda relâmpago. A revisão é sem culpados: ela corrige sistemas e processos, não pessoas.