BAEL-5798: Guide to Resilience4j with Spring Boot (#12854)

Co-authored-by: Tapan Avasthi <tavasthi@Tapans-MacBook-Air.local>
This commit is contained in:
Tapan Avasthi
2022-10-17 08:03:41 +05:30
committed by GitHub
parent 6da922c20a
commit 58b522ec56
8 changed files with 359 additions and 0 deletions
@@ -0,0 +1,35 @@
package com.baeldung.resilientapp;
import java.util.concurrent.TimeoutException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import io.github.resilience4j.bulkhead.BulkheadFullException;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler({ CallNotPermittedException.class })
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public void handleCallNotPermittedException() {
}
@ExceptionHandler({ TimeoutException.class })
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public void handleTimeoutException() {
}
@ExceptionHandler({ BulkheadFullException.class })
@ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
public void handleBulkheadFullException() {
}
@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public void handleRequestNotPermitted() {
}
}
@@ -0,0 +1,28 @@
package com.baeldung.resilientapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class ExternalAPICaller {
private final RestTemplate restTemplate;
@Autowired
public ExternalAPICaller(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String callApi() {
return restTemplate.getForObject("/api/external", String.class);
}
public String callApiWithDelay() {
String result = restTemplate.getForObject("/api/external", String.class);
try {
Thread.sleep(5000);
} catch (InterruptedException ignore) {
}
return result;
}
}
@@ -0,0 +1,15 @@
package com.baeldung.resilientapp;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ExternalApiCallerConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder().rootUri("http://localhost:9090")
.build();
}
}
@@ -0,0 +1,14 @@
package com.baeldung.resilientapp;
import org.jobrunr.autoconfigure.JobRunrAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(exclude = { JobRunrAutoConfiguration.class})
public class ResilientApp {
public static void main(String[] args) {
SpringApplication.run(ResilientApp.class, args);
}
}
@@ -0,0 +1,62 @@
package com.baeldung.resilientapp;
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
@RestController
@RequestMapping("/api/")
public class ResilientAppController {
private final ExternalAPICaller externalAPICaller;
@Autowired
public ResilientAppController(ExternalAPICaller externalApi) {
this.externalAPICaller = externalApi;
}
@GetMapping("/circuit-breaker")
@CircuitBreaker(name = "CircuitBreakerService")
public String circuitBreakerApi() {
return externalAPICaller.callApi();
}
@GetMapping("/retry")
@Retry(name = "retryApi", fallbackMethod = "fallbackAfterRetry")
public String retryApi() {
return externalAPICaller.callApi();
}
@GetMapping("/time-limiter")
@TimeLimiter(name = "timeLimiterApi")
public CompletableFuture<String> timeLimiterApi() {
return CompletableFuture.supplyAsync(externalAPICaller::callApiWithDelay);
}
@GetMapping("/bulkhead")
@Bulkhead(name = "bulkheadApi")
public String bulkheadApi() {
return externalAPICaller.callApi();
}
@GetMapping("/rate-limiter")
@RateLimiter(name = "rateLimiterApi")
public String rateLimitApi() {
return externalAPICaller.callApi();
}
public String fallbackAfterRetry(Exception ex) {
return "all retries have exhausted";
}
}
@@ -1,3 +1,50 @@
org.jobrunr.background-job-server.enabled=true
org.jobrunr.dashboard.enabled=true
org.jobrunr.dashboard.port=0
logging.level.root=DEBUG
management.health.circuitbreakers.enabled=true
management.health.ratelimiters.enabled=true
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.jackson.serialization.indent-output=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.CircuitBreakerService.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.CircuitBreakerService.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.wait-duration-in-open-state=5s
resilience4j.circuitbreaker.instances.CircuitBreakerService.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-size=10
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-type=count_based
resilience4j.circuitbreaker.metrics.enabled=true
resilience4j.circuitbreaker.metrics.legacy.enabled=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.register-health-indicator=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.event-consumer-buffer-size=10
resilience4j.retry.instances.retryApi.max-attempts=3
resilience4j.retry.instances.retryApi.wait-duration=1s
resilience4j.retry.metrics.legacy.enabled=true
resilience4j.retry.metrics.enabled=true
resilience4j.timelimiter.metrics.enabled=true
resilience4j.timelimiter.instances.timeLimiterApi.timeout-duration=2s
resilience4j.timelimiter.instances.timeLimiterApi.cancel-running-future=true
resilience4j.bulkhead.metrics.enabled=true
resilience4j.bulkhead.instances.bulkheadApi.max-concurrent-calls=3
resilience4j.bulkhead.instances.bulkheadApi.max-wait-duration=1
resilience4j.ratelimiter.metrics.enabled=true
resilience4j.ratelimiter.instances.rateLimiterApi.register-health-indicator=true
resilience4j.ratelimiter.instances.rateLimiterApi.limit-for-period=5
resilience4j.ratelimiter.instances.rateLimiterApi.limit-refresh-period=60s
resilience4j.ratelimiter.instances.rateLimiterApi.timeout-duration=0s
resilience4j.ratelimiter.instances.rateLimiterApi.allow-health-indicator-to-fail=true
resilience4j.ratelimiter.instances.rateLimiterApi.subscribe-for-events=true
resilience4j.ratelimiter.instances.rateLimiterApi.event-consumer-buffer-size=50