Categorias
Engenharia de Software Java

Circuit Breaker basico em Java

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

Por Fernando Boaglio