Category Archives: Engenharia de Software

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


Arquivos