Quem conhece com Java e quer trabalhar com o padrão Circuit Breaker de Martin Fowler certamente encontrou uma dificuldade em encontrar um exemplo simples, ou era uma implementação complexa ou era já um exemplo com o Netflix Hystrix, que já tem o padrão pronto e implementado.
Por esse motivo eu implementei da maneira mais simples possível (fontes aqui) e sem usar nenhum framework, e além de testes para validar o ambiente.
A aplicação
Consiste em uma API simples com um método que recebe o nome de um país e retorna a população.
Aqui a ideia é que países pequenos (Itália e Japão) o serviço retorna um valor rápido (1 segundo) e um país grande (Brasil, 100 segundos), e o método de busca não funciona em paralelo, calcula um por vez.
O problema
Em um cenário que temos um SLA de 2 segundos para retornar um serviço, um processo lento certamente vai atrapalhar todos os restantes.
Uma maneira elegante de resolver esse problema é o Circuit Breaker, colocando para ele esse limite de 2 segundos de espera.
Aqui uma chamada no serviço /sem-nada/pais/pop/ (que não tem Circuit Breaker):
Aqui outra chamada no serviço /circuitbreaker/pais/pop/ (que tem Circuit Breaker):
O resultado é o mesmo, pois o resultado esperado estava dentro do limite de 5 segundos.
Vamos agora testar o serviço lento com os dados do Brasil:
Veja o mesmo serviço sendo chamado com o Circuit Breaker:
Os mocks da aplicação
Começando pelo banco de dados (classe DatabaseMock), definimos estaticamente os valores:
private static final int PROCESSO_LENTO = 100;
private static final int PROCESSO_RAPIDO = 1;
static Map<String, String> mapPaises =
Map.of("brasil", "210.147.125",
"japao", "126.440.000",
"italia", "60.665.551");
Em seguida temos um método synchronized para propositalmente enfileirar todas chamadas e simular uma lentidão de serviços.
A lógica é bem simples, se for passado o Brasil como parâmetro retorna o processo lento, caso contrário é o rápido:
public static synchronized String buscaPopulacao(String pais) { System.out.println("- inicio busca por " + pais); int tempoDeProcessamento = PROCESSO_RAPIDO; if (pais.equalsIgnoreCase("brasil")) { tempoDeProcessamento = PROCESSO_LENTO; } try { Thread.sleep(tempoDeProcessamento * 1000); } catch (InterruptedException e) { } System.out.println("- fim busca por " + pais); return mapPaises.get(pais.toLowerCase()); }
Chamada sem Circuit Breaker
Aqui na classe ConsultaPaises chamada usamos o método DatabaseMock.buscaPopulacao e o Instant para calcular o tempo gasto.
public class ConsultaPaises { @GetMapping("/pais/pop/{pais}") public ServiceResponse calculaPopulacao(@PathVariable String pais) { String status; String message; Instant start = Instant.now(); String populacao = DatabaseMock.buscaPopulacao(pais); if (populacao != null) { status = "Ok"; message = pais + " - population: " + populacao; } else { status = "Erro"; message = pais + " sem cadastro"; } Instant finish = Instant.now(); long timeElapsed = Duration.between(start, finish).toMillis(); ServiceResponse resposta = new ServiceResponse(message, status, timeElapsed); System.out.println("Sem nada: " + resposta); return resposta; }
Chamada com Circuit Breaker
A implementação do Circuit Breaker é bem simples e tem apenas duas posições: aberto (bloqueia chamadas) e fechado (libera chamadas).
Definimos nas constantes também o limite de 5 falhas para normalizar o circuito como fechado.
public class CircuitBreaker { public static final int INVOCATION_TIMEOUT = 2; static final int LIMITE_FALHA = 5; static int falhas = 0; static Posicao estado = Posicao.FECHADO;
Aqui a rotina quando passa o limite de 2 segundos, abrindo o circuito:
public static void passouLimite() {
falhas += 1;
estado = Posicao.ABERTO;
}
Depois de normalizar, zerando tudo:
public static void normalizou() {
falhas = 0;
estado = Posicao.FECHADO;
}
E finalmente a rotina principal, que retorna o status atual do circuito:
public static Posicao call() {
System.out.println("Verificando circuito...");
if (falhas > LIMITE_FALHA) {
System.out.println("- passou o limite, vamos tentar novamente...");
normalizou();
} else {
System.out.println("- falhas = " + falhas);
}
switch (estado) {
case ABERTO:
System.out.println("- Circuito aberto =(");
falhas++;
break;
case FECHADO:
System.out.println("- Circuito fechado =)");
break;
}
return estado;
}
Aqui na classe ConsultaPaisesComCircuitBreaker é uma cópia da ConsultaPaises , usamos CompletableFuture para fazer o timeout do serviço.
ExecutorService executor = Executors.newCachedThreadPool();
Callable<String> task = new Callable<String>() {
public String call() {
return DatabaseMock.buscaPopulacao(pais);
}
};
Future<String> future = executor.submit(task);
Aqui a parte principal, é verificado o status do circuito, se estiver aberto volta a mensagem de erro de sistema indisponível; se estiver fechado o método de buscaPopulacao é chamado com o timeout de 2 segundos. Se estourar o tempo, caimos na exception TimeoutException,e chamamos o método passouLimite, que abre o circuito e começa a contar as falhas.
try {
Posicao posicaoDoCircuito = CircuitBreaker.call();
switch (posicaoDoCircuito) {
case ABERTO:
circuitoAberto = true;
break;
case FECHADO:
populacao = future.get(CircuitBreaker.INVOCATION_TIMEOUT, TimeUnit.SECONDS);
break;
}
} catch (TimeoutException ex) {
System.out.println("=== Estourou o tempo limite ===");
CircuitBreaker.passouLimite();
Testes
A classe TesteSemCircuitBreaker chama todos os serviços e conseguimos simular uma carga e uma demora de mais de 8 minutos.
== Servicos lentos == [1/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [1/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [1/5] - Resposta: brasil - population: 210.147.125 - tempo gasto: 100000ms ou 100s [2/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [2/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [2/5] - Resposta: brasil - population: 210.147.125 - tempo gasto: 100000ms ou 100s [3/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [3/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [3/5] - Resposta: brasil - population: 210.147.125 - tempo gasto: 100000ms ou 100s [4/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [4/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [4/5] - Resposta: brasil - population: 210.147.125 - tempo gasto: 100000ms ou 100s [5/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [5/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [5/5] - Resposta: brasil - population: 210.147.125 - tempo gasto: 100000ms ou 100s Tempo total = 510000.0ms ou 510.0s ou 8.5min
Chamando os mesmos serviços com Circuit Breaker conseguimos manter o SLA de 2 segundos :
== Servicos lentos ==
[1/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1003ms ou 1s
[1/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s
[1/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 2002ms ou 2s
[2/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 1ms ou 0s
[2/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[2/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[3/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[3/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[3/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 2001ms ou 2s
[4/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 1ms ou 0s
[4/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[4/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[5/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[5/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s
[5/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 2000ms ou 2s
Tempo total = 8008.0ms ou 8.008s ou 0.13346666666666668min
E finalmente chamando apenas os serviços rápidos, depois de algumas tentativas o circuito é normalizado e o sistema volta a responder normalmente (sempre dentro do SLA de 2 segundos).
== Servicos rapidos == [1/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s [1/5] - Resposta: Sistema indisponível, tente mais tarde =( - tempo gasto: 0ms ou 0s [2/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1000ms ou 1s [2/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [3/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1001ms ou 1s [3/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1001ms ou 1s [4/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1001ms ou 1s [4/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1000ms ou 1s [5/5] - Resposta: japao - population: 126.440.000 - tempo gasto: 1002ms ou 1s [5/5] - Resposta: italia - population: 60.665.551 - tempo gasto: 1001ms ou 1s Tempo total = 4002.0ms ou 4.002s ou 0.0667min
Espero que com esse exemplo tenha ficado mais claro esse padrão tão usado e necessário no mundo de nossas APIs.
Fernando Boaglio, para a comunidade