BAEL-1362 - Retry with Spring Cloud Ribbon (#9237)

This commit is contained in:
Catalin Burcea 2020-06-10 16:15:57 +03:00 committed by GitHub
parent ebc6cb54b3
commit 8f20c9cca4
17 changed files with 421 additions and 0 deletions

View File

@ -40,6 +40,7 @@
<module>spring-cloud-task</module>
<module>spring-cloud-zuul</module>
<module>spring-cloud-zuul-fallback</module>
<module>spring-cloud-ribbon-retry</module>
</modules>
<build>

View File

@ -0,0 +1,40 @@
<?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.baeldung.spring.cloud</groupId>
<artifactId>spring-cloud-ribbon-retry</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-ribbon-retry</name>
<packaging>pom</packaging>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<modules>
<module>ribbon-client-service</module>
<module>ribbon-weather-service</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>
</project>

View File

@ -0,0 +1,39 @@
<?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>
<artifactId>ribbon-client-service</artifactId>
<parent>
<groupId>com.baeldung.spring.cloud</groupId>
<artifactId>spring-cloud-ribbon-retry</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>com.baeldung.spring.cloud</groupId>
<artifactId>ribbon-weather-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>
</project>

View File

@ -0,0 +1,12 @@
package com.baeldung.spring.cloud.ribbon.retry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RibbonClientApp {
public static void main(String[] args) {
SpringApplication.run(RibbonClientApp.class, args);
}
}

View File

@ -0,0 +1,26 @@
package com.baeldung.spring.cloud.ribbon.retry.backoff;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryFactory;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.stereotype.Component;
@Component
@Profile("exponential-backoff")
class ExponentialBackoffRetryFactory extends RibbonLoadBalancedRetryFactory {
public ExponentialBackoffRetryFactory(SpringClientFactory clientFactory) {
super(clientFactory);
}
@Override
public BackOffPolicy createBackOffPolicy(String service) {
ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
exponentialBackOffPolicy.setInitialInterval(1000);
exponentialBackOffPolicy.setMultiplier(2);
exponentialBackOffPolicy.setMaxInterval(10000);
return exponentialBackOffPolicy;
}
}

View File

@ -0,0 +1,26 @@
package com.baeldung.spring.cloud.ribbon.retry.backoff;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryFactory;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy;
import org.springframework.stereotype.Component;
@Component
@Profile("exponential-random-backoff")
class ExponentialRandomBackoffRetryFactory extends RibbonLoadBalancedRetryFactory {
public ExponentialRandomBackoffRetryFactory(SpringClientFactory clientFactory) {
super(clientFactory);
}
@Override
public BackOffPolicy createBackOffPolicy(String service) {
ExponentialRandomBackOffPolicy exponentialRandomBackOffPolicy = new ExponentialRandomBackOffPolicy();
exponentialRandomBackOffPolicy.setInitialInterval(1000);
exponentialRandomBackOffPolicy.setMultiplier(2);
exponentialRandomBackOffPolicy.setMaxInterval(10000);
return exponentialRandomBackOffPolicy;
}
}

View File

@ -0,0 +1,24 @@
package com.baeldung.spring.cloud.ribbon.retry.backoff;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryFactory;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.stereotype.Component;
@Component
@Profile("fixed-backoff")
class FixedBackoffRetryFactory extends RibbonLoadBalancedRetryFactory {
public FixedBackoffRetryFactory(SpringClientFactory clientFactory) {
super(clientFactory);
}
@Override
public BackOffPolicy createBackOffPolicy(String service) {
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000);
return fixedBackOffPolicy;
}
}

View File

@ -0,0 +1,20 @@
package com.baeldung.spring.cloud.ribbon.retry.config;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.WeightedResponseTimeRule;
import org.springframework.context.annotation.Bean;
public class RibbonConfiguration {
@Bean
public IPing ribbonPing() {
return new PingUrl();
}
@Bean
public IRule ribbonRule() {
return new WeightedResponseTimeRule();
}
}

View File

