BAEL-6071: Resilience4j events endpoints (#13697)

* Add project for Resilience4j Events Endpoints article

* Update README

* Add spring-boot-resilience4j module

* Renamed test class to meet constraints

* Update package structure

* Added formatting

* Replaces .parallel() with Executor

* Updated concurrent calls for bulkhead test
This commit is contained in:
Adrian Bob
2023-04-04 15:49:54 +02:00
committed by GitHub
parent 9df061c513
commit 745e372844
20 changed files with 957 additions and 0 deletions
+1
View File
@@ -93,6 +93,7 @@
<module>spring-boot-3-native</module>
<module>spring-boot-3-observation</module>
<module>spring-boot-3-test-pitfalls</module>
<module>spring-boot-resilience4j</module>
</modules>
<dependencyManagement>
@@ -0,0 +1,3 @@
### Relevant Articles:
- [Resilience4j Events Endpoints](https://www.baeldung.com/resilience4j-events-endpoints)
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-resilience4j</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-resilience4j</name>
<parent>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>spring-boot-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,29 @@
package com.baeldung.resilience4j.eventendpoints;
import io.github.resilience4j.bulkhead.BulkheadFullException;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
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;
@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.resilience4j.eventendpoints;
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,14 @@
package com.baeldung.resilience4j.eventendpoints;
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,12 @@
package com.baeldung.resilience4j.eventendpoints;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication()
public class ResilientApp {
public static void main(String[] args) {
SpringApplication.run(ResilientApp.class, args);
}
}
@@ -0,0 +1,58 @@
package com.baeldung.resilience4j.eventendpoints;
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;
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;
@RestController
@RequestMapping("/api/")
public class ResilientAppController {
private final ExternalAPICaller externalAPICaller;
@Autowired
public ResilientAppController(ExternalAPICaller externalApi) {
this.externalAPICaller = externalApi;
}
@GetMapping("/circuit-breaker")
@CircuitBreaker(name = "externalService")
public String circuitBreakerApi() {
return externalAPICaller.callApi();
}
@GetMapping("/retry")
@Retry(name = "externalService", fallbackMethod = "fallbackAfterRetry")
public String retryApi() {
return externalAPICaller.callApi();
}
@GetMapping("/bulkhead")
@Bulkhead(name = "externalService")
public String bulkheadApi() {
return externalAPICaller.callApi();
}
@GetMapping("/rate-limiter")
@RateLimiter(name = "externalService")
public String rateLimitApi() {
return externalAPICaller.callApi();
}
@GetMapping("/time-limiter")
@TimeLimiter(name = "externalService")
public CompletableFuture<String> timeLimiterApi() {
return CompletableFuture.supplyAsync(externalAPICaller::callApiWithDelay);
}
public String fallbackAfterRetry(Exception ex) {
return "all retries have exhausted";
}
}
@@ -0,0 +1,60 @@
management:
endpoints:
web:
exposure:
include: '*'
resilience4j.circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 50
instances:
externalService:
baseConfig: default
resilience4j.retry:
configs:
default:
maxAttempts: 3
waitDuration: 100
instances:
externalService:
baseConfig: default
resilience4j.timelimiter:
configs:
default:
cancelRunningFuture: true
timeoutDuration: 2s
instances:
externalService:
baseConfig: default
resilience4j.bulkhead:
configs:
default:
max-concurrent-calls: 3
max-wait-duration: 1
instances:
externalService:
baseConfig: default
resilience4j.ratelimiter:
configs:
default:
limit-for-period: 5
limit-refresh-period: 60s
timeout-duration: 0s
allow-health-indicator-to-fail: true
subscribe-for-events: true
event-consumer-buffer-size: 50
instances:
externalService:
baseConfig: default
@@ -0,0 +1,50 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.time.ZonedDateTime;
import java.util.Objects;
public class BulkheadEvent {
private String bulkheadName;
private String type;
private ZonedDateTime creationTime;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BulkheadEvent that = (BulkheadEvent) o;
return Objects.equals(bulkheadName, that.bulkheadName)
&& Objects.equals(type, that.type)
&& Objects.equals(creationTime, that.creationTime);
}
@Override
public int hashCode() {
return Objects.hash(bulkheadName, type, creationTime);
}
public String getBulkheadName() {
return bulkheadName;
}
public void setBulkheadName(String bulkheadName) {
this.bulkheadName = bulkheadName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ZonedDateTime getCreationTime() {
return creationTime;
}
public void setCreationTime(ZonedDateTime creationTime) {
this.creationTime = creationTime;
}
}
@@ -0,0 +1,16 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.util.List;
public class BulkheadEvents {
private List<BulkheadEvent> bulkheadEvents;
public List<BulkheadEvent> getBulkheadEvents() {
return bulkheadEvents;
}
public void setBulkheadEvents(List<BulkheadEvent> bulkheadEvents) {
this.bulkheadEvents = bulkheadEvents;
}
}
@@ -0,0 +1,81 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.time.ZonedDateTime;
import java.util.Objects;
public class CircuitBreakerEvent {
private String circuitBreakerName;
private String type;
private ZonedDateTime creationTime;
private String errorMessage;
private Integer durationInMs;
private String stateTransition;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CircuitBreakerEvent that = (CircuitBreakerEvent) o;
return Objects.equals(circuitBreakerName, that.circuitBreakerName)
&& Objects.equals(type, that.type)
&& Objects.equals(creationTime, that.creationTime)
&& Objects.equals(errorMessage, that.errorMessage)
&& Objects.equals(durationInMs, that.durationInMs)
&& Objects.equals(stateTransition, that.stateTransition);
}
@Override
public int hashCode() {
return Objects.hash(
circuitBreakerName, type, creationTime, errorMessage, durationInMs, stateTransition);
}
public String getCircuitBreakerName() {
return circuitBreakerName;
}
public void setCircuitBreakerName(String circuitBreakerName) {
this.circuitBreakerName = circuitBreakerName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ZonedDateTime getCreationTime() {
return creationTime;
}
public void setCreationTime(ZonedDateTime creationTime) {
this.creationTime = creationTime;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public Integer getDurationInMs() {
return durationInMs;
}
public void setDurationInMs(Integer durationInMs) {
this.durationInMs = durationInMs;
}
public String getStateTransition() {
return stateTransition;
}
public void setStateTransition(String stateTransition) {
this.stateTransition = stateTransition;
}
}
@@ -0,0 +1,16 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.util.List;
public class CircuitBreakerEvents {
private List<CircuitBreakerEvent> circuitBreakerEvents;
public List<CircuitBreakerEvent> getCircuitBreakerEvents() {
return circuitBreakerEvents;
}
public void setCircuitBreakerEvents(List<CircuitBreakerEvent> circuitBreakerEvents) {
this.circuitBreakerEvents = circuitBreakerEvents;
}
}
@@ -0,0 +1,50 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.time.ZonedDateTime;
import java.util.Objects;
public class RateLimiterEvent {
private String rateLimiterName;
private String type;
private ZonedDateTime creationTime;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RateLimiterEvent that = (RateLimiterEvent) o;
return Objects.equals(rateLimiterName, that.rateLimiterName)
&& Objects.equals(type, that.type)
&& Objects.equals(creationTime, that.creationTime);
}
@Override
public int hashCode() {
return Objects.hash(rateLimiterName, type, creationTime);
}
public String getRateLimiterName() {
return rateLimiterName;
}
public void setRateLimiterName(String rateLimiterName) {
this.rateLimiterName = rateLimiterName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ZonedDateTime getCreationTime() {
return creationTime;
}
public void setCreationTime(ZonedDateTime creationTime) {
this.creationTime = creationTime;
}
}
@@ -0,0 +1,16 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.util.List;
public class RateLimiterEvents {
private List<RateLimiterEvent> rateLimiterEvents;
public List<RateLimiterEvent> getRateLimiterEvents() {
return rateLimiterEvents;
}
public void setRateLimiterEvents(List<RateLimiterEvent> rateLimiterEvents) {
this.rateLimiterEvents = rateLimiterEvents;
}
}
@@ -0,0 +1,70 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.time.ZonedDateTime;
import java.util.Objects;
public class RetryEvent {
private String retryName;
private String type;
private ZonedDateTime creationTime;
private String errorMessage;
private Integer numberOfAttempts;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RetryEvent that = (RetryEvent) o;
return Objects.equals(retryName, that.retryName)
&& Objects.equals(type, that.type)
&& Objects.equals(creationTime, that.creationTime)
&& Objects.equals(errorMessage, that.errorMessage)
&& Objects.equals(numberOfAttempts, that.numberOfAttempts);
}
@Override
public int hashCode() {
return Objects.hash(retryName, type, creationTime, errorMessage, numberOfAttempts);
}
public String getRetryName() {
return retryName;
}
public void setRetryName(String retryName) {
this.retryName = retryName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ZonedDateTime getCreationTime() {
return creationTime;
}
public void setCreationTime(ZonedDateTime creationTime) {
this.creationTime = creationTime;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public Integer getNumberOfAttempts() {
return numberOfAttempts;
}
public void setNumberOfAttempts(Integer numberOfAttempts) {
this.numberOfAttempts = numberOfAttempts;
}
}
@@ -0,0 +1,16 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.util.List;
public class RetryEvents {
private List<RetryEvent> retryEvents;
public List<RetryEvent> getRetryEvents() {
return retryEvents;
}
public void setRetryEvents(List<RetryEvent> retryEvents) {
this.retryEvents = retryEvents;
}
}
@@ -0,0 +1,50 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.time.ZonedDateTime;
import java.util.Objects;
public class TimeLimiterEvent {
private String timeLimiterName;
private String type;
private ZonedDateTime creationTime;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TimeLimiterEvent that = (TimeLimiterEvent) o;
return Objects.equals(timeLimiterName, that.timeLimiterName)
&& Objects.equals(type, that.type)
&& Objects.equals(creationTime, that.creationTime);
}
@Override
public int hashCode() {
return Objects.hash(timeLimiterName, type, creationTime);
}
public String getTimeLimiterName() {
return timeLimiterName;
}
public void setTimeLimiterName(String timeLimiterName) {
this.timeLimiterName = timeLimiterName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ZonedDateTime getCreationTime() {
return creationTime;
}
public void setCreationTime(ZonedDateTime creationTime) {
this.creationTime = creationTime;
}
}
@@ -0,0 +1,16 @@
package com.baeldung.resilience4j.eventendpoints.model;
import java.util.List;
public class TimeLimiterEvents {
private List<TimeLimiterEvent> timeLimiterEvents;
public List<TimeLimiterEvent> getTimeLimiterEvents() {
return timeLimiterEvents;
}
public void setTimeLimiterEvents(List<TimeLimiterEvent> timeLimiterEvents) {
this.timeLimiterEvents = timeLimiterEvents;
}
}