Skip to content

Commit d439851

Browse files
committed
Improves API integration resilience
Adds exception handling for external API integrations, specifically for the Coordenadas API, to prevent cascading failures. Includes detailed logging for integration errors, including endpoint, error type, and transaction ID. Implements retry logic in the scheduler for coordenada fetching and adds timeout configurations to the WebClient to avoid indefinite waiting. Preserves existing data when API integration fails by preventing the saving of empty coordinate lists.
1 parent 9a7938b commit d439851

File tree

6 files changed

+127
-13
lines changed

6 files changed

+127
-13
lines changed

src/main/java/com/dmware/api_onibusbh/config/WebClientConfig.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package com.dmware.api_onibusbh.config;
22

3+
import io.netty.channel.ChannelOption;
4+
import io.netty.handler.timeout.ReadTimeoutHandler;
5+
import io.netty.handler.timeout.WriteTimeoutHandler;
36
import org.springframework.context.annotation.Bean;
47
import org.springframework.context.annotation.Configuration;
58
import org.springframework.http.HttpHeaders;
9+
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
610
import org.springframework.web.reactive.function.client.ExchangeStrategies;
711
import org.springframework.web.reactive.function.client.WebClient;
12+
import reactor.netty.http.client.HttpClient;
13+
14+
import java.time.Duration;
15+
import java.util.concurrent.TimeUnit;
816

917
@Configuration
1018
public class WebClientConfig {
@@ -16,10 +24,18 @@ public WebClient webClient() {
1624
.codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(size))
1725
.build();
1826

27+
HttpClient httpClient = HttpClient.create()
28+
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
29+
.responseTimeout(Duration.ofSeconds(15))
30+
.doOnConnected(conn -> conn
31+
.addHandlerLast(new ReadTimeoutHandler(15, TimeUnit.SECONDS))
32+
.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)));
33+
1934
return WebClient.builder()
2035
.defaultHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
2136
.defaultHeader(HttpHeaders.REFERER, "https://dados.pbh.gov.br/")
2237
.exchangeStrategies(strategies)
38+
.clientConnector(new ReactorClientHttpConnector(httpClient))
2339
.build();
2440
}
2541
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.dmware.api_onibusbh.exceptions;
2+
3+
public class CoordenadasApiIntegrationException extends RuntimeException {
4+
private final String endpoint;
5+
private final String errorType;
6+
7+
public CoordenadasApiIntegrationException(String message, String endpoint, String errorType, Throwable cause) {
8+
super(message, cause);
9+
this.endpoint = endpoint;
10+
this.errorType = errorType;
11+
}
12+
13+
public CoordenadasApiIntegrationException(String endpoint, String errorType, Throwable cause) {
14+
super("Falha na integração com API externa de coordenadas");
15+
this.endpoint = endpoint;
16+
this.errorType = errorType;
17+
}
18+
19+
public String getEndpoint() {
20+
return endpoint;
21+
}
22+
23+
public String getErrorType() {
24+
return errorType;
25+
}
26+
}

src/main/java/com/dmware/api_onibusbh/infra/CustomExceptionHandler.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static net.logstash.logback.argument.StructuredArguments.kv;
1919

20+
import com.dmware.api_onibusbh.exceptions.CoordenadasApiIntegrationException;
21+
2022
@ControllerAdvice
2123
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
2224

@@ -168,4 +170,21 @@ public ResponseEntity<ErrorResponse> rateLimitExceededException(RateLimitExceede
168170
.body(new ErrorResponse(java.time.LocalDateTime.now(), ex.getMessage(), HttpStatus.TOO_MANY_REQUESTS));
169171
}
170172

