SpringCloud upgrade path 2020.0.x - 34. Verify the correctness of retry configuration

Posted by thepeccavi on Sat, 13 Nov 2021 05:21:29 +0100

Code address of this series: https://github.com/JoJoTec/sp...

In the previous section, we used resilience4j to glue OpenFeign to realize circuit breaker, retry and thread isolation, and used a new load balancing algorithm to optimize the performance of load balancing algorithm during business surge. In this section, we begin to write unit tests to verify the correctness of these functions, so as to upgrade the dependencies in the future and ensure the correctness when modifying them. At the same time, through unit testing, we can better understand Spring Cloud.

Verify retry configuration

For the retry we implemented, we need to verify:

  1. Verify that the configuration is loaded correctly: that is, the configuration of Resilience4j added in the Spring configuration (such as application.yml) is loaded correctly.
  2. Verify that the connection timeout retry is correct: FeignClient can configure the connection timeout. If the connection timeout occurs, a connection timeout exception will be thrown. For this exception, any request should be retried because the request has not been sent.
  3. Verify that the retry for the circuit breaker exception is correct: the circuit breaker is at the microservice instance method level. If a circuit breaker open exception is thrown, you should directly retry the next instance.
  4. Verify that the retry for the current limiter exception is correct: when the thread isolation of an instance is full, throw a thread current limiting exception, and directly retry the next instance.
  5. Verify that retryable methods for non 2xx response codes are correct
  6. Verify that the non retrieable method for non 2xx response code is not retried
  7. Verify that the response timeout exception for the retrieable method is correct: FeignClient can configure ReadTimeout, that is, the response timeout. If the method can be retried, it needs to be retried.
  8. Verify that the response timeout for the non retrieable method is abnormal and cannot be retried: FeignClient can configure ReadTimeout, that is, the response timeout. If the method cannot be retried, it cannot be retried.

Verify that the configuration is loaded correctly

We can define different feignclients, and then check the retry configuration loaded by resilience4j to verify that the retry configuration is loaded correctly.

First, define two feignclients. The microservices are testService1 and testService2 respectively, and the contextId is testService1Client and testService2Client respectively

@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
    @GetMapping("/anything")
    HttpBinAnythingResponse anything();
}
@FeignClient(name = "testService2", contextId = "testService2Client")
    public interface TestService2Client {
        @GetMapping("/anything")
        HttpBinAnythingResponse anything();
}

Then, we add Spring configuration and write unit test classes using Spring extension:

//The spring Extension also contains the Extension related to Mockito, so @ Mock and other annotations also take effect
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
        //The default request retry count is 3
        "resilience4j.retry.configs.default.maxAttempts=3",
        // The number of retries requested by all methods in testService2Client is 2
        "resilience4j.retry.configs.testService2Client.maxAttempts=2",
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    public static class App {
    }
}

Write test code to verify the correctness of configuration loading:

@Test
public void testConfigureRetry() {
    //Read all retries
    List<Retry> retries = retryRegistry.getAllRetries().asJava();
    //Verify that the configuration is consistent with the configuration we filled in
    Map<String, Retry> retryMap = retries.stream().collect(Collectors.toMap(Retry::getName, v -> v));
    //When we initialize the Retry, we use the ContextId of FeignClient as the Name of the Retry
    Retry retry = retryMap.get("testService1Client");
    //Verify that the Retry configuration exists
    Assertions.assertNotNull(retry);
    //Verify that the Retry configuration matches our configuration
    Assertions.assertEquals(retry.getRetryConfig().getMaxAttempts(), 3);
    retry = retryMap.get("testService2Client");
    //Verify that the Retry configuration exists
    Assertions.assertNotNull(retry);
    //Verify that the Retry configuration matches our configuration
    Assertions.assertEquals(retry.getRetryConfig().getMaxAttempts(), 2);
}

Verify that the retry for ConnectTimeout is correct

We can verify whether the retry is effective by registering two instances for a micro service. One instance cannot be connected and the other instance can be connected normally. No matter how FeignClient is called, the request will not fail. We use the HTTP test website to test, that is http://httpbin.org . The api of this website can be used to simulate various calls. Where / status/{status} is to return the sent request intact in the response. In the unit test, we will not deploy a registry separately, but directly the core interface DiscoveryClient of service discovery in Mock spring cloud, and turn off our Eureka service discovery and registration through configuration, that is:

//The spring Extension also contains the Extension related to Mockito, so @ Mock and other annotations also take effect
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
        //Close eureka client
        "eureka.client.enabled=false",
        //The default request retry count is 3
        "resilience4j.retry.configs.default.maxAttempts=3"
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    public static class App {
        @Bean
        public DiscoveryClient discoveryClient() {
            //Simulate two service instances
            ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
            ServiceInstance service1Instance4 = Mockito.spy(ServiceInstance.class);
            Map<String, String> zone1 = Map.ofEntries(
                    Map.entry("zone", "zone1")
            );
            when(service1Instance1.getMetadata()).thenReturn(zone1);
            when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
            when(service1Instance1.getHost()).thenReturn("httpbin.org");
            when(service1Instance1.getPort()).thenReturn(80);
            when(service1Instance4.getInstanceId()).thenReturn("service1Instance4");
            when(service1Instance4.getHost()).thenReturn("www.httpbin.org");
            //This port cannot be connected. Test IOException
            when(service1Instance4.getPort()).thenReturn(18080);
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            //Microservice testService3 has two instances, service1Instance1 and service1Instance4
            Mockito.when(spy.getInstances("testService3"))
                    .thenReturn(List.of(service1Instance1, service1Instance4));
            return spy;
        }
    }
}

Write FeignClient:

