Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Hystrix by Spring Cloud Circuit Breaker and Resilience4j #117 #141

Merged
merged 4 commits into from
Nov 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

[![Build Status](https://travis-ci.org/spring-petclinic/spring-petclinic-microservices.svg?branch=master)](https://travis-ci.org/spring-petclinic/spring-petclinic-microservices/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

This microservices branch was initially derived from [AngularJS version](https://github.com/spring-petclinic/spring-petclinic-angular1) to demonstrate how to split sample Spring application into [microservices](http://www.martinfowler.com/articles/microservices.html). To achieve that goal we used [Spring Cloud Netflix](https://github.com/spring-cloud/spring-cloud-netflix) technology stack.
This microservices branch was initially derived from [AngularJS version](https://github.com/spring-petclinic/spring-petclinic-angular1) to demonstrate how to split sample Spring application into [microservices](http://www.martinfowler.com/articles/microservices.html).
To achieve that goal we use Spring Cloud Gateway, Spring Cloud Circuit Breaker, Spring Cloud Config, Spring Cloud Sleuth, Resilience4j, Micrometer
and the Eureka Service Discovery from the [Spring Cloud Netflix](https://github.com/spring-cloud/spring-cloud-netflix) technology stack.

## Starting services locally without Docker

Expand All @@ -17,10 +19,6 @@ If everything goes well, you can access the following services at given location
* Admin Server (Spring Boot Admin) - http://localhost:9090
* Grafana Dashboards - http://localhost:3000
* Prometheus - http://localhost:9091
* Hystrix Dashboard for Circuit Breaker pattern - http://localhost:7979 - On the home page is a form where you can enter
the URL for an event stream to monitor, for example the `api-gateway` service running locally: `http://localhost:8080/actuator/hystrix.stream`
or running into docker: `http://api-gateway:8080/actuator/hystrix.stream`


You can tell Config Server to use your local Git repository by using `native` Spring profile and setting
`GIT_REPO` environment variable, for example:
Expand Down Expand Up @@ -131,7 +129,7 @@ All those three REST controllers `OwnerResource`, `PetResource` and `VisitResour
| Service Discovery | [Eureka server](spring-petclinic-discovery-server) and [Service discovery client](spring-petclinic-vets-service/src/main/java/org/springframework/samples/petclinic/vets/VetsServiceApplication.java) |
| API Gateway | [Spring Cloud Gateway starter](spring-petclinic-api-gateway/pom.xml) and [Routing configuration](/spring-petclinic-api-gateway/src/main/resources/application.yml) |
| Docker Compose | [Spring Boot with Docker guide](https://spring.io/guides/gs/spring-boot-docker/) and [docker-compose file](docker-compose.yml) |
| Circuit Breaker | [Hystrix fallback method](spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java) |
| Circuit Breaker | [Resilience4j fallback method](spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java) |
| Grafana / Prometheus Monitoring | [Micrometer implementation](https://micrometer.io/), [Spring Boot Actuator Production Ready Metrics] |

Front-end module | Files |
Expand Down
11 changes: 0 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,6 @@ services:
ports:
- 9090:9090

hystrix-dashboard:
image: springcommunity/spring-petclinic-hystrix-dashboard
container_name: hystrix-dashboard
mem_limit: 512M
depends_on:
- config-server
- discovery-server
entrypoint: ["./dockerize","-wait=tcp://discovery-server:8761","-timeout=60s","--","java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
ports:
- 7979:7979

## Grafana / Prometheus

grafana-server:
Expand Down
1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
<module>spring-petclinic-config-server</module>
<module>spring-petclinic-discovery-server</module>
<module>spring-petclinic-api-gateway</module>
<module>spring-petclinic-hystrix-dashboard</module>
</modules>

<properties>
Expand Down
22 changes: 12 additions & 10 deletions spring-petclinic-api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
Expand All @@ -73,6 +77,10 @@
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand All @@ -83,16 +91,6 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Third parties -->
<dependency>
Expand All @@ -107,6 +105,10 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-micrometer</artifactId>
</dependency>

<!-- Webjars -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
*/
package org.springframework.samples.petclinic.api;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
Expand All @@ -32,12 +36,13 @@
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import java.time.Duration;


/**
* @author Maciej Szarlinski
*/
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
public class ApiGatewayApplication {

Expand Down Expand Up @@ -71,4 +76,15 @@ RouterFunction<?> routerFunction() {
request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(indexHtml));
return router;
}

/**
* Default Resilience4j circuit breaker configuration
*/
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build())
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@
package org.springframework.samples.petclinic.api.application;

import lombok.RequiredArgsConstructor;
import org.springframework.samples.petclinic.api.dto.VisitDetails;
import org.springframework.samples.petclinic.api.dto.Visits;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.joining;

Expand All @@ -40,7 +37,6 @@ public class VisitsServiceClient {

private final WebClient.Builder webClientBuilder;

// FIXME HYSTRIX @HystrixCommand(fallbackMethod = "emptyVisitsForPets")
public Mono<Visits> getVisitsForPets(final List<Integer> petIds) {
return webClientBuilder.build()
.get()
Expand All @@ -53,10 +49,6 @@ private String joinIds(List<Integer> petIds) {
return petIds.stream().map(Object::toString).collect(joining(","));
}

private Mono<Map<Integer, List<VisitDetails>>> emptyVisitsForPets(Mono<List<Integer>> petIds) {
return Mono.just(Collections.emptyMap());
}

void setHostname(String hostname) {
this.hostname = hostname;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package org.springframework.samples.petclinic.api.boundary.web;

import lombok.RequiredArgsConstructor;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.samples.petclinic.api.application.CustomersServiceClient;
import org.springframework.samples.petclinic.api.application.VisitsServiceClient;
import org.springframework.samples.petclinic.api.dto.OwnerDetails;
Expand All @@ -41,11 +43,17 @@ public class ApiGatewayController {

private final VisitsServiceClient visitsServiceClient;

private final ReactiveCircuitBreakerFactory cbFactory;

@GetMapping(value = "owners/{ownerId}")
public Mono<OwnerDetails> getOwnerDetails(final @PathVariable int ownerId) {
return customersServiceClient.getOwner(ownerId)
.flatMap(owner ->
visitsServiceClient.getVisitsForPets(owner.getPetIds())
.transform(it -> {
ReactiveCircuitBreaker cb = cbFactory.create("getOwnerDetails");
return cb.run(it, throwable -> emptyVisitsForPets());
})
.map(addVisitsToOwner(owner))
);

Expand All @@ -62,4 +70,8 @@ private Function<Visits, OwnerDetails> addVisitsToOwner(OwnerDetails owner) {
return owner;
};
}

private Mono<Visits> emptyVisitsForPets() {
return Mono.just(new Visits());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
*/
package org.springframework.samples.petclinic.api.dto;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;

/**
* @author Maciej Szarlinski
*/
@Value
@Data
@NoArgsConstructor
public class VisitDetails {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.samples.petclinic.api.dto;

import java.util.ArrayList;
import java.util.List;

import lombok.NoArgsConstructor;
Expand All @@ -24,9 +25,8 @@
* @author Maciej Szarlinski
*/
@Value
@NoArgsConstructor
public class Visits {

private List<VisitDetails> items = null;
private List<VisitDetails> items = new ArrayList<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,6 @@ void getVisitsForPets_withAvailableVisitsService() {
assertVisitDescriptionEquals(visits.block(), PET_ID,"test visit");
}

/**
* Test Hystrix fallback method
*/
// @Test
// public void getVisitsForPets_withServerError() {
//
// mockServer.expect(requestTo("http://visits-service/pets/visits?petId=1"))
// .andRespond(withServerError());
//
// Map<Integer, List<VisitDetails>> visits = null;
// visitsServiceClient.getVisitsForPets(Mono.just(Collections.singletonList(1)));
//
// assertEquals(0, visits.size());
// }

private void assertVisitDescriptionEquals(Visits visits, int petId, String description) {
assertEquals(1, visits.getItems().size());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.springframework.samples.petclinic.api.boundary.web;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.samples.petclinic.api.application.CustomersServiceClient;
import org.springframework.samples.petclinic.api.application.VisitsServiceClient;
import org.springframework.samples.petclinic.api.dto.OwnerDetails;
import org.springframework.samples.petclinic.api.dto.PetDetails;
import org.springframework.samples.petclinic.api.dto.VisitDetails;
import org.springframework.samples.petclinic.api.dto.Visits;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.net.ConnectException;
import java.util.Collections;

@ExtendWith(SpringExtension.class)
@WebFluxTest(controllers = ApiGatewayController.class)
@Import(ReactiveResilience4JAutoConfiguration.class)
class ApiGatewayControllerTest {

@MockBean
private CustomersServiceClient customersServiceClient;

@MockBean
private VisitsServiceClient visitsServiceClient;

@Autowired
private WebTestClient client;


@Test
void getOwnerDetails_withAvailableVisitsService() {
OwnerDetails owner = new OwnerDetails();
PetDetails cat = new PetDetails();
cat.setId(20);
cat.setName("Garfield");
owner.getPets().add(cat);
Mockito
.when(customersServiceClient.getOwner(1))
.thenReturn(Mono.just(owner));

Visits visits = new Visits();
VisitDetails visit = new VisitDetails();
visit.setId(300);
visit.setDescription("First visit");
visit.setPetId(cat.getId());
visits.getItems().add(visit);
Mockito
.when(visitsServiceClient.getVisitsForPets(Collections.singletonList(cat.getId())))
.thenReturn(Mono.just(visits));

client.get()
.uri("/api/gateway/owners/1")
.exchange()
.expectStatus().isOk()
//.expectBody(String.class)
//.consumeWith(response ->
// Assertions.assertThat(response.getResponseBody()).isEqualTo("Garfield"));
.expectBody()
.jsonPath("$.pets[0].name").isEqualTo("Garfield")
.jsonPath("$.pets[0].visits[0].description").isEqualTo("First visit");
}

/**
* Test Resilience4j fallback method
*/
@Test
void getOwnerDetails_withServiceError() {
OwnerDetails owner = new OwnerDetails();
PetDetails cat = new PetDetails();
cat.setId(20);
cat.setName("Garfield");
owner.getPets().add(cat);
Mockito
.when(customersServiceClient.getOwner(1))
.thenReturn(Mono.just(owner));

Mockito
.when(visitsServiceClient.getVisitsForPets(Collections.singletonList(cat.getId())))
.thenReturn(Mono.error(new ConnectException("Simulate error")));

client.get()
.uri("/api/gateway/owners/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.pets[0].name").isEqualTo("Garfield")
.jsonPath("$.pets[0].visits").isEmpty();
}

}
Loading