parent
437aa29f65
commit
bbf02912ac
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reactor.PolarisCircuitBreakerReactorTransformer;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
|
||||
/**
|
||||
* ReactivePolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class ReactivePolarisCircuitBreaker implements ReactiveCircuitBreaker {
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
private final ConsumerAPI consumerAPI;
|
||||
|
||||
private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf;
|
||||
|
||||
public ReactivePolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
|
||||
ConsumerAPI consumerAPI,
|
||||
CircuitBreakAPI circuitBreakAPI) {
|
||||
InvokeContext.RequestContext requestContext = new FunctionalDecoratorRequest(
|
||||
new ServiceKey(conf.getNamespace(), conf.getService()), conf.getProtocol(), conf.getMethod(), conf.getPath());
|
||||
requestContext.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
|
||||
requestContext.setResultToErrorCode(new PolarisResultToErrorCode());
|
||||
this.consumerAPI = consumerAPI;
|
||||
this.conf = conf;
|
||||
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(requestContext);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> run(Mono<T> toRun, Function<Throwable, Mono<T>> fallback) {
|
||||
Mono<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
|
||||
if (fallback != null) {
|
||||
toReturn = toReturn.onErrorResume(throwable -> {
|
||||
if (throwable instanceof CallAbortedException) {
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
|
||||
}
|
||||
return fallback.apply(throwable);
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> run(Flux<T> toRun, Function<Throwable, Flux<T>> fallback) {
|
||||
Flux<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
|
||||
if (fallback != null) {
|
||||
toReturn = toReturn.onErrorResume(throwable -> {
|
||||
if (throwable instanceof CallAbortedException) {
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
|
||||
}
|
||||
return fallback.apply(throwable);
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerProperties;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.utils.ThreadPoolUtils;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.client.util.NamedThreadFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
|
||||
/**
|
||||
* ReactivePolarisCircuitBreakerFactory.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class ReactivePolarisCircuitBreakerFactory extends
|
||||
ReactiveCircuitBreakerFactory<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> implements DisposableBean {
|
||||
|
||||
private final CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
private final ConsumerAPI consumerAPI;
|
||||
|
||||
private final ScheduledExecutorService cleanupService = Executors.newSingleThreadScheduledExecutor(
|
||||
new NamedThreadFactory("sct-reactive-circuitbreaker-cleanup", true));
|
||||
|
||||
private Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration =
|
||||
id -> {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder()
|
||||
.namespace(metadata[0])
|
||||
.service(metadata[1])
|
||||
.path(metadata[2])
|
||||
.protocol(metadata[3])
|
||||
.method(metadata[4])
|
||||
.build();
|
||||
};
|
||||
|
||||
public ReactivePolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI,
|
||||
PolarisCircuitBreakerProperties polarisCircuitBreakerProperties) {
|
||||
this.circuitBreakAPI = circuitBreakAPI;
|
||||
this.consumerAPI = consumerAPI;
|
||||
cleanupService.scheduleWithFixedDelay(
|
||||
() -> {
|
||||
getConfigurations().clear();
|
||||
},
|
||||
polarisCircuitBreakerProperties.getConfigurationCleanupInterval(),
|
||||
polarisCircuitBreakerProperties.getConfigurationCleanupInterval(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveCircuitBreaker create(String id) {
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations()
|
||||
.computeIfAbsent(id, defaultConfiguration);
|
||||
return new ReactivePolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2], metadata[3], metadata[4]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(
|
||||
Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
|
||||
this.defaultConfiguration = defaultConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
ThreadPoolUtils.waitAndStopThreadPools(new ExecutorService[] {cleanupService});
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter;
|
||||
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.circuitbreaker.Customizer;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for ReactivePolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = {"reactor.core.publisher.Mono", "reactor.core.publisher.Flux"})
|
||||
@ConditionalOnPolarisCircuitBreakerEnabled
|
||||
@EnableConfigurationProperties(PolarisCircuitBreakerProperties.class)
|
||||
@AutoConfigureAfter(RpcEnhancementAutoConfiguration.class)
|
||||
public class ReactivePolarisCircuitBreakerAutoConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<Customizer<ReactivePolarisCircuitBreakerFactory>> customizers = new ArrayList<>();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SuccessCircuitBreakerReporter.class)
|
||||
public SuccessCircuitBreakerReporter successCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
|
||||
PolarisSDKContextManager polarisSDKContextManager) {
|
||||
return new SuccessCircuitBreakerReporter(properties, polarisSDKContextManager.getCircuitBreakAPI());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ExceptionCircuitBreakerReporter.class)
|
||||
public ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
|
||||
PolarisSDKContextManager polarisSDKContextManager) {
|
||||
return new ExceptionCircuitBreakerReporter(properties, polarisSDKContextManager.getCircuitBreakAPI());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
|
||||
public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(PolarisSDKContextManager polarisSDKContextManager,
|
||||
PolarisCircuitBreakerProperties polarisCircuitBreakerProperties) {
|
||||
ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(
|
||||
polarisSDKContextManager.getCircuitBreakAPI(), polarisSDKContextManager.getConsumerAPI(), polarisCircuitBreakerProperties);
|
||||
customizers.forEach(customizer -> customizer.customize(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CircuitBreakerConfigModifier.class)
|
||||
public CircuitBreakerConfigModifier reactiveCircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
|
||||
return new CircuitBreakerConfigModifier(properties);
|
||||
}
|
||||
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.gateway;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.InvalidPropertyException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
|
||||
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.support.HttpStatusHolder;
|
||||
import org.springframework.cloud.gateway.support.ServiceUnavailableException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFilterFactory.
|
||||
* mostly copy from SpringCloudCircuitBreakerFilterFactory, but create ReactiveCircuitBreaker per request to build method level CircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
|
||||
|
||||
private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;
|
||||
private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;
|
||||
private String routeIdPrefix;
|
||||
// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
|
||||
private volatile DispatcherHandler dispatcherHandler;
|
||||
|
||||
public PolarisCircuitBreakerFilterFactory(
|
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
|
||||
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider,
|
||||
ReactiveDiscoveryClient discoveryClient,
|
||||
DiscoveryLocatorProperties properties
|
||||
) {
|
||||
super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider);
|
||||
this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory;
|
||||
this.dispatcherHandlerProvider = dispatcherHandlerProvider;
|
||||
if (discoveryClient != null && properties != null) {
|
||||
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
|
||||
routeIdPrefix = properties.getRouteIdPrefix();
|
||||
}
|
||||
else {
|
||||
routeIdPrefix = discoveryClient.getClass().getSimpleName() + "_";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addExceptionDetails(Throwable t, ServerWebExchange exchange) {
|
||||
ofNullable(t).ifPresent(
|
||||
exception -> exchange.getAttributes().put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception));
|
||||
}
|
||||
|
||||
private DispatcherHandler getDispatcherHandler() {
|
||||
if (dispatcherHandler == null) {
|
||||
dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
|
||||
}
|
||||
return dispatcherHandler;
|
||||
}
|
||||
|
||||
private String getCircuitBreakerId(Config config) {
|
||||
if (!StringUtils.hasText(config.getName()) && StringUtils.hasText(config.getRouteId())) {
|
||||
if (routeIdPrefix != null && config.getRouteId().startsWith(routeIdPrefix)) {
|
||||
return config.getRouteId().replace(routeIdPrefix, "");
|
||||
}
|
||||
return config.getRouteId();
|
||||
}
|
||||
return config.getName();
|
||||
}
|
||||
|
||||
private boolean isNumeric(String statusString) {
|
||||
try {
|
||||
Integer.parseInt(statusString);
|
||||
return true;
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<HttpStatus> getSeriesStatus(String series) {
|
||||
if (!Arrays.asList("1**", "2**", "3**", "4**", "5**").contains(series)) {
|
||||
throw new InvalidPropertyException(Config.class, "statusCodes", "polaris circuit breaker status code can only be a numeric http status, or a http series pattern, e.g. [\"1**\",\"2**\",\"3**\",\"4**\",\"5**\"]");
|
||||
}
|
||||
HttpStatus[] allHttpStatus = HttpStatus.values();
|
||||
if (series.startsWith("1")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is1xxInformational).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("2")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is2xxSuccessful).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("3")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is3xxRedirection).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("4")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is4xxClientError).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("5")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is5xxServerError).collect(Collectors.toList());
|
||||
}
|
||||
return Arrays.asList(allHttpStatus);
|
||||
}
|
||||
|
||||
private Set<HttpStatus> getDefaultStatus() {
|
||||
return Arrays.stream(HttpStatus.values())
|
||||
.filter(HttpStatus::is5xxServerError)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
Set<HttpStatus> statuses = config.getStatusCodes().stream()
|
||||
.flatMap(statusCode -> {
|
||||
List<HttpStatus> httpStatuses = new ArrayList<>();
|
||||
if (isNumeric(statusCode)) {
|
||||
httpStatuses.add(HttpStatusHolder.parse(statusCode).getHttpStatus());
|
||||
}
|
||||
else {
|
||||
httpStatuses.addAll(getSeriesStatus(statusCode));
|
||||
}
|
||||
return httpStatuses.stream();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (CollectionUtils.isEmpty(statuses)) {
|
||||
statuses.addAll(getDefaultStatus());
|
||||
}
|
||||
String circuitBreakerId = getCircuitBreakerId(config);
|
||||
return new GatewayFilter() {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
|
||||
String serviceName = circuitBreakerId;
|
||||
if (route != null) {
|
||||
serviceName = route.getUri().getHost();
|
||||
}
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
String method = exchange.getRequest().getMethod() == null ?
|
||||
"GET" : exchange.getRequest().getMethod().name();
|
||||
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path + "#http#" + method);
|
||||
return cb.run(
|
||||
chain.filter(exchange).doOnSuccess(v -> {
|
||||
// throw CircuitBreakerStatusCodeException by default for all need checking status
|
||||
// so polaris can report right error status
|
||||
Set<HttpStatus> statusNeedToCheck = new HashSet<>();
|
||||
statusNeedToCheck.addAll(statuses);
|
||||
statusNeedToCheck.addAll(getDefaultStatus());
|
||||
HttpStatus status = exchange.getResponse().getStatusCode();
|
||||
if (statusNeedToCheck.contains(status)) {
|
||||
throw new CircuitBreakerStatusCodeException(status);
|
||||
}
|
||||
}),
|
||||
t -> {
|
||||
// pre-check CircuitBreakerStatusCodeException's status matches input status
|
||||
if (t instanceof CircuitBreakerStatusCodeException) {
|
||||
HttpStatus status = ((CircuitBreakerStatusCodeException) t).getStatusCode();
|
||||
// no need to fallback
|
||||
if (!statuses.contains(status)) {
|
||||
return Mono.error(t);
|
||||
}
|
||||
}
|
||||
// do fallback
|
||||
if (config.getFallbackUri() == null) {
|
||||
// polaris checking
|
||||
if (t instanceof CallAbortedException) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
|
||||
if (fallbackInfo != null) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setRawStatusCode(fallbackInfo.getCode());
|
||||
if (fallbackInfo.getHeaders() != null) {
|
||||
fallbackInfo.getHeaders()
|
||||
.forEach((k, v) -> response.getHeaders().add(k, v));
|
||||
}
|
||||
DataBuffer bodyBuffer = null;
|
||||
if (fallbackInfo.getBody() != null) {
|
||||
byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8);
|
||||
bodyBuffer = response.bufferFactory().wrap(bytes);
|
||||
}
|
||||
return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete();
|
||||
}
|
||||
}
|
||||
return Mono.error(t);
|
||||
}
|
||||
exchange.getResponse().setStatusCode(null);
|
||||
reset(exchange);
|
||||
|
||||
// TODO: copied from RouteToRequestUrlFilter
|
||||
URI uri = exchange.getRequest().getURI();
|
||||
// TODO: assume always?
|
||||
boolean encoded = containsEncodedParts(uri);
|
||||
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
|
||||
.uri(config.getFallbackUri()).scheme(null).build(encoded).toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
addExceptionDetails(t, exchange);
|
||||
|
||||
// Reset the exchange
|
||||
reset(exchange);
|
||||
|
||||
ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build();
|
||||
return getDispatcherHandler().handle(exchange.mutate().request(request).build());
|
||||
})
|
||||
.onErrorResume(t -> handleErrorWithoutFallback(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return filterToStringCreator(PolarisCircuitBreakerFilterFactory.this)
|
||||
.append("name", config.getName()).append("fallback", config.getFallbackUri()).toString();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> handleErrorWithoutFallback(Throwable t) {
|
||||
if (t instanceof java.util.concurrent.TimeoutException) {
|
||||
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
|
||||
}
|
||||
if (t instanceof CallAbortedException) {
|
||||
return Mono.error(new ServiceUnavailableException());
|
||||
}
|
||||
if (t instanceof CircuitBreakerStatusCodeException) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.error(t);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.tencent.cloud.polaris.circuitbreaker.reporter;
|
||||
|
||||
import com.tencent.cloud.common.constant.ContextConstant;
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import com.tencent.polaris.metadata.core.MetadataType;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
|
||||
import static com.tencent.cloud.rpc.enhancement.plugin.PluginOrderConstant.ClientPluginOrder.CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER;
|
||||
|
||||
/**
|
||||
* CircuitBreakerPlugin, do circuit breaker in enhance plugin and record info into metadata.
|
||||
*
|
||||
* @author Shedfree Wu
|
||||
*/
|
||||
public class CircuitBreakerPlugin implements EnhancedPlugin {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SuccessPolarisReporter.class);
|
||||
|
||||
private CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
public CircuitBreakerPlugin(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return CircuitBreakerPlugin.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnhancedPluginType getType() {
|
||||
return EnhancedPluginType.Client.PRE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(EnhancedPluginContext context) throws Throwable {
|
||||
|
||||
EnhancedRequestContext request = context.getRequest();
|
||||
EnhancedResponseContext response = context.getResponse();
|
||||
|
||||
String governanceNamespace = MetadataContextHolder.get().getDisposableMetadata().get(ContextConstant.POLARIS_GOVERNANCE_TARGET_NAMESPACE);
|
||||
if (StringUtils.isEmpty(governanceNamespace)) {
|
||||
governanceNamespace = MetadataContext.LOCAL_NAMESPACE;
|
||||
}
|
||||
|
||||
String host = request.getServiceUrl() != null ? request.getServiceUrl().getHost() : request.getUrl().getHost();
|
||||
String path = request.getServiceUrl() != null ? request.getServiceUrl().getPath() : request.getUrl().getPath();
|
||||
String httpMethod = request.getHttpMethod().name();
|
||||
|
||||
CircuitBreaker circuitBreaker = circuitBreakerFactory.create(governanceNamespace + "#" + host + "#" + path + "#http#" + httpMethod);
|
||||
if (circuitBreaker instanceof PolarisCircuitBreaker) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) circuitBreaker;
|
||||
putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker);
|
||||
putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME, System.currentTimeMillis());
|
||||
|
||||
try {
|
||||
polarisCircuitBreaker.acquirePermission();
|
||||
}
|
||||
catch (CallAbortedException e) {
|
||||
LOG.debug("[CircuitBreakerPlugin] request is aborted. request service url=[{}]", request.getServiceUrl());
|
||||
if (e.getFallbackInfo() != null) {
|
||||
Object fallbackResponse = new PolarisCircuitBreakerHttpResponse(e.getFallbackInfo());
|
||||
putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE, fallbackResponse);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) {
|
||||
LOG.error("SuccessCircuitBreakerReporter runs failed. context=[{}].",
|
||||
context, throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER;
|
||||
}
|
||||
|
||||
private void putMetadataObjectValue(String key, Object value) {
|
||||
MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true).
|
||||
putMetadataObjectValue(key, value);
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.core.type.StandardMethodMetadata;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerRestTemplateBeanPostProcessor.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerRestTemplateBeanPostProcessor implements MergedBeanDefinitionPostProcessor {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ConcurrentHashMap<String, PolarisCircuitBreaker> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public PolarisCircuitBreakerRestTemplateBeanPostProcessor(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
private void checkPolarisCircuitBreakerRestTemplate(PolarisCircuitBreaker polarisCircuitBreaker) {
|
||||
if (
|
||||
StringUtils.hasText(polarisCircuitBreaker.fallback()) &&
|
||||
!PolarisCircuitBreakerFallback.class.toGenericString()
|
||||
.equals(polarisCircuitBreaker.fallbackClass().toGenericString())
|
||||
) {
|
||||
throw new IllegalArgumentException("PolarisCircuitBreaker's fallback and fallbackClass could not set at sametime !");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
|
||||
if (checkAnnotated(beanDefinition, beanType, beanName)) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker;
|
||||
if (beanDefinition.getSource() instanceof StandardMethodMetadata) {
|
||||
polarisCircuitBreaker = ((StandardMethodMetadata) beanDefinition.getSource()).getIntrospectedMethod()
|
||||
.getAnnotation(PolarisCircuitBreaker.class);
|
||||
}
|
||||
else {
|
||||
polarisCircuitBreaker = beanDefinition.getResolvedFactoryMethod()
|
||||
.getAnnotation(PolarisCircuitBreaker.class);
|
||||
}
|
||||
checkPolarisCircuitBreakerRestTemplate(polarisCircuitBreaker);
|
||||
cache.put(beanName, polarisCircuitBreaker);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (cache.containsKey(beanName)) {
|
||||
// add interceptor for each RestTemplate with @PolarisCircuitBreaker annotation
|
||||
StringBuilder interceptorBeanNamePrefix = new StringBuilder();
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = cache.get(beanName);
|
||||
interceptorBeanNamePrefix
|
||||
.append(StringUtils.uncapitalize(
|
||||
PolarisCircuitBreaker.class.getSimpleName()))
|
||||
.append("_")
|
||||
.append(polarisCircuitBreaker.fallback())
|
||||
.append("_")
|
||||
.append(polarisCircuitBreaker.fallbackClass().getSimpleName());
|
||||
RestTemplate restTemplate = (RestTemplate) bean;
|
||||
String interceptorBeanName = interceptorBeanNamePrefix + "@" + bean;
|
||||
CircuitBreakerFactory circuitBreakerFactory = this.applicationContext.getBean(CircuitBreakerFactory.class);
|
||||
registerBean(interceptorBeanName, polarisCircuitBreaker, applicationContext, circuitBreakerFactory, restTemplate);
|
||||
PolarisCircuitBreakerRestTemplateInterceptor polarisCircuitBreakerRestTemplateInterceptor = applicationContext
|
||||
.getBean(interceptorBeanName, PolarisCircuitBreakerRestTemplateInterceptor.class);
|
||||
restTemplate.getInterceptors().add(0, polarisCircuitBreakerRestTemplateInterceptor);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private boolean checkAnnotated(RootBeanDefinition beanDefinition,
|
||||
Class<?> beanType, String beanName) {
|
||||
return beanName != null && beanType == RestTemplate.class
|
||||
&& beanDefinition.getSource() instanceof MethodMetadata
|
||||
&& ((MethodMetadata) beanDefinition.getSource())
|
||||
.isAnnotated(PolarisCircuitBreaker.class.getName());
|
||||
}
|
||||
|
||||
private void registerBean(String interceptorBeanName, PolarisCircuitBreaker polarisCircuitBreaker,
|
||||
ApplicationContext applicationContext, CircuitBreakerFactory circuitBreakerFactory, RestTemplate restTemplate) {
|
||||
// register PolarisCircuitBreakerRestTemplateInterceptor bean
|
||||
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext
|
||||
.getAutowireCapableBeanFactory();
|
||||
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(PolarisCircuitBreakerRestTemplateInterceptor.class);
|
||||
beanDefinitionBuilder.addConstructorArgValue(polarisCircuitBreaker);
|
||||
beanDefinitionBuilder.addConstructorArgValue(applicationContext);
|
||||
beanDefinitionBuilder.addConstructorArgValue(circuitBreakerFactory);
|
||||
beanDefinitionBuilder.addConstructorArgValue(restTemplate);
|
||||
BeanDefinition interceptorBeanDefinition = beanDefinitionBuilder
|
||||
.getRawBeanDefinition();
|
||||
beanFactory.registerBeanDefinition(interceptorBeanName,
|
||||
interceptorBeanDefinition);
|
||||
}
|
||||
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException;
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerRestTemplateInterceptor.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerRestTemplateInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private final PolarisCircuitBreaker polarisCircuitBreaker;
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public PolarisCircuitBreakerRestTemplateInterceptor(
|
||||
PolarisCircuitBreaker polarisCircuitBreaker,
|
||||
ApplicationContext applicationContext,
|
||||
CircuitBreakerFactory circuitBreakerFactory,
|
||||
RestTemplate restTemplate
|
||||
) {
|
||||
this.polarisCircuitBreaker = polarisCircuitBreaker;
|
||||
this.applicationContext = applicationContext;
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
|
||||
try {
|
||||
String httpMethod = "GET";
|
||||
if (request.getMethod() != null) {
|
||||
httpMethod = request.getMethod().name();
|
||||
}
|
||||
return circuitBreakerFactory.create(MetadataContext.LOCAL_NAMESPACE + "#" + request.getURI()
|
||||
.getHost() + "#" + request.getURI().getPath() + "#http#" + httpMethod).run(
|
||||
() -> {
|
||||
try {
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
ResponseErrorHandler errorHandler = restTemplate.getErrorHandler();
|
||||
if (errorHandler.hasError(response)) {
|
||||
errorHandler.handleError(request.getURI(), request.getMethod(), response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
if (StringUtils.hasText(polarisCircuitBreaker.fallback())) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, null, polarisCircuitBreaker.fallback());
|
||||
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
|
||||
}
|
||||
if (!PolarisCircuitBreakerFallback.class.toGenericString()
|
||||
.equals(polarisCircuitBreaker.fallbackClass().toGenericString())) {
|
||||
Method method = ReflectionUtils.findMethod(PolarisCircuitBreakerFallback.class, "fallback");
|
||||
PolarisCircuitBreakerFallback polarisCircuitBreakerFallback = applicationContext.getBean(polarisCircuitBreaker.fallbackClass());
|
||||
return (PolarisCircuitBreakerHttpResponse) ReflectionUtils.invokeMethod(method, polarisCircuitBreakerFallback);
|
||||
}
|
||||
if (t instanceof CallAbortedException) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
|
||||
if (fallbackInfo != null) {
|
||||
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
|
||||
}
|
||||
}
|
||||
throw new FallbackWrapperException(t);
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (FallbackWrapperException e) {
|
||||
// unwrap And Rethrow
|
||||
Throwable underlyingException = e.getCause();
|
||||
if (underlyingException instanceof RuntimeException) {
|
||||
throw (RuntimeException) underlyingException;
|
||||
}
|
||||
throw new IllegalStateException(underlyingException);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.zuul;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import com.netflix.zuul.exception.ZuulException;
|
||||
import com.tencent.cloud.common.constant.OrderConstant;
|
||||
import com.tencent.cloud.common.util.ZuulFilterUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
|
||||
|
||||
/**
|
||||
* Polaris circuit breaker post-processing. Including reporting.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public class PolarisCircuitBreakerPostZuulFilter extends ZuulFilter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerPostZuulFilter.class);
|
||||
|
||||
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public PolarisCircuitBreakerPostZuulFilter(PolarisZuulFallbackFactory polarisZuulFallbackFactory,
|
||||
Environment environment) {
|
||||
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filterType() {
|
||||
return POST_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterOrder() {
|
||||
return OrderConstant.Client.Zuul.CIRCUIT_BREAKER_POST_FILTER_ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
|
||||
return StringUtils.isEmpty(enabled) || enabled.equals("true");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() throws ZuulException {
|
||||
RequestContext context = RequestContext.getCurrentContext();
|
||||
|
||||
HttpServletResponse response = context.getResponse();
|
||||
HttpStatus status = HttpStatus.resolve(response.getStatus());
|
||||
|
||||
Object polarisCircuitBreakerObject = context.get(POLARIS_CIRCUIT_BREAKER);
|
||||
Object startTimeMilliObject = context.get(POLARIS_PRE_ROUTE_TIME);
|
||||
if (polarisCircuitBreakerObject != null && polarisCircuitBreakerObject instanceof PolarisCircuitBreaker
|
||||
&& startTimeMilliObject != null && startTimeMilliObject instanceof Long) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) polarisCircuitBreakerObject;
|
||||
Long startTimeMilli = (Long) startTimeMilliObject;
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
|
||||
if (status != null && status.is5xxServerError()) {
|
||||
Throwable throwable = new CircuitBreakerStatusCodeException(status);
|
||||
responseContext.setError(throwable);
|
||||
|
||||
// fallback if FallbackProvider is implemented.
|
||||
String serviceId = ZuulFilterUtils.getServiceId(context);
|
||||
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
|
||||
if (fallbackProvider != null) {
|
||||
ClientHttpResponse clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, throwable);
|
||||
try {
|
||||
// set status code
|
||||
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
|
||||
// set headers
|
||||
HttpHeaders headers = clientHttpResponse.getHeaders();
|
||||
for (String key : headers.keySet()) {
|
||||
List<String> values = headers.get(key);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
for (String value : values) {
|
||||
context.addZuulResponseHeader(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// set body
|
||||
context.getResponse().setCharacterEncoding("UTF-8");
|
||||
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error("error filter exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responseContext.getError() == null) {
|
||||
polarisCircuitBreaker.onSuccess(responseContext);
|
||||
}
|
||||
else {
|
||||
polarisCircuitBreaker.onError(responseContext);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public class CircuitBreakerStatusCodeException extends HttpStatusCodeException {
|
||||
|
||||
public CircuitBreakerStatusCodeException(HttpStatus statusCode) {
|
||||
super(statusCode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.zuul;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import com.netflix.zuul.exception.ZuulException;
|
||||
import com.tencent.cloud.common.constant.OrderConstant;
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.util.ZuulFilterUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_IS_IN_ROUTING_STATE;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
|
||||
|
||||
/**
|
||||
* Polaris circuit breaker implement in Zuul.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public class PolarisCircuitBreakerZuulFilter extends ZuulFilter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerZuulFilter.class);
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public PolarisCircuitBreakerZuulFilter(
|
||||
CircuitBreakerFactory circuitBreakerFactory,
|
||||
PolarisZuulFallbackFactory polarisZuulFallbackFactory,
|
||||
Environment environment) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filterType() {
|
||||
return PRE_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceId is set after PreDecorationFilter.
|
||||
*
|
||||
* @return filter order
|
||||
*/
|
||||
@Override
|
||||
public int filterOrder() {
|
||||
return OrderConstant.Client.Zuul.CIRCUIT_BREAKER_FILTER_ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
|
||||
return StringUtils.isEmpty(enabled) || enabled.equals("true");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() throws ZuulException {
|
||||
RequestContext context = RequestContext.getCurrentContext();
|
||||
|
||||
String serviceId = ZuulFilterUtils.getServiceId(context);
|
||||
String path = ZuulFilterUtils.getPath(context);
|
||||
String circuitName = com.tencent.polaris.api.utils.StringUtils.isBlank(path) ?
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId :
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId + "#" + path + "#http#" + context.getRequest()
|
||||
.getMethod();
|
||||
CircuitBreaker circuitBreaker = circuitBreakerFactory.create(circuitName);
|
||||
if (circuitBreaker instanceof PolarisCircuitBreaker) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) circuitBreaker;
|
||||
context.set(POLARIS_CIRCUIT_BREAKER, circuitBreaker);
|
||||
try {
|
||||
polarisCircuitBreaker.acquirePermission();
|
||||
}
|
||||
catch (CallAbortedException exception) {
|
||||
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
|
||||
ClientHttpResponse clientHttpResponse;
|
||||
if (fallbackProvider != null) {
|
||||
clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, exception);
|
||||
}
|
||||
else if (exception.getFallbackInfo() != null) {
|
||||
clientHttpResponse = new PolarisCircuitBreakerHttpResponse(exception.getFallbackInfo());
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
try {
|
||||
context.setSendZuulResponse(false);
|
||||
// set status code
|
||||
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
|
||||
// set headers
|
||||
HttpHeaders headers = clientHttpResponse.getHeaders();
|
||||
for (String key : headers.keySet()) {
|
||||
List<String> values = headers.get(key);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
for (String value : values) {
|
||||
context.addZuulResponseHeader(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// set body
|
||||
context.getResponse().setCharacterEncoding("UTF-8");
|
||||
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
|
||||
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", exception.getMessage());
|
||||
PolarisCircuitBreakerUtils.reportStatus(polarisCircuitBreaker.getConsumerAPI(), polarisCircuitBreaker.getConf(), exception);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Return circuit breaker fallback info failed: {}", e.getMessage());
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
context.set(POLARIS_PRE_ROUTE_TIME, Long.valueOf(System.currentTimeMillis()));
|
||||
context.set(POLARIS_IS_IN_ROUTING_STATE, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.common.util.ReflectionUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link ReactivePolarisCircuitBreaker}.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ReactivePolarisCircuitBreakerTest {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
PolarisContextAutoConfiguration.class,
|
||||
RpcEnhancementAutoConfiguration.class,
|
||||
LoadBalancerAutoConfiguration.class,
|
||||
ReactivePolarisCircuitBreakerAutoConfiguration.class))
|
||||
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true")
|
||||
.withPropertyValues("spring.cloud.polaris.circuitbreaker.configuration-cleanup-interval=5000");
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
|
||||
.thenReturn(NAMESPACE_TEST);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
|
||||
.thenReturn(SERVICE_CIRCUIT_BREAKER);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void run() {
|
||||
this.reactiveContextRunner.run(context -> {
|
||||
ReactivePolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(ReactivePolarisCircuitBreakerFactory.class);
|
||||
ReactiveCircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration =
|
||||
polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build();
|
||||
|
||||
polarisCircuitBreakerFactory.configureDefault(id -> configuration);
|
||||
|
||||
assertThat(Mono.just("foobar").transform(cb::run).block()).isEqualTo("foobar");
|
||||
|
||||
assertThat(Mono.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Mono.just("fallback")))
|
||||
.block()).isEqualTo("fallback");
|
||||
|
||||
assertThat(Flux.just("foobar", "hello world").transform(cb::run).collectList().block())
|
||||
.isEqualTo(Arrays.asList("foobar", "hello world"));
|
||||
|
||||
assertThat(Flux.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Flux.just("fallback")))
|
||||
.collectList().block()).isEqualTo(Collections.singletonList("fallback"));
|
||||
|
||||
Method getConfigurationsMethod = ReflectionUtils.findMethod(PolarisCircuitBreakerFactory.class,
|
||||
"getConfigurations");
|
||||
Assertions.assertNotNull(getConfigurationsMethod);
|
||||
ReflectionUtils.makeAccessible(getConfigurationsMethod);
|
||||
Map<?, ?> values = (Map<?, ?>) ReflectionUtils.invokeMethod(getConfigurationsMethod, polarisCircuitBreakerFactory);
|
||||
Assertions.assertNotNull(values);
|
||||
Assertions.assertTrue(values.size() >= 0);
|
||||
|
||||
Utils.sleepUninterrupted(10 * 1000);
|
||||
// clear by cleanupService in ReactivePolarisCircuitBreakerFactory
|
||||
Assertions.assertEquals(0, values.size());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.feign;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author Shedfree Wu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = PolarisCircuitBreakerFeignIntegrationTest.TestConfig.class,
|
||||
properties = {
|
||||
"feign.hystrix.enabled=false",
|
||||
"spring.cloud.gateway.enabled=false",
|
||||
"feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.polaris.namespace=" + NAMESPACE_TEST,
|
||||
"spring.cloud.polaris.service=test"
|
||||
})
|
||||
public class PolarisCircuitBreakerFeignIntegrationDisableFeignHystrixTest {
|
||||
|
||||
@Autowired
|
||||
private PolarisCircuitBreakerFeignIntegrationTest.EchoService echoService;
|
||||
|
||||
@Test
|
||||
public void testFeignClient() {
|
||||
assertThatThrownBy(() -> {
|
||||
echoService.echo("test");
|
||||
}).isInstanceOf(RuntimeException.class)
|
||||
.hasMessageContaining("Load balancer does not have available server for client");
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.feign;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = PolarisCircuitBreakerFeignIntegrationTest.TestConfig.class,
|
||||
properties = {
|
||||
"feign.hystrix.enabled=true",
|
||||
"spring.cloud.gateway.enabled=false",
|
||||
"spring.cloud.polaris.address=grpc://127.0.0.1:10081",
|
||||
"feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.polaris.namespace=" + NAMESPACE_TEST,
|
||||
"spring.cloud.polaris.service=test"
|
||||
})
|
||||
public class PolarisCircuitBreakerFeignIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
|
||||
private static NamingServer namingServer;
|
||||
|
||||
@Autowired
|
||||
private EchoService echoService;
|
||||
|
||||
@Autowired
|
||||
private FooService fooService;
|
||||
|
||||
@Autowired
|
||||
private BarService barService;
|
||||
|
||||
@Autowired
|
||||
private BazService bazService;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
PolarisSDKContextManager.innerDestroy();
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerFeignIntegrationTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextLoads() {
|
||||
assertThat(echoService).isNotNull();
|
||||
assertThat(fooService).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeignClient() throws InvocationTargetException {
|
||||
assertThat(echoService.echo("test")).isEqualTo("echo fallback");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThatThrownBy(() -> {
|
||||
echoService.echo(null);
|
||||
}).isInstanceOf(InvocationTargetException.class);
|
||||
assertThatThrownBy(() -> {
|
||||
fooService.echo("test");
|
||||
}).isInstanceOf(NoFallbackAvailableException.class);
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(barService.bar()).isEqualTo("\"fallback from polaris server\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(bazService.baz()).isEqualTo("\"fallback from polaris server\"");
|
||||
assertThat(fooService.toString()).isNotEqualTo(echoService.toString());
|
||||
assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode());
|
||||
assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "1", fallback = EchoServiceFallback.class)
|
||||
@Primary
|
||||
public interface EchoService {
|
||||
|
||||
@RequestMapping(path = "echo/{str}")
|
||||
String echo(@RequestParam("str") String param) throws InvocationTargetException;
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "2", fallbackFactory = CustomFallbackFactory.class)
|
||||
public interface FooService {
|
||||
|
||||
@RequestMapping("echo/{str}")
|
||||
String echo(@RequestParam("str") String param);
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "3")
|
||||
public interface BarService {
|
||||
|
||||
@RequestMapping(path = "bar")
|
||||
String bar();
|
||||
|
||||
}
|
||||
|
||||
public interface BazService {
|
||||
|
||||
@RequestMapping(path = "baz")
|
||||
String baz();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "4")
|
||||
public interface BazClient extends BazService {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class})
|
||||
@EnableFeignClients
|
||||
public static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public EchoServiceFallback echoServiceFallback() {
|
||||
return new EchoServiceFallback();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomFallbackFactory customFallbackFactory() {
|
||||
return new CustomFallbackFactory();
|
||||
}
|
||||
}
|
||||
|
||||
public static class EchoServiceFallback implements EchoService {
|
||||
|
||||
@Override
|
||||
public String echo(@RequestParam("str") String param) throws InvocationTargetException {
|
||||
if (param == null) {
|
||||
throw new InvocationTargetException(new Exception(), "test InvocationTargetException");
|
||||
}
|
||||
return "echo fallback";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooServiceFallback implements FooService {
|
||||
|
||||
@Override
|
||||
public String echo(@RequestParam("str") String param) {
|
||||
throw new NoFallbackAvailableException("fallback", new RuntimeException());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomFallbackFactory
|
||||
implements FallbackFactory<FooService> {
|
||||
|
||||
private final FooService fooService = new FooServiceFallback();
|
||||
|
||||
@Override
|
||||
public FooService create(Throwable throwable) {
|
||||
return fooService;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.gateway;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {
|
||||
"spring.cloud.gateway.enabled=true",
|
||||
"spring.cloud.polaris.namespace=" + NAMESPACE_TEST,
|
||||
"spring.cloud.polaris.service=test",
|
||||
"spring.main.web-application-type=reactive"
|
||||
},
|
||||
classes = PolarisCircuitBreakerGatewayIntegrationTest.TestApplication.class
|
||||
)
|
||||
@ActiveProfiles("test-gateway")
|
||||
@AutoConfigureWebTestClient(timeout = "1000000")
|
||||
public class PolarisCircuitBreakerGatewayIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
|
||||
private static NamingServer namingServer;
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webClient;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
PolarisSDKContextManager.innerDestroy();
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerGatewayIntegrationTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallback() throws Exception {
|
||||
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config();
|
||||
applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString();
|
||||
|
||||
webClient
|
||||
.get().uri("/err")
|
||||
.header("Host", "www.circuitbreaker.com")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.consumeWith(
|
||||
response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-skip-fallback")
|
||||
.header("Host", "www.circuitbreaker-skip-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
// this should be 200, but for some unknown reason, GitHub action run failed in windows, so we skip this check
|
||||
webClient
|
||||
.get().uri("/err-skip-fallback")
|
||||
.header("Host", "www.circuitbreaker-skip-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
public static class TestApplication {
|
||||
|
||||
@Bean
|
||||
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
|
||||
Set<String> codeSets = new HashSet<>();
|
||||
codeSets.add("4**");
|
||||
codeSets.add("5**");
|
||||
return builder.routes()
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setStatusCodes(codeSets)
|
||||
.setFallbackUri("forward:/fallback")
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("http://httpbin.org:80"))
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker-skip-fallback.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setStatusCodes(Collections.singleton("5**"))
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("http://httpbin.org:80"))
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker-no-fallback.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("lb://" + TEST_SERVICE_NAME))
|
||||
.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Controller {
|
||||
@RequestMapping("/fallback")
|
||||
public Mono<String> fallback() {
|
||||
return Mono.just("fallback");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.client.ExpectedCount;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = PolarisCircuitBreakerRestTemplateIntegrationTest.TestConfig.class,
|
||||
properties = {
|
||||
"spring.cloud.gateway.enabled=false",
|
||||
"spring.cloud.polaris.address=grpc://127.0.0.1:10081",
|
||||
"feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.polaris.namespace=" + NAMESPACE_TEST,
|
||||
"spring.cloud.polaris.service=test"
|
||||
})
|
||||
public class PolarisCircuitBreakerRestTemplateIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
|
||||
private static NamingServer namingServer;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("defaultRestTemplate")
|
||||
private RestTemplate defaultRestTemplate;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromPolaris")
|
||||
private RestTemplate restTemplateFallbackFromPolaris;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode")
|
||||
private RestTemplate restTemplateFallbackFromCode;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode2")
|
||||
private RestTemplate restTemplateFallbackFromCode2;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode3")
|
||||
private RestTemplate restTemplateFallbackFromCode3;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode4")
|
||||
private RestTemplate restTemplateFallbackFromCode4;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
PolarisSDKContextManager.innerDestroy();
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME);
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerRestTemplateIntegrationTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestTemplate() throws URISyntaxException {
|
||||
MockRestServiceServer mockServer = MockRestServiceServer.createServer(defaultRestTemplate);
|
||||
mockServer
|
||||
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andRespond(withStatus(HttpStatus.OK).body("OK"));
|
||||
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("OK");
|
||||
mockServer.verify();
|
||||
mockServer.reset();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
mockServer
|
||||
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andRespond(withStatus(HttpStatus.BAD_GATEWAY).headers(headers).body("BAD_GATEWAY"));
|
||||
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("fallback");
|
||||
mockServer.verify();
|
||||
mockServer.reset();
|
||||
assertThatThrownBy(() -> {
|
||||
restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class);
|
||||
}).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(restTemplateFallbackFromCode.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode2.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode3.getForEntity("/example/service/b/info", String.class)
|
||||
.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode4.getForObject("/example/service/b/info", String.class)).isEqualTo("fallback");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class)).isEqualTo("\"fallback from polaris server\"");
|
||||
// just for code coverage
|
||||
PolarisCircuitBreakerHttpResponse response = ((CustomPolarisCircuitBreakerFallback) applicationContext.getBean("customPolarisCircuitBreakerFallback")).fallback();
|
||||
assertThat(response.getStatusText()).isEqualTo("OK");
|
||||
assertThat(response.getFallbackInfo().getCode()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class})
|
||||
@EnableFeignClients
|
||||
public static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker(fallback = "fallback")
|
||||
public RestTemplate defaultRestTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker
|
||||
public RestTemplate restTemplateFallbackFromPolaris() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback.class)
|
||||
public RestTemplate restTemplateFallbackFromCode() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback2.class)
|
||||
public RestTemplate restTemplateFallbackFromCode2() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback3.class)
|
||||
public RestTemplate restTemplateFallbackFromCode3() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker(fallback = "fallback")
|
||||
public RestTemplate restTemplateFallbackFromCode4() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback customPolarisCircuitBreakerFallback() {
|
||||
return new CustomPolarisCircuitBreakerFallback();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback2 customPolarisCircuitBreakerFallback2() {
|
||||
return new CustomPolarisCircuitBreakerFallback2();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback3 customPolarisCircuitBreakerFallback3() {
|
||||
return new CustomPolarisCircuitBreakerFallback3();
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/example/service/b")
|
||||
public class ServiceBController {
|
||||
|
||||
/**
|
||||
* Get service information.
|
||||
*
|
||||
* @return service information
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public String info() {
|
||||
return "hello world ! I'm a service B1";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200,
|
||||
new HashMap<String, String>() {{
|
||||
put("xxx", "xxx");
|
||||
}},
|
||||
"\"this is a fallback class\"");
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback2 implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200,
|
||||
"\"this is a fallback class\""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback3 implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
|
||||
*
|
||||
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.polaris.context;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
|
||||
public class CircuitBreakerStatusCodeException extends HttpStatusCodeException {
|
||||
|
||||
public CircuitBreakerStatusCodeException(HttpStatus statusCode) {
|
||||
super(statusCode);
|
||||
}
|
||||
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the BSD 3-Clause License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://opensource.org/licenses/BSD-3-Clause
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.tencent.cloud.rpc.enhancement.resttemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.StaticMetadataManager;
|
||||
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.DefaultEnhancedPluginRunner;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.serviceregistry.Registration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class EnhancedRestTemplateInterceptorTest {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
@Mock
|
||||
private RpcEnhancementReporterProperties reporterProperties;
|
||||
@Mock
|
||||
private SDKContext sdkContext;
|
||||
@Mock
|
||||
Registration registration;
|
||||
@Mock
|
||||
private ClientHttpRequestExecution mockClientHttpRequestExecution;
|
||||
@Mock
|
||||
private ClientHttpResponse mockClientHttpResponse;
|
||||
@Mock
|
||||
private HttpRequest mockHttpRequest;
|
||||
@Mock
|
||||
private HttpHeaders mockHttpHeaders;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
|
||||
.thenReturn("unit-test");
|
||||
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
||||
MetadataLocalProperties metadataLocalProperties = mock(MetadataLocalProperties.class);
|
||||
StaticMetadataManager staticMetadataManager = mock(StaticMetadataManager.class);
|
||||
doReturn(metadataLocalProperties).when(applicationContext).getBean(MetadataLocalProperties.class);
|
||||
doReturn(staticMetadataManager).when(applicationContext).getBean(StaticMetadataManager.class);
|
||||
mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext).thenReturn(applicationContext);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
|
||||
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun() throws IOException, URISyntaxException {
|
||||
|
||||
ClientHttpResponse actualResult;
|
||||
final byte[] inputBody = null;
|
||||
|
||||
URI uri = new URI("http://0.0.0.0/");
|
||||
doReturn(uri).when(mockHttpRequest).getURI();
|
||||
doReturn(HttpMethod.GET).when(mockHttpRequest).getMethod();
|
||||
doReturn(mockHttpHeaders).when(mockHttpRequest).getHeaders();
|
||||
doReturn(mockClientHttpResponse).when(mockClientHttpRequestExecution).execute(mockHttpRequest, inputBody);
|
||||
|
||||
EnhancedRestTemplateInterceptor reporter = new EnhancedRestTemplateInterceptor(new DefaultEnhancedPluginRunner(new ArrayList<>(), registration, null));
|
||||
actualResult = reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution);
|
||||
assertThat(actualResult).isEqualTo(mockClientHttpResponse);
|
||||
|
||||
actualResult = reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution);
|
||||
assertThat(actualResult).isEqualTo(mockClientHttpResponse);
|
||||
|
||||
doThrow(new SocketTimeoutException()).when(mockClientHttpRequestExecution).execute(mockHttpRequest, inputBody);
|
||||
assertThatThrownBy(() -> reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution)).isInstanceOf(SocketTimeoutException.class);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in new issue