@ -0,0 +1,19 @@
package com.baeldung.spring.cloud.ribbon.retry.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
@RibbonClient(name = "weather-service", configuration = RibbonConfiguration.class)
public class WeatherClientRibbonConfiguration {
@LoadBalanced
@Bean
RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

View File

@ -0,0 +1,21 @@
package com.baeldung.spring.cloud.ribbon.retry.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class RibbonClientController {
private static final String WEATHER_SERVICE = "weather-service";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/client/weather")
public String weather() {
String result = restTemplate.getForObject("http://" + WEATHER_SERVICE + "/weather", String.class);
return "Weather Service Response: " + result;
}
}

View File

@ -0,0 +1,17 @@
spring:
profiles:
# fixed-backoff, exponential-backoff, exponential-random-backoff
active: fixed-backoff
application:
name: ribbon-client
weather-service:
ribbon:
eureka:
enabled: false
listOfServers: http://localhost:8081, http://localhost:8082
ServerListRefreshInterval: 5000
MaxAutoRetries: 3
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: true
retryableStatusCodes: 503, 408

View File

@ -0,0 +1,50 @@
package com.baeldung.spring.cloud.ribbon.retry;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RibbonClientApp.class)
public class RibbonRetryFailureIntegrationTest {
private static ConfigurableApplicationContext weatherServiceInstance1;
private static ConfigurableApplicationContext weatherServiceInstance2;
@LocalServerPort
private int port;
private TestRestTemplate restTemplate = new TestRestTemplate();
@BeforeAll
public static void setup() {
weatherServiceInstance1 = startApp(8081);
weatherServiceInstance2 = startApp(8082);
}
@AfterAll
public static void cleanup() {
weatherServiceInstance1.close();
weatherServiceInstance2.close();
}
private static ConfigurableApplicationContext startApp(int port) {
return SpringApplication.run(RibbonWeatherServiceApp.class, "--server.port=" + port, "--successful.call.divisor=6");
}
@Test
public void whenRibbonClientIsCalledAndServiceUnavailable_thenFailure() {
String url = "http://localhost:" + port + "/client/weather";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertTrue(response.getStatusCode().is5xxServerError());
}
}

View File

@ -0,0 +1,52 @@
package com.baeldung.spring.cloud.ribbon.retry;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RibbonClientApp.class)
public class RibbonRetrySuccessIntegrationTest {
private static ConfigurableApplicationContext weatherServiceInstance1;
private static ConfigurableApplicationContext weatherServiceInstance2;
@LocalServerPort
private int port;
private TestRestTemplate restTemplate = new TestRestTemplate();
@BeforeAll
public static void setup() {
weatherServiceInstance1 = startApp(8081);
weatherServiceInstance2 = startApp(8082);
}
private static ConfigurableApplicationContext startApp(int port) {
return SpringApplication.run(RibbonWeatherServiceApp.class, "--server.port=" + port, "--successful.call.divisor=3");
}
@AfterAll
public static void cleanup() {
weatherServiceInstance1.close();
weatherServiceInstance2.close();
}
@Test
public void whenRibbonClientIsCalledAndServiceAvailable_thenSuccess() {
String url = "http://localhost:" + port + "/client/weather";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertEquals(response.getBody(), "Weather Service Response: Today's a sunny day");
}
}

View File

@ -0,0 +1,21 @@
<?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>
<artifactId>ribbon-weather-service</artifactId>
<parent>
<groupId>com.baeldung.spring.cloud</groupId>
<artifactId>spring-cloud-ribbon-retry</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,12 @@
package com.baeldung.spring.cloud.ribbon.retry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RibbonWeatherServiceApp {
public static void main(String[] args) {
SpringApplication.run(RibbonWeatherServiceApp.class, args);
}
}

View File

@ -0,0 +1,39 @@
package com.baeldung.spring.cloud.ribbon.retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WeatherController {
private static final Logger LOGGER = LoggerFactory.getLogger(WeatherController.class);
private int nrOfCalls = 0;
@Value("${successful.call.divisor}")
private int divisor;
@GetMapping("/")
public String health() {
return "I am Ok";
}
@GetMapping("/weather")
public ResponseEntity<String> weather() {
LOGGER.info("Providing today's weather information");
if (isServiceUnavailable()) {
return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
}
LOGGER.info("Today's a sunny day");
return new ResponseEntity<>("Today's a sunny day", HttpStatus.OK);
}
private boolean isServiceUnavailable() {
return ++nrOfCalls % divisor != 0;
}
}

View File

@ -0,0 +1,2 @@
spring.application.name=weather-service
successful.call.divisor=3