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-zuul</module>
|
||||
<module>spring-cloud-zuul-fallback</module>
|
||||
<module>spring-cloud-ribbon-retry</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -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…
Reference in New Issue