173+
@ExceptionHandler(CoordenadasApiIntegrationException.class)
174+
public ResponseEntity<ErrorResponse> coordenadasApiIntegrationException(CoordenadasApiIntegrationException ex, HttpServletRequest request) {
175+
String clientIp = ClientIpUtils.getClientIp(request);
176+
logger.error("Falha na integração com API externa | {} {} | IP: {} | Endpoint: {} | Tipo: {} | Mensagem: {}",
177+
request.getMethod(), request.getRequestURI(), clientIp, ex.getEndpoint(), ex.getErrorType(), ex.getMessage(),
178+
kv("exception", ex.getClass().getSimpleName()),
179+
kv("status", HttpStatus.SERVICE_UNAVAILABLE),
180+
kv("endpoint", ex.getEndpoint()),
181+
kv("error_type", ex.getErrorType()),
182+
kv("detail", ex.getMessage()),
183+
kv("path", request.getRequestURI()),
184+
kv("method", request.getMethod()),
185+
kv("client_ip", clientIp),
186+
ex);
187+
return ErrorResponse.of("Falha na comunicação com serviço externo. Os dados existentes foram preservados.", HttpStatus.SERVICE_UNAVAILABLE);
188+
}
189+
171190
}

src/main/java/com/dmware/api_onibusbh/scheduler/CoordenadasScheduler.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dmware.api_onibusbh.scheduler;
22

33
import com.dmware.api_onibusbh.dto.CoordenadaDTO;
4+
import com.dmware.api_onibusbh.exceptions.CoordenadasApiIntegrationException;
45
import com.dmware.api_onibusbh.services.APIService;
56
import com.dmware.api_onibusbh.services.OnibusService;
67
import org.slf4j.MDC;
@@ -28,16 +29,39 @@ public CoordenadasScheduler(APIService apiService, OnibusService onibusService)
2829
@Scheduled(fixedDelay = 20, timeUnit = TimeUnit.SECONDS)
2930
public void fetchCoordenadasOnibus() {
3031
long startTime = System.currentTimeMillis();
32+
String transactionId = UUID.randomUUID().toString();
3133
try {
32-
MDC.put("transaction_id", UUID.randomUUID().toString());
34+
MDC.put("transaction_id", transactionId);
3335
logger.info("Job de Coordenadas iniciado.", kv("job_name", "Coordenadas"), kv("status", "STARTED"));
3436
List<CoordenadaDTO> coordenadas = apiService.getOnibusCoordenadaBH();
3537
onibusService.salvaCoordenadas(coordenadas);
3638
long duration = System.currentTimeMillis() - startTime;
3739
logger.info("Job de Coordenadas finalizado | Tempo: {}ms", duration,
3840
kv("job_name", "Coordenadas"),
3941
kv("status", "FINISHED"),
40-
kv("duration_ms", duration));
42+
kv("duration_ms", duration),
43+
kv("transaction_id", transactionId));
44+
} catch (CoordenadasApiIntegrationException e) {
45+
long duration = System.currentTimeMillis() - startTime;
46+
logger.error("Falha na integração com API externa. Coordenadas preservadas. Tentando novamente em 20s.",
47+
kv("job_name", "Coordenadas"),
48+
kv("status", "FAILED"),
49+
kv("error_type", "API_INTEGRATION"),
50+
kv("endpoint", e.getEndpoint()),
51+
kv("error_detail", e.getErrorType()),
52+
kv("duration_ms", duration),
53+
kv("transaction_id", transactionId),
54+
kv("next_retry_seconds", 20));
55+
} catch (Exception e) {
56+
long duration = System.currentTimeMillis() - startTime;
57+
logger.error("Erro inesperado no job de coordenadas. Tentando novamente em 20s.", e,
58+
kv("job_name", "Coordenadas"),
59+
kv("status", "FAILED"),
60+
kv("error_type", "UNEXPECTED"),
61+
kv("error_class", e.getClass().getSimpleName()),
62+
kv("duration_ms", duration),
63+
kv("transaction_id", transactionId),
64+
kv("next_retry_seconds", 20));
4165
} finally {
4266
MDC.clear();
4367
}

