BAEL-1362 - Retry with Spring Cloud Ribbon (#9237)
This commit is contained in:
parent
ebc6cb54b3
commit
8f20c9cca4
@ -40,6 +40,7 @@
|
|||||||
<module>spring-cloud-task</module>
|
<module>spring-cloud-task</module>
|
||||||
<module>spring-cloud-zuul</module>
|
<module>spring-cloud-zuul</module>
|
||||||
<module>spring-cloud-zuul-fallback</module>
|
<module>spring-cloud-zuul-fallback</module>
|
||||||
|
<module>spring-cloud-ribbon-retry</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
40
spring-cloud/spring-cloud-ribbon-retry/pom.xml
Normal file
40
spring-cloud/spring-cloud-ribbon-retry/pom.xml
Normal 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>
|
@ -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>
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
spring.application.name=weather-service
|
||||||
|
successful.call.divisor=3
|
Loading…
x
Reference in New Issue
Block a user