@FeignClient(name = "testService3", contextId = "testService3Client")
public interface TestService3Client {
    @PostMapping("/anything")
    HttpBinAnythingResponse anything();
}

Call anything method of TestService3Client to verify whether there is a retry:

@SpyBean
private TestService3Client testService3Client;

/**
 * Verify whether the connect timeout request is retried normally for an abnormal instance (the instance being closed)
 */
@Test
public void testIOExceptionRetry() {
    //Prevent the influence of circuit breaker
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    for (int i = 0; i < 5; i++) {
        Span span = tracer.nextSpan();
        try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
            //If no exception is thrown, it will be retried normally
            testService3Client.anything();
            testService3Client.anything();
        }
    }
}

It should be emphasized here that since we will test other exceptions and circuit breakers in this class, we need to avoid that when these tests are executed together, the circuit breaker is opened. Therefore, we clear the data of all circuit breakers at the beginning of all tests calling FeignClient methods. Through:

circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);

And it can be seen from the log that the retry is due to the connect timeout:

call url: POST -> http://www.httpbin.org:18080/anything, ThreadPoolStats(testService3Client:www.httpbin.org:18080): {"coreThreadPoolSize":10,"maximumThreadPoolSize":10,"queueCapacity":100,"queueDepth":0,"remainingQueueCapacity":100,"threadPoolSize":1}, CircuitBreakStats(testService3Client:www.httpbin.org:18080:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService3Client.anything()): {"failureRate":-1.0,"numberOfBufferedCalls":0,"numberOfFailedCalls":0,"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfSlowFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"slowCallRate":-1.0}
TestService3Client#anything() response: 582-Connect to www.httpbin.org:18080 [www.httpbin.org/34.192.79.103, www.httpbin.org/18.232.227.86, www.httpbin.org/3.216.167.140, www.httpbin.org/54.156.165.4] failed: Connect timed out, should retry: true
call url: POST -> http://httpbin.org:80/anything, ThreadPoolStats(testService3Client:httpbin.org:80): {"coreThreadPoolSize":10,"maximumThreadPoolSize":10,"queueCapacity":100,"queueDepth":0,"remainingQueueCapacity":100,"threadPoolSize":1}, CircuitBreakStats(testService3Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService3Client.anything()): {"failureRate":-1.0,"numberOfBufferedCalls":0,"numberOfFailedCalls":0,"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfSlowFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"slowCallRate":-1.0}
response: 200 - OK

Verify that the retry for the circuit breaker exception is correct

Through the source code analysis in the previous series, we know that the FeignClient of spring cloud openfeign is actually lazy to load. Therefore, the circuit breaker we implemented is also lazy loaded. We need to call it first before initializing the circuit breaker. Therefore, if we want to simulate the abnormal opening of the circuit breaker, we need to manually read and load the circuit breaker before we can obtain the circuit breaker of the corresponding method and modify the status.

Let's first define a FeignClient:

@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
    @GetMapping("/anything")
    HttpBinAnythingResponse anything();
}

In the same way as before, add an instance to this micro service:

//The spring Extension also contains the Extension related to Mockito, so @ Mock and other annotations also take effect
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
        //Close eureka client
        "eureka.client.enabled=false",
        //The default request retry count is 3
        "resilience4j.retry.configs.default.maxAttempts=3",
        //Add breaker configuration
        "resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
        "resilience4j.circuitbreaker.configs.default.slidingWindowType=COUNT_BASED",
        "resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
        "resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    public static class App {
        @Bean
        public DiscoveryClient discoveryClient() {
            //Simulate two service instances
            ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
            ServiceInstance service1Instance3 = Mockito.spy(ServiceInstance.class);
            Map<String, String> zone1 = Map.ofEntries(
                    Map.entry("zone", "zone1")
            );
            when(service1Instance1.getMetadata()).thenReturn(zone1);
            when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
            when(service1Instance1.getHost()).thenReturn("httpbin.org");
            when(service1Instance1.getPort()).thenReturn(80);
            when(service1Instance3.getMetadata()).thenReturn(zone1);
            when(service1Instance3.getInstanceId()).thenReturn("service1Instance3");
            //This is actually httpbin.org. To distinguish it from the first instance, add www
            when(service1Instance3.getHost()).thenReturn("www.httpbin.org");
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            //Microservice testService3 has two instances, service1Instance1 and service1Instance4
            Mockito.when(spy.getInstances("testService1"))
                    .thenReturn(List.of(service1Instance1, service1Instance3));
            return spy;
        }
    }
}

Then, write the test code:

@Test
public void testRetryOnCircuitBreakerException() {
    //Prevent the influence of circuit breaker
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    CircuitBreaker testService1ClientInstance1Anything;
    try {
        testService1ClientInstance1Anything = circuitBreakerRegistry
                .circuitBreaker("testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()", "testService1Client");
    } catch (ConfigurationNotFoundException e) {
        //If it cannot be found, use the default configuration
        testService1ClientInstance1Anything = circuitBreakerRegistry
                .circuitBreaker("testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()");
    }
    //Open the circuit breaker
    testService1ClientInstance1Anything.transitionToOpenState();
    //The call is repeated, and the call is successful, that is, the circuit breaker exception is retried
    for (int i = 0; i < 10; i++) {
        this.testService1Client.anything();
    }
}

After running the test, it can be seen from the log that the abnormal opening of the circuit breaker was retried:

2021-11-13 03:40:13.546  INFO [,,] 4388 --- [           main] c.g.j.s.c.w.f.DefaultErrorDecoder        : TestService1Client#anything() response: 581-CircuitBreaker 'testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()' is OPEN and does not permit further calls, should retry: true

WeChat search "my programming meow" attention to the official account, daily brush, easy to upgrade technology, and capture all kinds of offer:

Topics: spring-cloud