From 097dad88ee05bb439e20871ead268e2708386496 Mon Sep 17 00:00:00 2001 From: "Shanyou Yu (Sean Yu)" Date: Fri, 24 Mar 2023 19:24:49 +0800 Subject: [PATCH] feature: improve circuit breaker usage (#913) --- CHANGELOG.md | 3 +- pom.xml | 2 +- ...olarisCircuitBreakerAutoConfiguration.java | 10 + ...sCircuitBreakerBootstrapConfiguration.java | 7 +- ...itBreakerFeignClientAutoConfiguration.java | 21 ++ ...olarisCircuitBreakerAutoConfiguration.java | 2 +- .../PolarisCircuitBreakerFallbackFactory.java | 97 ++++++ .../PolarisCircuitBreakerNameResolver.java | 5 +- .../feign/PolarisFeignCircuitBreaker.java | 94 +++++ ...sFeignCircuitBreakerInvocationHandler.java | 200 +++++++++++ .../PolarisFeignCircuitBreakerTargeter.java | 103 ++++++ .../PolarisCircuitBreakerFilterFactory.java | 117 +++++-- .../resttemplate/PolarisCircuitBreaker.java | 54 +++ .../PolarisCircuitBreakerFallback.java | 29 ++ .../PolarisCircuitBreakerHttpResponse.java | 105 ++++++ ...tBreakerRestTemplateBeanPostProcessor.java | 125 +++++++ ...CircuitBreakerRestTemplateInterceptor.java | 111 ++++++ ...risCircuitBreakerFeignIntegrationTest.java | 87 ++++- ...sCircuitBreakerGatewayIntegrationTest.java | 130 +++++-- .../PolarisCircuitBreakerIntegrationTest.java | 322 ++++++++++++++++++ .../PolarisCircuitBreakerMockServerTest.java | 2 +- .../resources/circuitBreakerRule-method.json | 59 ++++ .../test/resources/circuitBreakerRule.json | 8 +- .../example/ServiceBController.java | 12 + .../example/ServiceBController.java | 15 + .../feign/example/ProviderB.java | 4 +- .../feign/example/ProviderBFallback.java | 2 +- .../feign/example/ProviderBWithFallback.java | 39 +++ .../feign/example/ServiceAController.java | 16 +- .../src/main/resources/bootstrap.yml | 4 +- .../resttemplate/example/CustomFallback.java | 43 +++ .../example/ServiceAController.java | 25 +- .../example/ServiceAResTemplate.java | 24 +- .../EnhancedRestTemplateReporter.java | 16 +- 34 files changed, 1798 insertions(+), 95 deletions(-) create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerFallbackFactory.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreaker.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerInvocationHandler.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerTargeter.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreaker.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerFallback.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerHttpResponse.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerIntegrationTest.java create mode 100644 spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule-method.json create mode 100644 spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBWithFallback.java create mode 100644 spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/CustomFallback.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8279670..3cae1b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,5 @@ - [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: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) \ No newline at end of file +- [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) diff --git a/pom.xml b/pom.xml index c8cd4eb03..5454486bf 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ 1.11.0-2021.0.6-SNAPSHOT - 5.3.26 + 5.3.25 2.6.14 diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java index f838214f2..b2792737a 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java @@ -22,6 +22,7 @@ import java.util.List; import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory; 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.RpcEnhancementReporterProperties; 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.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.Customizer; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -72,4 +75,11 @@ public class PolarisCircuitBreakerAutoConfiguration { return new CircuitBreakerConfigModifier(properties); } + @Bean + @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") + public static PolarisCircuitBreakerRestTemplateBeanPostProcessor polarisCircuitBreakerRestTemplateBeanPostProcessor( + ApplicationContext applicationContext) { + return new PolarisCircuitBreakerRestTemplateBeanPostProcessor(applicationContext); + } + } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java index c1cafea0b..6d6274253 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java @@ -28,7 +28,12 @@ import org.springframework.context.annotation.Import; */ @Configuration(proxyBeanMethods = false) @ConditionalOnProperty("spring.cloud.polaris.enabled") -@Import({PolarisCircuitBreakerAutoConfiguration.class, ReactivePolarisCircuitBreakerAutoConfiguration.class, PolarisCircuitBreakerFeignClientAutoConfiguration.class}) +@Import({ + PolarisCircuitBreakerAutoConfiguration.class, + ReactivePolarisCircuitBreakerAutoConfiguration.class, + PolarisCircuitBreakerFeignClientAutoConfiguration.class, + GatewayPolarisCircuitBreakerAutoConfiguration.class +}) public class PolarisCircuitBreakerBootstrapConfiguration { } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java index 91096e1e6..57e25bcba 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java @@ -18,14 +18,20 @@ package com.tencent.cloud.polaris.circuitbreaker.config; 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 org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.FeignClientFactoryBean; +import org.springframework.cloud.openfeign.Targeter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; /** * PolarisCircuitBreakerFeignClientAutoConfiguration. @@ -43,4 +49,19 @@ public class PolarisCircuitBreakerFeignClientAutoConfiguration { 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(); + } + } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java index 58ca348a5..9d9bf911e 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java @@ -60,7 +60,7 @@ public class ReactivePolarisCircuitBreakerAutoConfiguration { @Bean @ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class) - public ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI) { + public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI) { ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI); customizers.forEach(customizer -> customizer.customize(factory)); return factory; diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerFallbackFactory.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerFallbackFactory.java new file mode 100644 index 000000000..0bb6b9644 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerFallbackFactory.java @@ -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> 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); + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerNameResolver.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerNameResolver.java index 2b1689c45..929a71d22 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerNameResolver.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisCircuitBreakerNameResolver.java @@ -36,6 +36,7 @@ public class PolarisCircuitBreakerNameResolver implements CircuitBreakerNameReso @Override public String resolveCircuitBreakerName(String feignClientName, Target target, Method method) { + String serviceName = target.name(); RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class); String path = ""; if (requestMapping != null) { @@ -44,8 +45,8 @@ public class PolarisCircuitBreakerNameResolver implements CircuitBreakerNameReso requestMapping.path()[0]; } return "".equals(path) ? - MetadataContext.LOCAL_NAMESPACE + "#" + feignClientName : - MetadataContext.LOCAL_NAMESPACE + "#" + feignClientName + "#" + path; + MetadataContext.LOCAL_NAMESPACE + "#" + serviceName : + MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path; } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreaker.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreaker.java new file mode 100644 index 000000000..85eae2666 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreaker.java @@ -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 target(Target target, T fallback) { + return build(fallback != null ? new FallbackFactory.Default(fallback) : null).newInstance(target); + } + + public T target(Target target, FallbackFactory fallbackFactory) { + return build(fallbackFactory).newInstance(target); + } + + @Override + public T target(Target 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(); + } + + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerInvocationHandler.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerInvocationHandler.java new file mode 100644 index 000000000..ab16bf625 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerInvocationHandler.java @@ -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 dispatch; + + private final FallbackFactory nullableFallbackFactory; + + private final Map fallbackMethodMap; + + private final CircuitBreakerNameResolver circuitBreakerNameResolver; + + private final Decoder decoder; + + public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target target, + Map 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 supplier = asSupplier(method, args); + Function 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 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 toFallbackMethod(Map dispatch) { + Map 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(); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerTargeter.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerTargeter.java new file mode 100644 index 000000000..c6a865826 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/PolarisFeignCircuitBreakerTargeter.java @@ -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 target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + Target.HardCodedTarget 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 targetWithFallbackFactory(String feignClientName, FeignContext context, + Target.HardCodedTarget target, PolarisFeignCircuitBreaker.Builder builder, Class fallbackFactoryClass) { + FallbackFactory fallbackFactory = (FallbackFactory) getFromContext("fallbackFactory", + feignClientName, context, fallbackFactoryClass, FallbackFactory.class); + return builder(feignClientName, builder).target(target, fallbackFactory); + } + + private T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget target, + PolarisFeignCircuitBreaker.Builder builder, Class fallback) { + T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); + return builder(feignClientName, builder).target(target, fallbackInstance); + } + + private T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, + Class beanType, Class 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); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/gateway/PolarisCircuitBreakerFilterFactory.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/gateway/PolarisCircuitBreakerFilterFactory.java index 90a9749fe..ea9bd3c45 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/gateway/PolarisCircuitBreakerFilterFactory.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/gateway/PolarisCircuitBreakerFilterFactory.java @@ -19,16 +19,18 @@ package com.tencent.cloud.polaris.circuitbreaker.gateway; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; 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.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; +import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.HttpStatusHolder; import org.springframework.cloud.gateway.support.ServiceUnavailableException; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.ResponseStatusException; @@ -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.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset; @@ -65,8 +72,6 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.r */ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerFilterFactory.class); - private String routeIdPrefix; private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; @@ -150,6 +155,12 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake return Arrays.asList(allHttpStatus); } + private Set getDefaultStatus() { + return Arrays.stream(HttpStatus.values()) + .filter(HttpStatus::is5xxServerError) + .collect(Collectors.toSet()); + } + @Override public GatewayFilter apply(Config config) { Set statuses = config.getStatusCodes().stream() @@ -165,40 +176,82 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake }) .filter(Objects::nonNull) .collect(Collectors.toSet()); + if (CollectionUtils.isEmpty(statuses)) { + statuses.addAll(getDefaultStatus()); + } String circuitBreakerId = getCircuitBreakerId(config); return new GatewayFilter() { @Override public Mono 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(); - ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(circuitBreakerId + "#" + path); - return cb.run(chain.filter(exchange).doOnSuccess(v -> { - if (statuses.contains(exchange.getResponse().getStatusCode())) { - HttpStatus status = exchange.getResponse().getStatusCode(); - throw new CircuitBreakerStatusCodeException(status); - } - }), t -> { - if (config.getFallbackUri() == null) { - return Mono.error(t); - } - - exchange.getResponse().setStatusCode(null); - reset(exchange); + ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(serviceName + "#" + path); + return cb.run( + chain.filter(exchange) + .doOnSuccess(v -> { + // throw CircuitBreakerStatusCodeException by default for all need checking status + // so polaris can report right error status + Set statusNeedToCheck = new HashSet<>(); + statusNeedToCheck.addAll(statuses); + statusNeedToCheck.addAll(getDefaultStatus()); + HttpStatus status = exchange.getResponse().getStatusCode(); + if (statusNeedToCheck.contains(status)) { + throw new CircuitBreakerStatusCodeException(status); + } + }), + t -> { + // pre-check CircuitBreakerStatusCodeException's status matches input status + if (t instanceof CircuitBreakerStatusCodeException) { + HttpStatus status = ((CircuitBreakerStatusCodeException) t).getStatusCode(); + // no need to fallback + if (!statuses.contains(status)) { + return Mono.error(t); + } + } + // do fallback + if (config.getFallbackUri() == null) { + // polaris checking + if (t instanceof CallAbortedException) { + CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo(); + if (fallbackInfo != null) { + ServerHttpResponse response = exchange.getResponse(); + response.setRawStatusCode(fallbackInfo.getCode()); + if (fallbackInfo.getHeaders() != null) { + fallbackInfo.getHeaders().forEach((k, v) -> response.getHeaders().add(k, v)); + } + DataBuffer bodyBuffer = null; + if (fallbackInfo.getBody() != null) { + byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8); + bodyBuffer = response.bufferFactory().wrap(bytes); + } + return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete(); + } + } + return Mono.error(t); + } + exchange.getResponse().setStatusCode(null); + reset(exchange); - // TODO: copied from RouteToRequestUrlFilter - URI uri = exchange.getRequest().getURI(); - // TODO: assume always? - boolean encoded = containsEncodedParts(uri); - URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null) - .uri(config.getFallbackUri()).scheme(null).build(encoded).toUri(); - exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); - addExceptionDetails(t, exchange); + // TODO: copied from RouteToRequestUrlFilter + URI uri = exchange.getRequest().getURI(); + // TODO: assume always? + boolean encoded = containsEncodedParts(uri); + URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null) + .uri(config.getFallbackUri()).scheme(null).build(encoded).toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + addExceptionDetails(t, exchange); - // Reset the exchange - reset(exchange); + // Reset the exchange + reset(exchange); - ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build(); - return getDispatcherHandler().handle(exchange.mutate().request(request).build()); - }).onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError())); + ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build(); + return getDispatcherHandler().handle(exchange.mutate().request(request).build()); + }) + .onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError())); } @Override @@ -216,9 +269,11 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t)); } if (t instanceof CallAbortedException) { - LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", t.getMessage()); return Mono.error(new ServiceUnavailableException()); } + if (t instanceof CircuitBreakerStatusCodeException) { + return Mono.empty(); + } if (resumeWithoutError) { return Mono.empty(); } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreaker.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreaker.java new file mode 100644 index 000000000..d3a8af492 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreaker.java @@ -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 fallbackClass() default PolarisCircuitBreakerFallback.class; + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerFallback.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerFallback.java new file mode 100644 index 000000000..9c59795c9 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerFallback.java @@ -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(); + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerHttpResponse.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerHttpResponse.java new file mode 100644 index 000000000..ce6f640a5 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerHttpResponse.java @@ -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 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; + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java new file mode 100644 index 000000000..775a6cfa6 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java @@ -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 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); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java new file mode 100644 index 000000000..e8de9294c --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java @@ -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); + } + ); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerFeignIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerFeignIntegrationTest.java index f2963b9b3..8712d4f77 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerFeignIntegrationTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerFeignIntegrationTest.java @@ -18,7 +18,25 @@ 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.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; @@ -26,6 +44,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.cloud.openfeign.FeignClient; @@ -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.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.assertThatThrownBy; 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 public class PolarisCircuitBreakerFeignIntegrationTest { + private static final String TEST_SERVICE_NAME = "test-service-callee"; + @Autowired private EchoService echoService; @@ -67,6 +89,15 @@ public class PolarisCircuitBreakerFeignIntegrationTest { @Autowired private BazService bazService; + private static NamingServer namingServer; + + @AfterAll + public static void afterAll() { + if (null != namingServer) { + namingServer.terminate(); + } + } + @Test public void contextLoads() throws Exception { assertThat(echoService).isNotNull(); @@ -74,18 +105,19 @@ public class PolarisCircuitBreakerFeignIntegrationTest { } @Test - public void testFeignClient() { + public void testFeignClient() throws InvocationTargetException { assertThat(echoService.echo("test")).isEqualTo("echo fallback"); - assertThat(fooService.echo("test")).isEqualTo("foo fallback"); - + Utils.sleepUninterrupted(2000); assertThatThrownBy(() -> { - barService.bar(); + echoService.echo(null); }).isInstanceOf(Exception.class); - assertThatThrownBy(() -> { - bazService.baz(); - }).isInstanceOf(Exception.class); - + fooService.echo("test"); + }).isInstanceOf(NoFallbackAvailableException.class); + Utils.sleepUninterrupted(2000); + assertThat(barService.bar()).isEqualTo("\"fallback from polaris server\""); + Utils.sleepUninterrupted(2000); + assertThat(bazService.baz()).isEqualTo("\"fallback from polaris server\""); assertThat(fooService.toString()).isNotEqualTo(echoService.toString()); assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode()); assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE); @@ -107,17 +139,39 @@ public class PolarisCircuitBreakerFeignIntegrationTest { 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 { @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 { @RequestMapping("echo/{str}") @@ -125,7 +179,7 @@ public class PolarisCircuitBreakerFeignIntegrationTest { } - @FeignClient("bar-service") + @FeignClient(value = TEST_SERVICE_NAME, contextId = "3") public interface BarService { @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 { } @@ -148,7 +202,10 @@ public class PolarisCircuitBreakerFeignIntegrationTest { public static class EchoServiceFallback implements EchoService { @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"; } @@ -158,7 +215,7 @@ public class PolarisCircuitBreakerFeignIntegrationTest { @Override public String echo(@RequestParam("str") String param) { - return "foo fallback"; + throw new NoFallbackAvailableException("fallback", new RuntimeException()); } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerGatewayIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerGatewayIntegrationTest.java index cf0823417..22513c613 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerGatewayIntegrationTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerGatewayIntegrationTest.java @@ -18,9 +18,27 @@ 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.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.extension.ExtendWith; 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.context.SpringBootTest; 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.builder.RouteLocatorBuilder; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ActiveProfiles; @@ -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.RestController; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -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 com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV; import static org.assertj.core.api.Assertions.assertThat; @@ -67,17 +84,27 @@ import static org.assertj.core.api.Assertions.assertThat; @AutoConfigureWebTestClient(timeout = "10000") public class PolarisCircuitBreakerGatewayIntegrationTest { + private static final String TEST_SERVICE_NAME = "test-service-callee"; + @Autowired private WebTestClient webClient; + @Autowired + private ApplicationContext applicationContext; + + private static NamingServer namingServer; + + @AfterAll + public static void afterAll() { + if (null != namingServer) { + namingServer.terminate(); + } + } + @Test public void fallback() throws Exception { - - stubFor(get(urlEqualTo("/err")) - .willReturn(aResponse() - .withStatus(500) - .withBody("err") - .withFixedDelay(3000))); + SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config(); + applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString(); webClient .get().uri("/err") @@ -87,22 +114,47 @@ public class PolarisCircuitBreakerGatewayIntegrationTest { .expectBody() .consumeWith( response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes())); - } - @Test - public void noFallback() throws Exception { + Utils.sleepUninterrupted(2000); - stubFor(get(urlEqualTo("/err-no-fallback")) - .willReturn(aResponse() - .withStatus(500) - .withBody("err") - .withFixedDelay(3000))); + webClient + .get().uri("/err-skip-fallback") + .header("Host", "www.circuitbreaker-skip-fallback.com") + .exchange() + .expectStatus(); + + Utils.sleepUninterrupted(2000); + + // this should be 200, but for some unknown reason, GitHub action run failed in windows, so we skip this check + webClient + .get().uri("/err-skip-fallback") + .header("Host", "www.circuitbreaker-skip-fallback.com") + .exchange() + .expectStatus(); + + Utils.sleepUninterrupted(2000); webClient .get().uri("/err-no-fallback") .header("Host", "www.circuitbreaker-no-fallback.com") .exchange() - .expectStatus().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 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 public RouteLocator myRoutes(RouteLocatorBuilder builder) { - String httpUri = "http://httpbin.org:80"; Set codeSets = new HashSet<>(); codeSets.add("4**"); codeSets.add("5**"); @@ -123,15 +196,24 @@ public class PolarisCircuitBreakerGatewayIntegrationTest { .circuitBreaker(config -> config .setStatusCodes(codeSets) .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 .host("*.circuitbreaker-no-fallback.com") .filters(f -> f .circuitBreaker(config -> config - .setStatusCodes(codeSets) + .setName(TEST_SERVICE_NAME) )) - .uri(httpUri)) + .uri("lb://" + TEST_SERVICE_NAME)) .build(); } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerIntegrationTest.java new file mode 100644 index 000000000..a78b59aff --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerIntegrationTest.java @@ -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() {{ + 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 + ); + } + } + + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java index e8315c2e7..36976da74 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java @@ -121,7 +121,7 @@ public class PolarisCircuitBreakerMockServerTest { } }, t -> "fallback"); resList.add(res); - Utils.sleepUninterrupted(1000); + Utils.sleepUninterrupted(2000); } assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback")); diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule-method.json b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule-method.json new file mode 100644 index 000000000..749e43479 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule-method.json @@ -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\"" + } + } +} \ No newline at end of file diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule.json b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule.json index 0a1e97f2a..7adef7fba 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule.json +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/resources/circuitBreakerRule.json @@ -49,11 +49,11 @@ "enable": true }, "fallbackConfig": { - "enable": false, + "enable": true, "response": { - "code": 0, - "headers": [], - "body": "" + "code": 200, + "headers": [{"key": "xxx", "value": "xxx"}], + "body": "\"fallback from polaris server\"" } } } \ No newline at end of file diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceBController.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceBController.java index 128ea5e4d..8628db06a 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceBController.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceBController.java @@ -40,4 +40,16 @@ public class ServiceBController { public String info() { 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"; + } } diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2/src/main/java/com/tencent/cloud/polaris/ciruitbreaker/example/ServiceBController.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2/src/main/java/com/tencent/cloud/polaris/ciruitbreaker/example/ServiceBController.java index 21325f7e7..2d5287c3a 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2/src/main/java/com/tencent/cloud/polaris/ciruitbreaker/example/ServiceBController.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2/src/main/java/com/tencent/cloud/polaris/ciruitbreaker/example/ServiceBController.java @@ -26,6 +26,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; 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); } + + @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"; + } } diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderB.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderB.java index 5e1160877..63dbbdb17 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderB.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderB.java @@ -18,7 +18,6 @@ package com.tencent.cloud.polaris.circuitbreaker.feign.example; import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.context.annotation.Primary; import org.springframework.web.bind.annotation.GetMapping; /** @@ -26,8 +25,7 @@ import org.springframework.web.bind.annotation.GetMapping; * * @author sean yu */ -@Primary -@FeignClient(name = "polaris-circuitbreaker-callee-service", fallback = ProviderBFallback.class) +@FeignClient(name = "polaris-circuitbreaker-callee-service", contextId = "fallback-from-polaris") public interface ProviderB { /** diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBFallback.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBFallback.java index e62cbf24c..419b18cfc 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBFallback.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBFallback.java @@ -25,7 +25,7 @@ import org.springframework.stereotype.Component; * @author sean yu */ @Component -public class ProviderBFallback implements ProviderB { +public class ProviderBFallback implements ProviderBWithFallback { @Override public String info() { diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBWithFallback.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBWithFallback.java new file mode 100644 index 000000000..194ac1305 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ProviderBWithFallback.java @@ -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(); + +} diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ServiceAController.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ServiceAController.java index 5c7108716..30aa94c49 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ServiceAController.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/feign/example/ServiceAController.java @@ -35,12 +35,24 @@ public class ServiceAController { @Autowired 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. * @return info of Service B */ - @GetMapping("/getBServiceInfo") - public String getBServiceInfo() { + @GetMapping("/getBServiceInfo/fallbackFromPolaris") + public String getBServiceInfoFallbackFromPolaris() { return polarisServiceB.info(); } diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/src/main/resources/bootstrap.yml b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/src/main/resources/bootstrap.yml index 399af3ca1..a01c12e59 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/src/main/resources/bootstrap.yml +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/src/main/resources/bootstrap.yml @@ -35,7 +35,9 @@ spring: 'filters[1]': name: CircuitBreaker args: - statusCodes: '''4**,502''' + # statusCodes 缺省时会自动识别 "4**,5**" 为错误 +# statusCodes: '''4**,502''' + # fallbackUri 缺省时会在熔断触发后拉取 plaris server 配置的降级作为 response fallbackUri: '''forward:/polaris-fallback''' # routes: # - id: polaris-circuitbreaker-callee-service diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/CustomFallback.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/CustomFallback.java new file mode 100644 index 000000000..a7f5f1a65 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/CustomFallback.java @@ -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() {{ + put("Content-Type", "application/json"); + }}, + "{\"msg\": \"this is a fallback class\"}"); + } +} diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAController.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAController.java index d76816ed1..d17744ce3 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAController.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAController.java @@ -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.Qualifier; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -35,7 +37,16 @@ import org.springframework.web.client.RestTemplate; public class ServiceAController { @Autowired - private RestTemplate restTemplate; + @Qualifier("defaultRestTemplate") + private RestTemplate defaultRestTemplate; + + @Autowired + @Qualifier("restTemplateFallbackFromPolaris") + private RestTemplate restTemplateFallbackFromPolaris; + + @Autowired + @Qualifier("restTemplateFallbackFromCode") + private RestTemplate restTemplateFallbackFromCode; @Autowired private CircuitBreakerFactory circuitBreakerFactory; @@ -45,9 +56,19 @@ public class ServiceAController { return circuitBreakerFactory .create("polaris-circuitbreaker-callee-service#/example/service/b/info") .run(() -> - restTemplate.getForObject("/example/service/b/info", String.class), + defaultRestTemplate.getForObject("/example/service/b/info", String.class), throwable -> "trigger the refuse for service b" ); } + @GetMapping("/getBServiceInfo/fallbackFromPolaris") + public ResponseEntity getBServiceInfoFallback() { + return restTemplateFallbackFromPolaris.getForEntity("/example/service/b/info", String.class); + } + + @GetMapping("/getBServiceInfo/fallbackFromCode") + public ResponseEntity getBServiceInfoFallbackClass() { + return restTemplateFallbackFromCode.getForEntity("/example/service/b/info", String.class); + } + } diff --git a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAResTemplate.java b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAResTemplate.java index 21e2f4778..fb6495912 100644 --- a/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAResTemplate.java +++ b/spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example/src/main/java/com/tencent/cloud/polaris/circuitbreaker/resttemplate/example/ServiceAResTemplate.java @@ -18,6 +18,8 @@ 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.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; @@ -39,7 +41,27 @@ public class ServiceAResTemplate { @Bean @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"); RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(uriBuilderFactory); diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/resttemplate/EnhancedRestTemplateReporter.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/resttemplate/EnhancedRestTemplateReporter.java index 4a2f9c3e5..2c3ea1fe2 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/resttemplate/EnhancedRestTemplateReporter.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/resttemplate/EnhancedRestTemplateReporter.java @@ -56,7 +56,14 @@ import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; */ 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 final ConsumerAPI consumerAPI; private ResponseErrorHandler delegateHandler; @@ -117,6 +124,9 @@ public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter } private void reportResult(URI url, ClientHttpResponse response) { + if (Boolean.parseBoolean(response.getHeaders().getFirst(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER))) { + return; + } try { ServiceCallResult resultRequest = createServiceCallResult(url, response); Map loadBalancerContext = MetadataContextHolder.get().getLoadbalancerMetadata(); @@ -190,10 +200,8 @@ public class EnhancedRestTemplateReporter extends AbstractPolarisReporterAdapter } private void clear(ClientHttpResponse response) { - if (!response.getHeaders().containsKey(HEADER_HAS_ERROR)) { - return; - } response.getHeaders().remove(HEADER_HAS_ERROR); + response.getHeaders().remove(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER); } private ServiceCallResult createServiceCallResult(URI uri, ClientHttpResponse response) throws IOException {