Spring cloud upgrade 2020.0.x - 35. Verify the correctness of thread isolation

Keywords: spring-cloud

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

In the previous section, we verified the correctness of retry through unit test. In this section, we verified the correctness of our thread isolation, mainly including:

  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. When the same microservice calls different instances, different threads (pools) are used.

Verify that the configuration is loaded correctly

Similar to the previous verification retry, we can define different FeignClient, and then check the thread isolation configuration loaded by resilience4j to verify that the thread isolation configuration is loaded correctly.

Moreover, different from the retry configuration, through the source code analysis in the previous series, we know that the FeignClient of spring cloud openfeign is lazy to load. Therefore, the thread isolation we implemented is also lazy loading. We need to call it first before initializing the thread pool. Therefore, we need to make a call before verifying the thread pool configuration.

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, add an instance to both microservices, 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",
        //Default thread pool configuration
        "resilience4j.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
        //Thread pool configuration for testService2Client
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@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 service2Instance2 = 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("www.httpbin.org");
            when(service1Instance1.getPort()).thenReturn(80);
            when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
            when(service2Instance2.getHost()).thenReturn("httpbin.org");
            when(service2Instance2.getPort()).thenReturn(80);
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            Mockito.when(spy.getInstances("testService1"))
                    .thenReturn(List.of(service1Instance1));
            Mockito.when(spy.getInstances("testService2"))
                    .thenReturn(List.of(service2Instance2));
            return spy;
        }
    }
}

Write test code to verify that the configuration is correct:

@Test
public void testConfigureThreadPool() {
    //Prevent the influence of circuit breaker
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    //Call these two feignclients to ensure that the corresponding NamedContext is initialized
    testService1Client.anything();
    testService2Client.anything();
    //Verify that the actual configuration of thread isolation conforms to our filled configuration
    ThreadPoolBulkhead threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
            .stream().filter(t -> t.getName().contains("service1Instance1")).findFirst().get();
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 10);
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 10);
    threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
            .stream().filter(t -> t.getName().contains("service1Instance2")).findFirst().get();
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 5);
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 5);
}

When the same microservice calls different instances, different threads (pools) are used.

We need to make sure that the thread pool of the last call (that is, sending http requests) must be the corresponding thread pool in ThreadPoolBulkHead. We need to implement ApacheHttpClient in all aspects and add the annotation @ EnableAspectJAutoProxy(proxyTargetClass = true):

//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",
        //Default thread pool configuration
        "resilience4j.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
        //Thread pool configuration for testService2Client
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public static class App {
        @Bean
        public DiscoveryClient discoveryClient() {
            //Simulate two service instances
            ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
            ServiceInstance service2Instance2 = 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("www.httpbin.org");
            when(service1Instance1.getPort()).thenReturn(80);
            when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
            when(service2Instance2.getHost()).thenReturn("httpbin.org");
            when(service2Instance2.getPort()).thenReturn(80);
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            Mockito.when(spy.getInstances("testService1"))
                    .thenReturn(List.of(service1Instance1));
            Mockito.when(spy.getInstances("testService2"))
                    .thenReturn(List.of(service2Instance2));
            return spy;
        }
    }
}

Intercept the execute method of ApacheHttpClient, so that you can get the thread pool that is really responsible for http calls and put it into the request Header:

@Aspect
public static class ApacheHttpClientAop {
    //In the last step, the Apache httpclient aspect
    @Pointcut("execution(* com.github.jojotech.spring.cloud.webmvc.feign.ApacheHttpClient.execute(..))")
    public void annotationPointcut() {
    }

    @Around("annotationPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //Setting Header does not go through the Feign RequestInterceptor, because we have to get the thread context that finally calls ApacheHttpClient.
        Request request = (Request) pjp.getArgs()[0];
        Field headers = ReflectionUtils.findField(Request.class, "headers");
        ReflectionUtils.makeAccessible(headers);
        Map<String, Collection<String>> map = (Map<String, Collection<String>>) ReflectionUtils.getField(headers, request);
        HashMap<String, Collection<String>> stringCollectionHashMap = new HashMap<>(map);
        stringCollectionHashMap.put(THREAD_ID_HEADER, List.of(String.valueOf(Thread.currentThread().getName())));
        ReflectionUtils.setField(headers, request, stringCollectionHashMap);
        return pjp.proceed();
    }
}

In this way, we can get the name of the specific thread carrying the request. From the name, we can see the thread pool in which it is located (the format is "bulkhead - thread isolation name - n", for example, bulkhead testservice1client: www.httpbin. Org: 80-1). Next, let's see whether different instances are called with different thread pools:

@Test
public void testDifferentThreadPoolForDifferentInstance() throws InterruptedException {
    //Prevent the influence of circuit breaker
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    Set<String> threadIds = Sets.newConcurrentHashSet();
    Thread[] threads = new Thread[100];
    //Cycle 100 times
    for (int i = 0; i < 100; i++) {
        threads[i] = new Thread(() -> {
            Span span = tracer.nextSpan();
            try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
                HttpBinAnythingResponse response = testService1Client.anything();
                //Because anything will return all the contents of the request entity we sent, we can get the thread name header of the request
                String threadId = response.getHeaders().get(THREAD_ID_HEADER);
                threadIds.add(threadId);
            }
        });
        threads[i].start();
    }
    for (int i = 0; i < 100; i++) {
        threads[i].join();
    }
    //Confirm that the thread of the instance testService1Client:httpbin.org:80 thread pool exists
    Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:httpbin.org:80")));
    //Confirm that the thread of the instance testService1Client:httpbin.org:80 thread pool exists
    Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:www.httpbin.org:80")));
}

In this way, we have successfully verified the thread pool isolation of instance calls.

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

Posted by flappy_warbucks on Tue, 16 Nov 2021 08:09:57 -0800