SpringCloud升級之路2020.0.x版-36. 驗證斷路器正確性

本系列代碼地址:https://github.com/JoJoTec/spring-cloud-parent

上一節我們通過單元測試驗證了線程隔離的正確性,這一節我們來驗證我們斷路器的正確性,主要包括:

  1. 驗證配置正確加載:即我們在 Spring 配置(例如 application.yml)中的加入的 Resilience4j 的配置被正確加載應用了。
  2. 驗證斷路器是基於服務和方法打開的,也就是某個微服務的某個方法斷路器打開但是不會影響這個微服務的其他方法調用

驗證配置正確加載

與之前驗證重試類似,我們可以定義不同的 FeignClient,之後檢查 resilience4j 加載的斷路器配置來驗證線程隔離配置的正確加載。

並且,與重試配置不同的是,通過系列前面的源碼分析,我們知道 spring-cloud-openfeign 的 FeignClient 其實是懶加載的。所以我們實現的斷路器也是懶加載的,需要先調用,之後纔會初始化斷路器。所以這裏我們需要先進行調用之後,再驗證斷路器配置。

首先定義兩個 FeignClient,微服務分別是 testService1 和 testService2,contextId 分別是 testService1Client 和 testService2Client

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

然後,我們增加 Spring 配置,並且給兩個微服務都添加一個實例,使用 SpringExtension 編寫單元測試類:

//SpringExtension也包含了 Mockito 相關的 Extension,所以 @Mock 等註解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
        //默認請求重試次數爲 3
        "resilience4j.retry.configs.default.maxAttempts=3",
        // testService2Client 裏面的所有方法請求重試次數爲 2
        "resilience4j.retry.configs.testService2Client.maxAttempts=2",
        //默認斷路器配置
        "resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
        "resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
        //testService2Client 的 斷路器配置
        "resilience4j.circuitbreaker.configs.testService2Client.failureRateThreshold=30",
        "resilience4j.circuitbreaker.configs.testService2Client.minimumNumberOfCalls=10",
        
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    public static class App {
        @Bean
        public DiscoveryClient discoveryClient() {
            //模擬兩個服務實例
            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;
        }
    }
}

編寫測試代碼,驗證配置正確:

@Test
    public void testConfigureCircuitBreaker() {
        //防止斷路器影響
        circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
        //調用下這兩個 FeignClient 確保對應的 NamedContext 被初始化
        testService1Client.anything();
        testService2Client.anything();
        //驗證斷路器的實際配置,符合我們的填入的配置
        List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
        Set<String> collect = circuitBreakers.stream().map(CircuitBreaker::getName)
                .filter(name -> {
                    try {
                        return name.contains(TestService1Client.class.getMethod("anything").toGenericString())
                                || name.contains(TestService2Client.class.getMethod("anything").toGenericString());
                    } catch (NoSuchMethodException e) {
                        return false;
                    }
                }).collect(Collectors.toSet());
        Assertions.assertEquals(collect.size(), 2);
        circuitBreakers.forEach(circuitBreaker -> {
            if (circuitBreaker.getName().contains(TestService1Client.class.getName())) {
                Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) DEFAULT_FAILURE_RATE_THRESHOLD);
                Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), DEFAULT_MINIMUM_NUMBER_OF_CALLS);
            } else if (circuitBreaker.getName().contains(TestService2Client.class.getName())) {
                Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) TEST_SERVICE_2_FAILURE_RATE_THRESHOLD);
                Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), TEST_SERVICE_2_MINIMUM_NUMBER_OF_CALLS);
            }
        });
    }

驗證斷路器是基於服務和方法打開的。

我們給 TestService1Client 添加一個方法:

@GetMapping("/status/500")
String testCircuitBreakerStatus500();

這個方法一定會調用失敗,從而導致斷路器打開。經過 2 次失敗以上後(因爲配置最少觸發斷路器打開的請求個數爲 2),驗證斷路器狀態:

@Test
public void testCircuitBreakerOpenBasedOnServiceAndMethod() {
    //防止斷路器影響
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    AtomicBoolean passed = new AtomicBoolean(false);
    for (int i = 0; i < 10; i++) {
        //多次調用會導致斷路器打開
        try {
            System.out.println(testService1Client.testCircuitBreakerStatus500());
        } catch(Exception e) {}
        List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
        circuitBreakers.stream().filter(circuitBreaker -> {
            return circuitBreaker.getName().contains("testCircuitBreakerStatus500")
                    && circuitBreaker.getName().contains("TestService1Client");
        }).findFirst().ifPresent(circuitBreaker -> {
            //驗證對應微服務和方法的斷路器被打開
            if (circuitBreaker.getState().equals(CircuitBreaker.State.OPEN)) {
                passed.set(true);
                //斷路器打開後,調用其他方法,不會拋出斷路器打開異常
                testService1Client.testAnything();
            }
        });
    }
    
    Assertions.assertTrue(passed.get());
}

這樣,我們就成功驗證了,驗證斷路器是基於服務和方法打開的。

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章