src/main/java/com/dmware/api_onibusbh/services/APIService.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.dmware.api_onibusbh.dto.CoordenadaDTO;
44
import com.dmware.api_onibusbh.dto.DicionarioDTO;
55
import com.dmware.api_onibusbh.dto.LinhaDTO;
6+
import com.dmware.api_onibusbh.exceptions.CoordenadasApiIntegrationException;
67
import com.fasterxml.jackson.core.JsonProcessingException;
78
import com.fasterxml.jackson.core.type.TypeReference;
89
import com.fasterxml.jackson.databind.JsonNode;
@@ -18,6 +19,8 @@
1819
import java.util.ArrayList;
1920
import java.util.List;
2021

22+
import static net.logstash.logback.argument.StructuredArguments.kv;
23+
2124
@Service
2225
public class APIService {
2326

@@ -32,11 +35,16 @@ public APIService(WebClient webClient, ObjectMapper objectMapper) {
3235
}
3336

3437
public List<DicionarioDTO> getDicionarioAPIBH() {
38+
String endpoint = "ckan.pbh.gov.br/dicionario";
3539
try {
3640
String json = webClient.get()
3741
.uri("https://ckan.pbh.gov.br/api/3/action/datastore_search?resource_id=825337e5-8cd5-43d9-ac52-837d80346721&limit=20")
3842
.retrieve()
3943
.bodyToMono(String.class)
44+
.onErrorResume(e -> {
45+
logger.error("Erro ao buscar dicionário da API BH", kv("endpoint", endpoint), kv("error", e.getMessage()));
46+
return Mono.error(new CoordenadasApiIntegrationException("Falha ao buscar dicionário", endpoint, e.getClass().getSimpleName(), e));
47+
})
4048
.block();
4149

4250
JsonNode rootNode = objectMapper.readTree(json);
@@ -45,17 +53,23 @@ public List<DicionarioDTO> getDicionarioAPIBH() {
4553
return objectMapper.convertValue(recordsNode, new TypeReference<List<DicionarioDTO>>() {
4654
});
4755

48-
} catch (JsonProcessingException | WebClientResponseException e) {
49-
throw new RuntimeException(e);
56+
} catch (JsonProcessingException e) {
57+
logger.error("Erro ao processar JSON do dicionário", kv("endpoint", endpoint), kv("error", e.getMessage()));
58+
throw new CoordenadasApiIntegrationException("Erro ao processar resposta do dicionário", endpoint, "JsonProcessingException", e);
5059
}
5160
}
5261

5362
public List<LinhaDTO> getLinhasAPIBH() {
63+
String endpoint = "ckan.pbh.gov.br/linhas";
5464
try {
5565
String json = webClient.get()
5666
.uri("https://ckan.pbh.gov.br/api/3/action/datastore_search?resource_id=150bddd0-9a2c-4731-ade9-54aa56717fb6&limit=3000")
5767
.retrieve()
5868
.bodyToMono(String.class)
69+
.onErrorResume(e -> {
70+
logger.error("Erro ao buscar linhas da API BH", kv("endpoint", endpoint), kv("error", e.getMessage()));
71+
return Mono.error(new CoordenadasApiIntegrationException("Falha ao buscar linhas", endpoint, e.getClass().getSimpleName(), e));
72+
})
5973
.block();
6074

6175
JsonNode rootNode = objectMapper.readTree(json);
@@ -64,16 +78,25 @@ public List<LinhaDTO> getLinhasAPIBH() {
6478
return objectMapper.convertValue(recordsNode, new TypeReference<List<LinhaDTO>>() {
6579
});
6680

67-
} catch (JsonProcessingException | WebClientResponseException e) {
68-
throw new RuntimeException(e);
81+
} catch (JsonProcessingException e) {
82+
logger.error("Erro ao processar JSON das linhas", kv("endpoint", endpoint), kv("error", e.getMessage()));
83+
throw new CoordenadasApiIntegrationException("Erro ao processar resposta das linhas", endpoint, "JsonProcessingException", e);
6984
}
7085
}
7186

7287
public List<CoordenadaDTO> getOnibusCoordenadaBH() {
7388
List<String> responses = fetchCoordenadasDirectly();
7489

7590
if (responses == null || responses.size() < 2) {
76-
throw new IllegalStateException("Não foi possível obter resposta de um ou mais endpoints.");
91+
throw new CoordenadasApiIntegrationException("Resposta incompleta das APIs de coordenadas", "temporeal.pbh.gov.br", "IncompleteResponse", null);
92+
}
93+
94+
String jsonD = responses.get(0);
95+
String jsonSD = responses.get(1);
96+
97+
if ("[]".equals(jsonD) && "[]".equals(jsonSD)) {
98+
logger.warn("Ambas as APIs de coordenadas retornaram vazio");
99+
throw new CoordenadasApiIntegrationException("APIs de coordenadas retornaram dados vazios", "temporeal.pbh.gov.br", "EmptyResponse", null);
77100
}
78101

79102
return processAndReturnCoordenadas(responses);
@@ -85,17 +108,17 @@ private List<String> fetchCoordenadasDirectly() {
85108
.retrieve()
86109
.bodyToMono(String.class)
87110
.onErrorResume(e -> {
88-
logger.error("Erro ao buscar coordenadas D", e);
89-
return Mono.just("[]");
111+
logger.error("Erro ao buscar coordenadas D", kv("param", "D"), kv("error", e.getMessage()));
112+
return Mono.error(new CoordenadasApiIntegrationException("Falha ao buscar coordenadas D", "temporeal.pbh.gov.br/?param=D", e.getClass().getSimpleName(), e));
90113
});
91114

92115
Mono<String> monoParamSD = webClient.get()
93116
.uri("https://temporeal.pbh.gov.br/?param=SD")
94117
.retrieve()
95118
.bodyToMono(String.class)
96119
.onErrorResume(e -> {
97-
logger.error("Erro ao buscar coordenadas SD", e);
98-
return Mono.just("[]");
120+
logger.error("Erro ao buscar coordenadas SD", kv("param", "SD"), kv("error", e.getMessage()));
121+
return Mono.error(new CoordenadasApiIntegrationException("Falha ao buscar coordenadas SD", "temporeal.pbh.gov.br/?param=SD", e.getClass().getSimpleName(), e));
99122
});
100123

101124
return Flux.mergeSequential(monoParamD, monoParamSD).collectList().block();
@@ -119,7 +142,7 @@ private List<CoordenadaDTO> processAndReturnCoordenadas(List<String> responses)
119142
return todasCoordenadas;
120143

121144
} catch (JsonProcessingException e) {
122-
throw new RuntimeException("Erro ao processar o JSON das coordenadas.", e);
145+
throw new CoordenadasApiIntegrationException("Erro ao processar JSON das coordenadas", "temporeal.pbh.gov.br", "JsonProcessingException", e);
123146
}
124147
}
125-
}
148+
}

src/main/java/com/dmware/api_onibusbh/services/OnibusService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public List<CoordenadaDTO> listarPorNumeroLinha(Integer numeroLinha, Optional<In
8686
@CacheEvict(value = {"onibus", "onibusPorLinha"}, allEntries = true)
8787
public void salvaCoordenadas(List<CoordenadaDTO> todasCoordenadasNovas) {
8888
logger.info("Iniciando o salvamento das coordenadas...");
89+
90+
if (todasCoordenadasNovas == null || todasCoordenadasNovas.isEmpty()) {
91+
logger.error("Lista de coordenadas está vazia ou nula. Abortando salvamento para preservar dados existentes.");
92+
return;
93+
}
94+
8995
List<LinhaEntity> linhasExistentes;
9096
try {
9197
linhasExistentes = linhasRepository.findAll();

0 commit comments

Comments
 (0)