feature: improve circuit breaker usage (#913)

pull/921/head
Shanyou Yu (Sean Yu) 2 years ago committed by GitHub
parent 6daa381190
commit 097dad88ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,4 +4,5 @@
- [fix:fix log feign response stream close bug.](https://github.com/Tencent/spring-cloud-tencent/pull/897) - [fix:fix log feign response stream close bug.](https://github.com/Tencent/spring-cloud-tencent/pull/897)
- [fix:remove the secondary report.](https://github.com/Tencent/spring-cloud-tencent/pull/899) - [fix:remove the secondary report.](https://github.com/Tencent/spring-cloud-tencent/pull/899)
- [fix:optimize instance circuit beaker.](https://github.com/Tencent/spring-cloud-tencent/pull/909) - [fix:optimize instance circuit beaker.](https://github.com/Tencent/spring-cloud-tencent/pull/909)
- [fix:optimize multi service registration and discovery.](https://github.com/Tencent/spring-cloud-tencent/pull/912) - [fix:optimize multi service registration and discovery.](https://github.com/Tencent/spring-cloud-tencent/pull/912)
- [feature: improve circuit breaker usage.](https://github.com/Tencent/spring-cloud-tencent/pull/913)

@ -91,7 +91,7 @@
<revision>1.11.0-2021.0.6-SNAPSHOT</revision> <revision>1.11.0-2021.0.6-SNAPSHOT</revision>
<!-- Spring Framework --> <!-- Spring Framework -->
<spring.framework.version>5.3.26</spring.framework.version> <spring.framework.version>5.3.25</spring.framework.version>
<!-- Spring Boot --> <!-- Spring Boot -->
<spring.boot.version>2.6.14</spring.boot.version> <spring.boot.version>2.6.14</spring.boot.version>

@ -22,6 +22,7 @@ import java.util.List;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory; import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory;
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier; import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerRestTemplateBeanPostProcessor;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
@ -31,9 +32,11 @@ import com.tencent.polaris.client.api.SDKContext;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -72,4 +75,11 @@ public class PolarisCircuitBreakerAutoConfiguration {
return new CircuitBreakerConfigModifier(properties); return new CircuitBreakerConfigModifier(properties);
} }
@Bean
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
public static PolarisCircuitBreakerRestTemplateBeanPostProcessor polarisCircuitBreakerRestTemplateBeanPostProcessor(
ApplicationContext applicationContext) {
return new PolarisCircuitBreakerRestTemplateBeanPostProcessor(applicationContext);
}
} }

@ -28,7 +28,12 @@ import org.springframework.context.annotation.Import;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("spring.cloud.polaris.enabled") @ConditionalOnProperty("spring.cloud.polaris.enabled")
@Import({PolarisCircuitBreakerAutoConfiguration.class, ReactivePolarisCircuitBreakerAutoConfiguration.class, PolarisCircuitBreakerFeignClientAutoConfiguration.class}) @Import({
PolarisCircuitBreakerAutoConfiguration.class,
ReactivePolarisCircuitBreakerAutoConfiguration.class,
PolarisCircuitBreakerFeignClientAutoConfiguration.class,
GatewayPolarisCircuitBreakerAutoConfiguration.class
})
public class PolarisCircuitBreakerBootstrapConfiguration { public class PolarisCircuitBreakerBootstrapConfiguration {
} }

@ -18,14 +18,20 @@
package com.tencent.cloud.polaris.circuitbreaker.config; package com.tencent.cloud.polaris.circuitbreaker.config;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver; import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignCircuitBreaker;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignCircuitBreakerTargeter;
import feign.Feign; import feign.Feign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.cloud.openfeign.Targeter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/** /**
* PolarisCircuitBreakerFeignClientAutoConfiguration. * PolarisCircuitBreakerFeignClientAutoConfiguration.
@ -43,4 +49,19 @@ public class PolarisCircuitBreakerFeignClientAutoConfiguration {
return new PolarisCircuitBreakerNameResolver(); return new PolarisCircuitBreakerNameResolver();
} }
@Bean
@ConditionalOnBean(CircuitBreakerFactory.class)
@ConditionalOnMissingBean(Targeter.class)
public Targeter polarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) {
return new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver);
}
@Bean
@Scope("prototype")
@ConditionalOnBean(CircuitBreakerFactory.class)
@ConditionalOnMissingBean(Feign.Builder.class)
public Feign.Builder circuitBreakerFeignBuilder() {
return PolarisFeignCircuitBreaker.builder();
}
} }

@ -60,7 +60,7 @@ public class ReactivePolarisCircuitBreakerAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class) @ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
public ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI) { public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI) {
ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI); ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI);
customizers.forEach(customizer -> customizer.customize(factory)); customizers.forEach(customizer -> customizer.customize(factory));
return factory; return factory;

@ -0,0 +1,97 @@
/*
* 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.circuitbreaker.feign;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import feign.Request;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import org.springframework.cloud.openfeign.FallbackFactory;
/**
* PolarisCircuitBreakerFallbackFactory.
*
* @author sean yu
*/
public class PolarisCircuitBreakerFallbackFactory implements FallbackFactory {
private final Decoder decoder;
public PolarisCircuitBreakerFallbackFactory(Decoder decoder) {
this.decoder = decoder;
}
@Override
public Object create(Throwable t) {
return new DefaultFallback(t, decoder);
}
public class DefaultFallback {
private final Throwable t;
private final Decoder decoder;
public DefaultFallback(Throwable t, Decoder decoder) {
this.t = t;
this.decoder = decoder;
}
public Object fallback(Method method) {
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
Response.Builder responseBuilder = Response.builder()
.status(fallbackInfo.getCode());
if (fallbackInfo.getHeaders() != null) {
Map<String, Collection<String>> headers = new HashMap<>();
fallbackInfo.getHeaders().forEach((k, v) -> headers.put(k, Collections.singleton(v)));
responseBuilder.headers(headers);
}
if (fallbackInfo.getBody() != null) {
responseBuilder.body(fallbackInfo.getBody(), StandardCharsets.UTF_8);
}
// Feign Response need a nonnull Request,
// which is not important in fallback response (no real request),
// so we create a fake one
Request fakeRequest = Request.create(Request.HttpMethod.GET, "/", new HashMap<>(), Request.Body.empty(), new RequestTemplate());
responseBuilder.request(fakeRequest);
try (Response response = responseBuilder.build()) {
return decoder.decode(response, method.getGenericReturnType());
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
throw new IllegalStateException(t);
}
}
}

@ -36,6 +36,7 @@ public class PolarisCircuitBreakerNameResolver implements CircuitBreakerNameReso
@Override @Override
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) { public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
String serviceName = target.name();
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class); RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
String path = ""; String path = "";
if (requestMapping != null) { if (requestMapping != null) {
@ -44,8 +45,8 @@ public class PolarisCircuitBreakerNameResolver implements CircuitBreakerNameReso
requestMapping.path()[0]; requestMapping.path()[0];
} }
return "".equals(path) ? return "".equals(path) ?
MetadataContext.LOCAL_NAMESPACE + "#" + feignClientName : MetadataContext.LOCAL_NAMESPACE + "#" + serviceName :
MetadataContext.LOCAL_NAMESPACE + "#" + feignClientName + "#" + path; MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path;
} }
} }

@ -0,0 +1,94 @@
/*
* 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.circuitbreaker.feign;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
/**
* PolarisFeignCircuitBreaker, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreaker}, but giving Polaris modification.
*
* @author sean yu
*/
public final class PolarisFeignCircuitBreaker {
private PolarisFeignCircuitBreaker() {
throw new IllegalStateException("Don't instantiate a utility class");
}
/**
* @return builder for Feign CircuitBreaker integration
*/
public static PolarisFeignCircuitBreaker.Builder builder() {
return new PolarisFeignCircuitBreaker.Builder();
}
/**
* Builder for Feign CircuitBreaker integration.
*/
public static final class Builder extends Feign.Builder {
public Builder() {
}
private CircuitBreakerFactory circuitBreakerFactory;
private String feignClientName;
private CircuitBreakerNameResolver circuitBreakerNameResolver;
public PolarisFeignCircuitBreaker.Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
return this;
}
public PolarisFeignCircuitBreaker.Builder feignClientName(String feignClientName) {
this.feignClientName = feignClientName;
return this;
}
public PolarisFeignCircuitBreaker.Builder circuitBreakerNameResolver(CircuitBreakerNameResolver circuitBreakerNameResolver) {
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
return this;
}
public <T> T target(Target<T> target, T fallback) {
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
}
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
return build(fallbackFactory).newInstance(target);
}
@Override
public <T> T target(Target<T> target) {
return build(null).newInstance(target);
}
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler(
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, this.decoder));
return this.build();
}
}
}

@ -0,0 +1,200 @@
/*
* 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.circuitbreaker.feign;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.codec.Decoder;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import static feign.Util.checkNotNull;
/**
* PolarisFeignCircuitBreakerInvocationHandler, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler}, but giving Polaris modification.
*
* @author sean yu
*/
public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final CircuitBreakerFactory factory;
private final String feignClientName;
private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
private final FallbackFactory<?> nullableFallbackFactory;
private final Map<Method, Method> fallbackMethodMap;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
private final Decoder decoder;
public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory,
CircuitBreakerNameResolver circuitBreakerNameResolver, Decoder decoder) {
this.factory = factory;
this.feignClientName = feignClientName;
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.nullableFallbackFactory = nullableFallbackFactory;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
this.decoder = decoder;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
// early exit if the invoked method is from java.lang.Object
// code is the same as ReflectiveFeign.FeignInvocationHandler
if ("equals".equals(method.getName())) {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
}
catch (IllegalArgumentException e) {
return false;
}
}
else if ("hashCode".equals(method.getName())) {
return hashCode();
}
else if ("toString".equals(method.getName())) {
return toString();
}
String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method);
CircuitBreaker circuitBreaker = factory.create(circuitName);
Supplier<Object> supplier = asSupplier(method, args);
Function<Throwable, Object> fallbackFunction;
if (this.nullableFallbackFactory != null) {
fallbackFunction = throwable -> {
Object fallback = this.nullableFallbackFactory.create(throwable);
try {
return this.fallbackMethodMap.get(method).invoke(fallback, args);
}
catch (Exception exception) {
unwrapAndRethrow(exception);
}
return null;
};
}
else {
fallbackFunction = throwable -> {
PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback =
(PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(throwable);
return fallback.fallback(method);
};
}
return circuitBreaker.run(supplier, fallbackFunction);
}
private void unwrapAndRethrow(Exception exception) {
if (exception instanceof InvocationTargetException || exception instanceof NoFallbackAvailableException) {
Throwable underlyingException = exception.getCause();
if (underlyingException instanceof RuntimeException) {
throw (RuntimeException) underlyingException;
}
if (underlyingException != null) {
throw new IllegalStateException(underlyingException);
}
throw new IllegalStateException(exception);
}
}
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
final Thread caller = Thread.currentThread();
return () -> {
boolean isAsync = caller != Thread.currentThread();
try {
if (isAsync) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
return dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
finally {
if (isAsync) {
RequestContextHolder.resetRequestAttributes();
}
}
};
}
/**
* If the method param of {@link InvocationHandler#invoke(Object, Method, Object[])}
* is not accessible, i.e in a package-private interface, the fallback call will cause
* of access restrictions. But methods in dispatch are copied methods. So setting
* access to dispatch method doesn't take effect to the method in
* InvocationHandler.invoke. Use map to store a copy of method to invoke the fallback
* to bypass this and reducing the count of reflection calls.
* @return cached methods map for fallback invoking
*/
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
Map<Method, Method> result = new LinkedHashMap<>();
for (Method method : dispatch.keySet()) {
method.setAccessible(true);
result.put(method, method);
}
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) {
PolarisFeignCircuitBreakerInvocationHandler other = (PolarisFeignCircuitBreakerInvocationHandler) obj;
return this.target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return this.target.hashCode();
}
@Override
public String toString() {
return this.target.toString();
}
}

@ -0,0 +1,103 @@
/*
* 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.circuitbreaker.feign;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.cloud.openfeign.Targeter;
import org.springframework.util.StringUtils;
/**
* PolarisFeignCircuitBreakerTargeter, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerTargeter}, but giving Polaris modification.
*
* @author sean yu
*/
public class PolarisFeignCircuitBreakerTargeter implements Targeter {
private final CircuitBreakerFactory circuitBreakerFactory;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
public PolarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) {
this.circuitBreakerFactory = circuitBreakerFactory;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
}
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
if (!(feign instanceof PolarisFeignCircuitBreaker.Builder)) {
return feign.target(target);
}
PolarisFeignCircuitBreaker.Builder builder = (PolarisFeignCircuitBreaker.Builder) feign;
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() : factory.getContextId();
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(name, context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
}
return builder(name, builder).target(target);
}
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target, PolarisFeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
return builder(feignClientName, builder).target(target, fallbackFactory);
}
private <T> T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget<T> target,
PolarisFeignCircuitBreaker.Builder builder, Class<?> fallback) {
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
return builder(feignClientName, builder).target(target, fallbackInstance);
}
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
Class<?> beanType, Class<T> targetType) {
Object fallbackInstance = context.getInstance(feignClientName, beanType);
if (fallbackInstance == null) {
throw new IllegalStateException(
String.format("No " + fallbackMechanism + " instance of type %s found for feign client %s",
beanType, feignClientName));
}
if (!targetType.isAssignableFrom(beanType)) {
throw new IllegalStateException(String.format("Incompatible " + fallbackMechanism
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
beanType, targetType, feignClientName));
}
return (T) fallbackInstance;
}
private PolarisFeignCircuitBreaker.Builder builder(String feignClientName, PolarisFeignCircuitBreaker.Builder builder) {
return builder
.circuitBreakerFactory(circuitBreakerFactory)
.feignClientName(feignClientName)
.circuitBreakerNameResolver(circuitBreakerNameResolver);
}
}

@ -19,16 +19,18 @@ package com.tencent.cloud.polaris.circuitbreaker.gateway;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger; import reactor.core.publisher.Flux;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.InvalidPropertyException;
@ -40,10 +42,14 @@ import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; 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.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServiceUnavailableException; import org.springframework.cloud.gateway.support.ServiceUnavailableException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest; 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.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@ -54,6 +60,7 @@ import static java.util.Optional.ofNullable;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; 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.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_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.containsEncodedParts;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset;
@ -65,8 +72,6 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.r
*/ */
public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory { public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerFilterFactory.class);
private String routeIdPrefix; private String routeIdPrefix;
private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;
@ -150,6 +155,12 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
return Arrays.asList(allHttpStatus); return Arrays.asList(allHttpStatus);
} }
private Set<HttpStatus> getDefaultStatus() {
return Arrays.stream(HttpStatus.values())
.filter(HttpStatus::is5xxServerError)
.collect(Collectors.toSet());
}
@Override @Override
public GatewayFilter apply(Config config) { public GatewayFilter apply(Config config) {
Set<HttpStatus> statuses = config.getStatusCodes().stream() Set<HttpStatus> statuses = config.getStatusCodes().stream()
@ -165,40 +176,82 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (CollectionUtils.isEmpty(statuses)) {
statuses.addAll(getDefaultStatus());
}
String circuitBreakerId = getCircuitBreakerId(config); String circuitBreakerId = getCircuitBreakerId(config);
return new GatewayFilter() { return new GatewayFilter() {
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 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 path = exchange.getRequest().getPath().value();
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(circuitBreakerId + "#" + path); ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(serviceName + "#" + path);
return cb.run(chain.filter(exchange).doOnSuccess(v -> { return cb.run(
if (statuses.contains(exchange.getResponse().getStatusCode())) { chain.filter(exchange)
HttpStatus status = exchange.getResponse().getStatusCode(); .doOnSuccess(v -> {
throw new CircuitBreakerStatusCodeException(status); // throw CircuitBreakerStatusCodeException by default for all need checking status
} // so polaris can report right error status
}), t -> { Set<HttpStatus> statusNeedToCheck = new HashSet<>();
if (config.getFallbackUri() == null) { statusNeedToCheck.addAll(statuses);
return Mono.error(t); statusNeedToCheck.addAll(getDefaultStatus());
} HttpStatus status = exchange.getResponse().getStatusCode();
if (statusNeedToCheck.contains(status)) {
exchange.getResponse().setStatusCode(null); throw new CircuitBreakerStatusCodeException(status);
reset(exchange); }
}),
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 // TODO: copied from RouteToRequestUrlFilter
URI uri = exchange.getRequest().getURI(); URI uri = exchange.getRequest().getURI();
// TODO: assume always? // TODO: assume always?
boolean encoded = containsEncodedParts(uri); boolean encoded = containsEncodedParts(uri);
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null) URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
.uri(config.getFallbackUri()).scheme(null).build(encoded).toUri(); .uri(config.getFallbackUri()).scheme(null).build(encoded).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
addExceptionDetails(t, exchange); addExceptionDetails(t, exchange);
// Reset the exchange // Reset the exchange
reset(exchange); reset(exchange);
ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build(); ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build();
return getDispatcherHandler().handle(exchange.mutate().request(request).build()); return getDispatcherHandler().handle(exchange.mutate().request(request).build());
}).onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError())); })
.onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError()));
} }
@Override @Override
@ -216,9 +269,11 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t)); return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
} }
if (t instanceof CallAbortedException) { if (t instanceof CallAbortedException) {
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", t.getMessage());
return Mono.error(new ServiceUnavailableException()); return Mono.error(new ServiceUnavailableException());
} }
if (t instanceof CircuitBreakerStatusCodeException) {
return Mono.empty();
}
if (resumeWithoutError) { if (resumeWithoutError) {
return Mono.empty(); return Mono.empty();
} }

@ -0,0 +1,54 @@
/*
* 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.circuitbreaker.resttemplate;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* PolarisCircuitBreaker annotation.
* if coded fallback or fallbackClass provided, RestTemplate will always return fallback when any exception occurs,
* if none coded fallback or fallbackClass provided, RestTemplate will return fallback response from Polaris server when fallback occurs.
* fallback and fallbackClass cannot provide at same time.
*
* @author sean yu
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PolarisCircuitBreaker {
/**
* a fallback string, will return a response { status: 200, body: fallback string} when any exception occurs.
*
* @return fallback string
*/
String fallback() default "";
/**
* a fallback Class, will return a PolarisCircuitBreakerHttpResponse when any exception occurs.
* fallback Class must be a spring bean.
*
* @return PolarisCircuitBreakerFallback
*/
Class<? extends PolarisCircuitBreakerFallback> fallbackClass() default PolarisCircuitBreakerFallback.class;
}

@ -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.circuitbreaker.resttemplate;
/**
* PolarisCircuitBreakerFallback.
*
* @author sean yu
*/
public interface PolarisCircuitBreakerFallback {
PolarisCircuitBreakerHttpResponse fallback();
}

@ -0,0 +1,105 @@
/*
* 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.circuitbreaker.resttemplate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.AbstractClientHttpResponse;
import static com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER;
/**
* PolarisCircuitBreakerHttpResponse.
*
* @author sean yu
*/
public class PolarisCircuitBreakerHttpResponse extends AbstractClientHttpResponse {
private final CircuitBreakerStatus.FallbackInfo fallbackInfo;
private HttpHeaders headers = new HttpHeaders();
private InputStream body;
public PolarisCircuitBreakerHttpResponse(int code) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, null));
}
public PolarisCircuitBreakerHttpResponse(int code, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, body));
}
public PolarisCircuitBreakerHttpResponse(int code, Map<String, String> headers, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, headers, body));
}
PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) {
this.fallbackInfo = fallbackInfo;
headers.add(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER, "true");
if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders().forEach(headers::add);
}
if (fallbackInfo.getBody() != null) {
body = new ByteArrayInputStream(fallbackInfo.getBody().getBytes());
}
}
@Override
public final int getRawStatusCode() {
return fallbackInfo.getCode();
}
@Override
public final String getStatusText() {
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
return (status != null ? status.getReasonPhrase() : "");
}
@Override
public final void close() {
if (this.body != null) {
try {
this.body.close();
}
catch (IOException e) {
// Ignore exception on close...
}
}
}
@Override
public final InputStream getBody() {
return this.body;
}
@Override
public final HttpHeaders getHeaders() {
return this.headers;
}
public CircuitBreakerStatus.FallbackInfo getFallbackInfo() {
return this.fallbackInfo;
}
}

@ -0,0 +1,125 @@
/*
* 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.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;
public PolarisCircuitBreakerRestTemplateBeanPostProcessor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private final ConcurrentHashMap<String, PolarisCircuitBreaker> cache = new ConcurrentHashMap<>();
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);
}
}

@ -0,0 +1,111 @@
/*
* 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.circuitbreaker.resttemplate;
import java.io.IOException;
import java.lang.reflect.Method;
import com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter;
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;
import static com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.HEADER_HAS_ERROR;
/**
* 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 {
return circuitBreakerFactory.create(request.getURI().getHost() + "#" + request.getURI().getPath()).run(
() -> {
try {
ClientHttpResponse response = execution.execute(request, body);
// pre handle response error
// EnhancedRestTemplateReporter always return true,
// so we need to check header set by EnhancedRestTemplateReporter
ResponseErrorHandler errorHandler = restTemplate.getErrorHandler();
boolean hasError = errorHandler.hasError(response);
if (errorHandler instanceof EnhancedRestTemplateReporter) {
hasError = Boolean.parseBoolean(response.getHeaders().getFirst(HEADER_HAS_ERROR));
}
if (hasError) {
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 IllegalStateException(t);
}
);
}
}

@ -18,7 +18,25 @@
package com.tencent.cloud.polaris.circuitbreaker; package com.tencent.cloud.polaris.circuitbreaker;
import java.io.BufferedReader;
import java.io.IOException;
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.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration; import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
import com.tencent.polaris.client.util.Utils;
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
import com.tencent.polaris.test.common.TestUtils;
import com.tencent.polaris.test.mock.discovery.NamingServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -26,6 +44,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest; 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.EnableFeignClients;
import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
@ -36,6 +55,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@ -55,6 +75,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
@DirtiesContext @DirtiesContext
public class PolarisCircuitBreakerFeignIntegrationTest { public class PolarisCircuitBreakerFeignIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
@Autowired @Autowired
private EchoService echoService; private EchoService echoService;
@ -67,6 +89,15 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
@Autowired @Autowired
private BazService bazService; private BazService bazService;
private static NamingServer namingServer;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test @Test
public void contextLoads() throws Exception { public void contextLoads() throws Exception {
assertThat(echoService).isNotNull(); assertThat(echoService).isNotNull();
@ -74,18 +105,19 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
} }
@Test @Test
public void testFeignClient() { public void testFeignClient() throws InvocationTargetException {
assertThat(echoService.echo("test")).isEqualTo("echo fallback"); assertThat(echoService.echo("test")).isEqualTo("echo fallback");
assertThat(fooService.echo("test")).isEqualTo("foo fallback"); Utils.sleepUninterrupted(2000);
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
barService.bar(); echoService.echo(null);
}).isInstanceOf(Exception.class); }).isInstanceOf(Exception.class);
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
bazService.baz(); fooService.echo("test");
}).isInstanceOf(Exception.class); }).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.toString()).isNotEqualTo(echoService.toString());
assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode()); assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode());
assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE); assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE);
@ -107,17 +139,39 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
return new CustomFallbackFactory(); return new CustomFallbackFactory();
} }
@Bean
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
try {
namingServer = NamingServer.startNamingServer(10081);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
InputStream inputStream = PolarisCircuitBreakerMockServerTest.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);
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
}
} }
@FeignClient(value = "test-service", fallback = EchoServiceFallback.class) @FeignClient(value = TEST_SERVICE_NAME, contextId = "1", fallback = EchoServiceFallback.class)
public interface EchoService { public interface EchoService {
@RequestMapping(path = "echo/{str}") @RequestMapping(path = "echo/{str}")
String echo(@RequestParam("str") String param); String echo(@RequestParam("str") String param) throws InvocationTargetException;
} }
@FeignClient(value = "foo-service", fallbackFactory = CustomFallbackFactory.class) @FeignClient(value = TEST_SERVICE_NAME, contextId = "2", fallbackFactory = CustomFallbackFactory.class)
public interface FooService { public interface FooService {
@RequestMapping("echo/{str}") @RequestMapping("echo/{str}")
@ -125,7 +179,7 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
} }
@FeignClient("bar-service") @FeignClient(value = TEST_SERVICE_NAME, contextId = "3")
public interface BarService { public interface BarService {
@RequestMapping(path = "bar") @RequestMapping(path = "bar")
@ -140,7 +194,7 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
} }
@FeignClient("baz-service") @FeignClient(value = TEST_SERVICE_NAME, contextId = "4")
public interface BazClient extends BazService { public interface BazClient extends BazService {
} }
@ -148,7 +202,10 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
public static class EchoServiceFallback implements EchoService { public static class EchoServiceFallback implements EchoService {
@Override @Override
public String echo(@RequestParam("str") String param) { public String echo(@RequestParam("str") String param) throws InvocationTargetException {
if (param == null) {
throw new InvocationTargetException(new Exception());
}
return "echo fallback"; return "echo fallback";
} }
@ -158,7 +215,7 @@ public class PolarisCircuitBreakerFeignIntegrationTest {
@Override @Override
public String echo(@RequestParam("str") String param) { public String echo(@RequestParam("str") String param) {
return "foo fallback"; throw new NoFallbackAvailableException("fallback", new RuntimeException());
} }
} }

@ -18,9 +18,27 @@
package com.tencent.cloud.polaris.circuitbreaker; package com.tencent.cloud.polaris.circuitbreaker;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.polaris.circuitbreaker.gateway.PolarisCircuitBreakerFilterFactory;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
import com.tencent.polaris.client.util.Utils;
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
import com.tencent.polaris.test.common.TestUtils;
import com.tencent.polaris.test.mock.discovery.NamingServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -30,8 +48,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
@ -40,10 +60,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -67,17 +84,27 @@ import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureWebTestClient(timeout = "10000") @AutoConfigureWebTestClient(timeout = "10000")
public class PolarisCircuitBreakerGatewayIntegrationTest { public class PolarisCircuitBreakerGatewayIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
@Autowired @Autowired
private WebTestClient webClient; private WebTestClient webClient;
@Autowired
private ApplicationContext applicationContext;
private static NamingServer namingServer;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test @Test
public void fallback() throws Exception { public void fallback() throws Exception {
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config();
stubFor(get(urlEqualTo("/err")) applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString();
.willReturn(aResponse()
.withStatus(500)
.withBody("err")
.withFixedDelay(3000)));
webClient webClient
.get().uri("/err") .get().uri("/err")
@ -87,22 +114,47 @@ public class PolarisCircuitBreakerGatewayIntegrationTest {
.expectBody() .expectBody()
.consumeWith( .consumeWith(
response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes())); response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
}
@Test Utils.sleepUninterrupted(2000);
public void noFallback() throws Exception {
stubFor(get(urlEqualTo("/err-no-fallback")) webClient
.willReturn(aResponse() .get().uri("/err-skip-fallback")
.withStatus(500) .header("Host", "www.circuitbreaker-skip-fallback.com")
.withBody("err") .exchange()
.withFixedDelay(3000))); .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 webClient
.get().uri("/err-no-fallback") .get().uri("/err-no-fallback")
.header("Host", "www.circuitbreaker-no-fallback.com") .header("Host", "www.circuitbreaker-no-fallback.com")
.exchange() .exchange()
.expectStatus().isEqualTo(500); .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();
} }
@ -110,9 +162,30 @@ public class PolarisCircuitBreakerGatewayIntegrationTest {
@EnableAutoConfiguration @EnableAutoConfiguration
public static class TestApplication { public static class TestApplication {
@Bean
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
try {
namingServer = NamingServer.startNamingServer(10081);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
InputStream inputStream = PolarisCircuitBreakerMockServerTest.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);
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
}
@Bean @Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) { public RouteLocator myRoutes(RouteLocatorBuilder builder) {
String httpUri = "http://httpbin.org:80";
Set<String> codeSets = new HashSet<>(); Set<String> codeSets = new HashSet<>();
codeSets.add("4**"); codeSets.add("4**");
codeSets.add("5**"); codeSets.add("5**");
@ -123,15 +196,24 @@ public class PolarisCircuitBreakerGatewayIntegrationTest {
.circuitBreaker(config -> config .circuitBreaker(config -> config
.setStatusCodes(codeSets) .setStatusCodes(codeSets)
.setFallbackUri("forward:/fallback") .setFallbackUri("forward:/fallback")
.setName(TEST_SERVICE_NAME)
)) ))
.uri(httpUri)) .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 .route(p -> p
.host("*.circuitbreaker-no-fallback.com") .host("*.circuitbreaker-no-fallback.com")
.filters(f -> f .filters(f -> f
.circuitBreaker(config -> config .circuitBreaker(config -> config
.setStatusCodes(codeSets) .setName(TEST_SERVICE_NAME)
)) ))
.uri(httpUri)) .uri("lb://" + TEST_SERVICE_NAME))
.build(); .build();
} }

@ -0,0 +1,322 @@
/*
* 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.circuitbreaker;
import java.io.BufferedReader;
import java.io.IOException;
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.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerFallback;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter;
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.factory.CircuitBreakAPIFactory;
import com.tencent.polaris.client.util.Utils;
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
import com.tencent.polaris.test.common.TestUtils;
import com.tencent.polaris.test.mock.discovery.NamingServer;
import org.junit.jupiter.api.AfterAll;
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.annotation.DirtiesContext;
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.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.HEADER_HAS_ERROR;
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat;
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 = PolarisCircuitBreakerIntegrationTest.TestConfig.class,
properties = {
"spring.cloud.gateway.enabled=false",
"feign.circuitbreaker.enabled=true",
"spring.cloud.polaris.namespace=default",
"spring.cloud.polaris.service=test"
})
@DirtiesContext
public class PolarisCircuitBreakerIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
private static NamingServer namingServer;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@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;
@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();
headers.add(HEADER_HAS_ERROR, "true");
// no delegateHandler in EnhancedRestTemplateReporter, so this will except err
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("BAD_GATEWAY");
mockServer.verify();
mockServer.reset();
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
@PolarisCircuitBreaker(fallback = "fallback")
public RestTemplate defaultRestTemplate(RpcEnhancementReporterProperties properties, ConsumerAPI consumerAPI) {
RestTemplate defaultRestTemplate = new RestTemplate();
EnhancedRestTemplateReporter enhancedRestTemplateReporter = new EnhancedRestTemplateReporter(properties, consumerAPI);
defaultRestTemplate.setErrorHandler(enhancedRestTemplateReporter);
return defaultRestTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker
public RestTemplate restTemplateFallbackFromPolaris() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@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
@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
@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();
}
@Bean
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
try {
namingServer = NamingServer.startNamingServer(10081);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
InputStream inputStream = PolarisCircuitBreakerMockServerTest.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);
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
}
@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
);
}
}
}

@ -121,7 +121,7 @@ public class PolarisCircuitBreakerMockServerTest {
} }
}, t -> "fallback"); }, t -> "fallback");
resList.add(res); resList.add(res);
Utils.sleepUninterrupted(1000); Utils.sleepUninterrupted(2000);
} }
assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback")); assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback"));

@ -0,0 +1,59 @@
{
"@type": "type.googleapis.com/v1.CircuitBreakerRule",
"id": "5f1601f01823474d9be39c0bbb26ab87",
"name": "test",
"namespace": "TestCircuitBreakerRule",
"enable": true,
"revision": "10b120c08706429f8fdc3fb44a53224b",
"ctime": "1754-08-31 06:49:24",
"mtime": "2023-02-21 17:35:31",
"etime": "",
"description": "",
"level": "METHOD",
"ruleMatcher": {
"source": {
"service": "*",
"namespace": "*"
},
"destination": {
"service": "*",
"namespace": "*",
"method": {"type": "REGEX", "value": "*"}
}
},
"errorConditions": [
{
"inputType": "RET_CODE",
"condition": {
"type": "NOT_EQUALS",
"value": "200",
"valueType": "TEXT"
}
}
],
"triggerCondition": [
{
"triggerType": "CONSECUTIVE_ERROR",
"errorCount": 1,
"errorPercent": 1,
"interval": 5,
"minimumRequest": 5
}
],
"maxEjectionPercent": 0,
"recoverCondition": {
"sleepWindow": 60,
"consecutiveSuccess": 3
},
"faultDetectConfig": {
"enable": true
},
"fallbackConfig": {
"enable": true,
"response": {
"code": 200,
"headers": [{"key": "xxx", "value": "xxx"}],
"body": "\"fallback from polaris server\""
}
}
}

@ -49,11 +49,11 @@
"enable": true "enable": true
}, },
"fallbackConfig": { "fallbackConfig": {
"enable": false, "enable": true,
"response": { "response": {
"code": 0, "code": 200,
"headers": [], "headers": [{"key": "xxx", "value": "xxx"}],
"body": "" "body": "\"fallback from polaris server\""
} }
} }
} }

@ -40,4 +40,16 @@ public class ServiceBController {
public String info() { public String info() {
return "hello world ! I'm a service B1"; return "hello world ! I'm a service B1";
} }
@GetMapping("/health")
public String health() {
System.out.println("health check: 200 instance");
return "hello world ! I'm a service B1";
}
@GetMapping("/health-svc")
public String healthsvc() {
System.out.println("health-svc check: 200 instance");
return "hello world ! I'm a service B1";
}
} }

@ -26,6 +26,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
@ -80,4 +81,18 @@ public class ServiceBController {
} }
return new ResponseEntity<>("hello world ! I'm a service B2", HttpStatus.OK); return new ResponseEntity<>("hello world ! I'm a service B2", HttpStatus.OK);
} }
@GetMapping("/health")
@ResponseStatus(value = HttpStatus.BAD_GATEWAY, reason = "failed for call my service")
public String health() {
System.out.println("health check: 502 instance");
return "hello world ! I'm a service B1";
}
@GetMapping("/health-svc")
@ResponseStatus(value = HttpStatus.BAD_GATEWAY, reason = "failed for call my service")
public String healthsvc() {
System.out.println("health-svc check: 502 instance");
return "hello world ! I'm a service B1";
}
} }

@ -18,7 +18,6 @@
package com.tencent.cloud.polaris.circuitbreaker.feign.example; package com.tencent.cloud.polaris.circuitbreaker.feign.example;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Primary;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
/** /**
@ -26,8 +25,7 @@ import org.springframework.web.bind.annotation.GetMapping;
* *
* @author sean yu * @author sean yu
*/ */
@Primary @FeignClient(name = "polaris-circuitbreaker-callee-service", contextId = "fallback-from-polaris")
@FeignClient(name = "polaris-circuitbreaker-callee-service", fallback = ProviderBFallback.class)
public interface ProviderB { public interface ProviderB {
/** /**

@ -25,7 +25,7 @@ import org.springframework.stereotype.Component;
* @author sean yu * @author sean yu
*/ */
@Component @Component
public class ProviderBFallback implements ProviderB { public class ProviderBFallback implements ProviderBWithFallback {
@Override @Override
public String info() { public String info() {

@ -0,0 +1,39 @@
/*
* 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.circuitbreaker.feign.example;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* ProviderBWithFallback.
*
* @author sean yu
*/
@FeignClient(name = "polaris-circuitbreaker-callee-service", contextId = "fallback-from-code", fallback = ProviderBFallback.class)
public interface ProviderBWithFallback {
/**
* Get info of service B.
*
* @return info of service B
*/
@GetMapping("/example/service/b/info")
String info();
}

@ -35,12 +35,24 @@ public class ServiceAController {
@Autowired @Autowired
private ProviderB polarisServiceB; private ProviderB polarisServiceB;
@Autowired
private ProviderBWithFallback providerBWithFallback;
/**
* Get info of Service B by Feign.
* @return info of Service B
*/
@GetMapping("/getBServiceInfo/fallbackFromCode")
public String getBServiceInfoFallbackFromCode() {
return providerBWithFallback.info();
}
/** /**
* Get info of Service B by Feign. * Get info of Service B by Feign.
* @return info of Service B * @return info of Service B
*/ */
@GetMapping("/getBServiceInfo") @GetMapping("/getBServiceInfo/fallbackFromPolaris")
public String getBServiceInfo() { public String getBServiceInfoFallbackFromPolaris() {
return polarisServiceB.info(); return polarisServiceB.info();
} }

@ -35,7 +35,9 @@ spring:
'filters[1]': 'filters[1]':
name: CircuitBreaker name: CircuitBreaker
args: args:
statusCodes: '''4**,502''' # statusCodes 缺省时会自动识别 "4**,5**" 为错误
# statusCodes: '''4**,502'''
# fallbackUri 缺省时会在熔断触发后拉取 plaris server 配置的降级作为 response
fallbackUri: '''forward:/polaris-fallback''' fallbackUri: '''forward:/polaris-fallback'''
# routes: # routes:
# - id: polaris-circuitbreaker-callee-service # - id: polaris-circuitbreaker-callee-service

@ -0,0 +1,43 @@
/*
* 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.circuitbreaker.resttemplate.example;
import java.util.HashMap;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerFallback;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
import org.springframework.stereotype.Component;
/**
* CustomFallback.
*
* @author sean yu
*/
@Component
public class CustomFallback implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200,
new HashMap<String, String>() {{
put("Content-Type", "application/json");
}},
"{\"msg\": \"this is a fallback class\"}");
}
}

@ -19,7 +19,9 @@ package com.tencent.cloud.polaris.circuitbreaker.resttemplate.example;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -35,7 +37,16 @@ import org.springframework.web.client.RestTemplate;
public class ServiceAController { public class ServiceAController {
@Autowired @Autowired
private RestTemplate restTemplate; @Qualifier("defaultRestTemplate")
private RestTemplate defaultRestTemplate;
@Autowired
@Qualifier("restTemplateFallbackFromPolaris")
private RestTemplate restTemplateFallbackFromPolaris;
@Autowired
@Qualifier("restTemplateFallbackFromCode")
private RestTemplate restTemplateFallbackFromCode;
@Autowired @Autowired
private CircuitBreakerFactory circuitBreakerFactory; private CircuitBreakerFactory circuitBreakerFactory;
@ -45,9 +56,19 @@ public class ServiceAController {
return circuitBreakerFactory return circuitBreakerFactory
.create("polaris-circuitbreaker-callee-service#/example/service/b/info") .create("polaris-circuitbreaker-callee-service#/example/service/b/info")
.run(() -> .run(() ->
restTemplate.getForObject("/example/service/b/info", String.class), defaultRestTemplate.getForObject("/example/service/b/info", String.class),
throwable -> "trigger the refuse for service b" throwable -> "trigger the refuse for service b"
); );
} }
@GetMapping("/getBServiceInfo/fallbackFromPolaris")
public ResponseEntity<String> getBServiceInfoFallback() {
return restTemplateFallbackFromPolaris.getForEntity("/example/service/b/info", String.class);
}
@GetMapping("/getBServiceInfo/fallbackFromCode")
public ResponseEntity<String> getBServiceInfoFallbackClass() {
return restTemplateFallbackFromCode.getForEntity("/example/service/b/info", String.class);
}
} }

@ -18,6 +18,8 @@
package com.tencent.cloud.polaris.circuitbreaker.resttemplate.example; package com.tencent.cloud.polaris.circuitbreaker.resttemplate.example;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalanced;
@ -39,7 +41,27 @@ public class ServiceAResTemplate {
@Bean @Bean
@LoadBalanced @LoadBalanced
public RestTemplate restTemplate() { public RestTemplate defaultRestTemplate() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://polaris-circuitbreaker-callee-service");
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker
public RestTemplate restTemplateFallbackFromPolaris() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://polaris-circuitbreaker-callee-service");
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomFallback.class)
public RestTemplate restTemplateFallbackFromCode() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://polaris-circuitbreaker-callee-service"); DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://polaris-circuitbreaker-callee-service");
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory); restTemplate.setUriTemplateHandler(uriBuilderFactory);

@ -56,7 +56,14 @@ import static com.tencent.cloud.common.constant.ContextConstant.UTF_8;
*/ */
public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter implements ResponseErrorHandler, ApplicationContextAware { public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter implements ResponseErrorHandler, ApplicationContextAware {
static final String HEADER_HAS_ERROR = "X-SCT-Has-Error"; /**
* Polaris-CircuitBreaker-Fallback header flag.
*/
public static final String POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER = "X-SCT-Polaris-CircuitBreaker-Fallback";
/**
* response has error header flag, since EnhancedRestTemplateReporter#hasError always return true.
*/
public static final String HEADER_HAS_ERROR = "X-SCT-Has-Error";
private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedRestTemplateReporter.class); private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedRestTemplateReporter.class);
private final ConsumerAPI consumerAPI; private final ConsumerAPI consumerAPI;
private ResponseErrorHandler delegateHandler; private ResponseErrorHandler delegateHandler;
@ -117,6 +124,9 @@ public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter
} }
private void reportResult(URI url, ClientHttpResponse response) { private void reportResult(URI url, ClientHttpResponse response) {
if (Boolean.parseBoolean(response.getHeaders().getFirst(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER))) {
return;
}
try { try {
ServiceCallResult resultRequest = createServiceCallResult(url, response); ServiceCallResult resultRequest = createServiceCallResult(url, response);
Map<String, String> loadBalancerContext = MetadataContextHolder.get().getLoadbalancerMetadata(); Map<String, String> loadBalancerContext = MetadataContextHolder.get().getLoadbalancerMetadata();
@ -190,10 +200,8 @@ public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter
} }
private void clear(ClientHttpResponse response) { private void clear(ClientHttpResponse response) {
if (!response.getHeaders().containsKey(HEADER_HAS_ERROR)) {
return;
}
response.getHeaders().remove(HEADER_HAS_ERROR); response.getHeaders().remove(HEADER_HAS_ERROR);
response.getHeaders().remove(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER);
} }
private ServiceCallResult createServiceCallResult(URI uri, ClientHttpResponse response) throws IOException { private ServiceCallResult createServiceCallResult(URI uri, ClientHttpResponse response) throws IOException {

Loading…
Cancel
Save