我們繼續上一節針對我們的重試進行測試
驗證針對限流器異常的重試正確
通過系列前面的源碼分析,我們知道 spring-cloud-openfeign 的 FeignClient 其實是懶加載的。所以我們實現的斷路器也是懶加載的,需要先調用,之后才會初始化線程隔離。所以這里如果我們要模擬線程隔離滿的異常,需要先手動讀取載入線程隔離,之后才能獲取對應實例的線程隔離,將線程池填充滿。
我們先定義一個 FeignClient:
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
使用前面同樣的方式,給這個微服務添加實例:
//SpringExtension也包含了 Mockito 相關的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//關閉 eureka client
"eureka.client.enabled=false",
//默認請求重試次數為 3
"resilience4j.retry.configs.default.maxAttempts=3",
//增加斷路器配置
"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() {
//模擬兩個服務實例
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");
//這其實就是 httpbin.org ,為了和第一個實例進行區分加上 www
when(service1Instance3.getHost()).thenReturn("www.httpbin.org");
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
//微服務 testService3 有兩個實例即 service1Instance1 和 service1Instance4
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1, service1Instance3));
return spy;
}
}
}
然后,編寫測試代碼:
@Test
public void testRetryOnBulkheadException() {
//防止斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
this.testService1Client.anything();
ThreadPoolBulkhead threadPoolBulkhead;
try {
threadPoolBulkhead = threadPoolBulkheadRegistry
.bulkhead("testService1Client:httpbin.org:80", "testService1Client");
} catch (ConfigurationNotFoundException e) {
//找不到就用默認配置
threadPoolBulkhead = threadPoolBulkheadRegistry
.bulkhead("testService1Client:httpbin.org:80");
}
//線程隊列我們配置的是 1,線程池大小是 10,這樣會將線程池填充滿
for (int i = 0; i < 10 + 1; i++) {
threadPoolBulkhead.submit(() -> {
try {
//這樣任務永遠不會結束了
Thread.currentThread().join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//調用多次,調用成功即對斷路器異常重試了
for (int i = 0; i < 10; i++) {
this.testService1Client.anything();
}
}
運行測試,日志中可以看出,針對線程池滿的異常進行重試了:
2021-11-13 03:35:16.371 INFO [,,] 3824 --- [ main] c.g.j.s.c.w.f.DefaultErrorDecoder : TestService1Client#anything() response: 584-Bulkhead 'testService1Client:httpbin.org:80' is full and does not permit further calls, should retry: true
驗證針對非 2xx 響應碼可重試的方法重試正確
我們通過使用 http.bin 的 /status/{statusCode}
接口,這個接口會根據路徑參數 statusCode
返回對應狀態碼的響應:
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/status/500")
String testGetRetryStatus500();
}
我們如何感知被重試三次呢?每次調用,就會從負載均衡器獲取一個服務實例。在負載均衡器代碼中,我們使用了根據當前 sleuth 的上下文的 traceId 的緩存,每次調用,traceId 對應的 position 值就會加 1。我們可以通過觀察這個值的變化獲取到究竟本次請求調用了幾次負載均衡器,也就是做了幾次調用。
編寫測試:
@Test
public void testNon2xxRetry() {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
//防止斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
long l = span.context().traceId();
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
= (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
int start = atomicInteger.get();
try {
//get 方法會重試
testService1Client.testGetRetryStatus500();
} catch (Exception e) {
}
//因為每次調用都會失敗,所以會重試配置的 3 次
Assertions.assertEquals(3, atomicInteger.get() - start);
}
}
驗證針對非 2xx 響應碼不可重試的方法沒有重試
我們通過使用 http.bin 的 /status/{statusCode}
接口,這個接口會根據路徑參數 statusCode
返回對應狀態碼的響應,並且支持各種 HTTP 請求方式:
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@PostMapping("/status/500")
String testPostRetryStatus500();
}
默認情況下,我們只會對 GET 方法重試,對於其他 HTTP 請求方法,是不會重試的:
@Test
public void testNon2xxRetry() {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
//防止斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
long l = span.context().traceId();
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
= (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
int start = atomicInteger.get();
try {
//post 方法不會重試
testService1Client.testPostRetryStatus500();
} catch (Exception e) {
}
//不會重試,因此只會被調用 1 次
Assertions.assertEquals(1, atomicInteger.get() - start);
}
}
微信搜索“我的編程喵”關注公眾號,每日一刷,輕松提升技術,斬獲各種offer: