feature:add polaris circuit breaker support

pull/1080/head
Shanyou Yu (Sean Yu) 2 years ago committed by Haotian Zhang
parent 3859cd7eef
commit 1dffb915af

@ -1,3 +1,14 @@
# Change Log
---
- [feature:add PolarisRateLimiterLimitedFallback spi.](https://github.com/Tencent/spring-cloud-tencent/commit/52689515f5dd7f1ddf22dbe9b62fcd9d19fc8dbe)
- [fix:fix the error capture of rate limit exception.](https://github.com/Tencent/spring-cloud-tencent/commit/edeeaded922ee3837eb5a58582a62f1f4c77608e)
- [feat:enable stat reporting as default.](https://github.com/Tencent/spring-cloud-tencent/commit/f97a1b2450bfba047d22e96a85e2ab39e849f4a0)
- [refactor:update to junit 5.](https://github.com/Tencent/spring-cloud-tencent/commit/21953e17b6549e0ce52fc4df0a94f96a10dbe0c5)
- [docs:support auto snapshot release in GitHub Action.](https://github.com/Tencent/spring-cloud-tencent/commit/da2422ae71f83d229e8c322ce2ccd5850ffa8653)
- [feature:add User-Agent:polaris for healthyCheck api.](https://github.com/Tencent/spring-cloud-tencent/commit/b2cecba273bfd0eaf5160791c1efe3acb01d0bfe)
- [optimize ServiceRuleManager](https://github.com/Tencent/spring-cloud-tencent/commit/07f04174ca487cf1db1d9c19bbe44853e2bcf117)
- [refactor:refactor stat module.](https://github.com/Tencent/spring-cloud-tencent/commit/9408bfbb948446738c050b3b18d8ed3124da36c1)
- [fix:fix NPE.](https://github.com/Tencent/spring-cloud-tencent/commit/17a9a8d75aef6ed39a0a1c7aa4b7d547f5d3d40e)
- [refactor:optimize sct-all.](https://github.com/Tencent/spring-cloud-tencent/commit/3859cd7eefcd93752d05113597656e584d97c38d)
- [feature:add polaris circuit breaker support]()

@ -89,7 +89,7 @@
<properties>
<!-- Project revision -->
<revision>1.11.1-Hoxton.SR12-SNAPSHOT</revision>
<revision>1.11.2-Hoxton.SR12-SNAPSHOT</revision>
<!-- Spring Framework -->
<spring.framework.version>5.2.22.RELEASE</spring.framework.version>

@ -29,6 +29,7 @@ import com.tencent.cloud.metadata.core.DecodeTransferMetadataServletFilter;
import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataScgFilter;
import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientFilter;
import com.tencent.cloud.metadata.core.EncodeTransferMetadataZuulFilter;
import org.springframework.beans.factory.SmartInitializingSingleton;
@ -41,6 +42,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import static javax.servlet.DispatcherType.ASYNC;
import static javax.servlet.DispatcherType.ERROR;
@ -66,10 +68,12 @@ public class MetadataTransferAutoConfiguration {
@Bean
public FilterRegistrationBean<DecodeTransferMetadataServletFilter> metadataServletFilterRegistrationBean(
DecodeTransferMetadataServletFilter decodeTransferMetadataServletFilter) {
FilterRegistrationBean<DecodeTransferMetadataServletFilter> filterRegistrationBean =
new FilterRegistrationBean<>(decodeTransferMetadataServletFilter);
filterRegistrationBean.setDispatcherTypes(ASYNC, ERROR, FORWARD, INCLUDE, REQUEST);
filterRegistrationBean.setOrder(MetadataConstant.OrderConstant.WEB_FILTER_ORDER);
FilterRegistrationBean<DecodeTransferMetadataServletFilter> filterRegistrationBean = new FilterRegistrationBean<>(
decodeTransferMetadataServletFilter);
filterRegistrationBean.setDispatcherTypes(ASYNC, ERROR, FORWARD, INCLUDE,
REQUEST);
filterRegistrationBean
.setOrder(MetadataConstant.OrderConstant.WEB_FILTER_ORDER);
return filterRegistrationBean;
}
@ -77,7 +81,6 @@ public class MetadataTransferAutoConfiguration {
public DecodeTransferMetadataServletFilter metadataServletFilter() {
return new DecodeTransferMetadataServletFilter();
}
}
/**
@ -133,7 +136,6 @@ public class MetadataTransferAutoConfiguration {
public EncodeTransferMedataFeignInterceptor encodeTransferMedataFeignInterceptor() {
return new EncodeTransferMedataFeignInterceptor();
}
}
/**
@ -160,4 +162,26 @@ public class MetadataTransferAutoConfiguration {
});
}
}
/**
* Create when WebClient.Builder exists.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.springframework.web.reactive.function.client.WebClient")
protected static class MetadataTransferWebClientConfig {
@Autowired(required = false)
private List<WebClient.Builder> webClientBuilder = Collections.emptyList();
@Bean
public EncodeTransferMedataWebClientFilter encodeTransferMedataWebClientFilter() {
return new EncodeTransferMedataWebClientFilter();
}
@Bean
public SmartInitializingSingleton addEncodeTransferMetadataFilterForWebClient(EncodeTransferMedataWebClientFilter filter) {
return () -> webClientBuilder.forEach(webClient -> {
webClient.filter(filter);
});
}
}
}

@ -0,0 +1,90 @@
/*
* 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.metadata.core;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.util.JacksonUtils;
import reactor.core.publisher.Mono;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import static com.tencent.cloud.common.constant.ContextConstant.UTF_8;
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_DISPOSABLE_METADATA;
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_METADATA;
/**
* web client filter used for writing metadata in HTTP request header.
*
* @author sean yu
*/
public class EncodeTransferMedataWebClientFilter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction next) {
MetadataContext metadataContext = MetadataContextHolder.get();
Map<String, String> customMetadata = metadataContext.getCustomMetadata();
Map<String, String> disposableMetadata = metadataContext.getDisposableMetadata();
Map<String, String> transHeaders = metadataContext.getTransHeadersKV();
ClientRequest.Builder requestBuilder = ClientRequest.from(clientRequest);
this.buildMetadataHeader(requestBuilder, customMetadata, CUSTOM_METADATA);
this.buildMetadataHeader(requestBuilder, disposableMetadata, CUSTOM_DISPOSABLE_METADATA);
this.buildTransmittedHeader(requestBuilder, transHeaders);
ClientRequest request = requestBuilder.build();
return next.exchange(request);
}
private void buildTransmittedHeader(ClientRequest.Builder requestBuilder, Map<String, String> transHeaders) {
if (!CollectionUtils.isEmpty(transHeaders)) {
transHeaders.forEach(requestBuilder::header);
}
}
/**
* Set metadata into the request header for {@link ClientRequest} .
* @param requestBuilder instance of {@link ClientRequest.Builder}
* @param metadata metadata map .
* @param headerName target metadata http header name .
*/
private void buildMetadataHeader(ClientRequest.Builder requestBuilder, Map<String, String> metadata, String headerName) {
if (!CollectionUtils.isEmpty(metadata)) {
String encodedMetadata = JacksonUtils.serialize2Json(metadata);
try {
requestBuilder.header(headerName, URLEncoder.encode(encodedMetadata, UTF_8));
}
catch (UnsupportedEncodingException e) {
requestBuilder.header(headerName, encodedMetadata);
}
}
}
}

@ -26,10 +26,12 @@ import java.util.stream.Collectors;
import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientFilter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
@ -47,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class MetadataTransferAutoConfigurationTest {
private final ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner();
private final ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner = new ReactiveWebApplicationContextRunner();
/**
* No any web application.
@ -88,6 +91,23 @@ public class MetadataTransferAutoConfigurationTest {
});
}
/**
* Reactive web application.
*/
@Test
public void test3() {
this.reactiveWebApplicationContextRunner.withConfiguration(AutoConfigurations.of(MetadataTransferAutoConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(MetadataTransferAutoConfiguration.MetadataTransferFeignInterceptorConfig.class);
assertThat(context).hasSingleBean(EncodeTransferMedataFeignInterceptor.class);
assertThat(context).hasSingleBean(MetadataTransferAutoConfiguration.MetadataTransferRestTemplateConfig.class);
assertThat(context).hasSingleBean(EncodeTransferMedataRestTemplateInterceptor.class);
assertThat(context).hasSingleBean(MetadataTransferAutoConfiguration.MetadataTransferScgFilterConfig.class);
assertThat(context).hasSingleBean(GlobalFilter.class);
assertThat(context).hasSingleBean(EncodeTransferMedataWebClientFilter.class);
});
}
@Configuration
static class RestTemplateConfiguration {

@ -0,0 +1,83 @@
/*
* 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.metadata.core;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* Test for {@link EncodeTransferMedataWebClientFilter}.
*
* @author sean yu
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT,
classes = EncodeTransferMedataWebClientFilterTest.TestApplication.class,
properties = {"spring.config.location = classpath:application-test.yml",
"spring.main.web-application-type = reactive"})
public class EncodeTransferMedataWebClientFilterTest {
@Autowired
private WebClient.Builder webClientBuilder;
@LocalServerPort
private int localServerPort;
@Test
public void testTransitiveMetadataFromApplicationConfig() {
MetadataContext metadataContext = MetadataContextHolder.get();
metadataContext.setTransHeadersKV("xxx", "xxx");
String metadata = webClientBuilder.baseUrl("http://localhost:" + localServerPort).build()
.get()
.uri("/test")
.retrieve()
.bodyToMono(String.class)
.block();
assertThat(metadata).isEqualTo("2");
}
@SpringBootApplication
@RestController
protected static class TestApplication {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@RequestMapping("/test")
public String test() {
return MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "b");
}
}
}

@ -15,8 +15,34 @@
<dependencies>
<!-- Spring Cloud Tencent dependencies start -->
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-loadbalancer</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<optional>true</optional>
</dependency>
<dependency>
@ -60,6 +86,21 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>healthchecker-http</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>healthchecker-udp</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>healthchecker-tcp</artifactId>
</dependency>
<!-- Polaris dependencies end -->
<dependency>
@ -67,5 +108,35 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>polaris-test-common</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>polaris-test-mock-discovery</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,102 @@
/*
* 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.util.function.Function;
import java.util.function.Supplier;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode;
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.api.FunctionalDecorator;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest;
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
/**
* PolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreaker implements CircuitBreaker, InvokeHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreaker.class);
private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf;
private final ConsumerAPI consumerAPI;
private final FunctionalDecorator decorator;
private final InvokeHandler invokeHandler;
public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
ConsumerAPI consumerAPI,
CircuitBreakAPI circuitBreakAPI) {
FunctionalDecoratorRequest makeDecoratorRequest = new FunctionalDecoratorRequest(new ServiceKey(conf.getNamespace(), conf.getService()), conf.getMethod());
makeDecoratorRequest.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
makeDecoratorRequest.setResultToErrorCode(new PolarisResultToErrorCode());
this.consumerAPI = consumerAPI;
this.conf = conf;
this.decorator = circuitBreakAPI.makeFunctionalDecorator(makeDecoratorRequest);
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(makeDecoratorRequest);
}
@Override
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
Supplier<T> toRunDecorator = decorator.decorateSupplier(toRun);
try {
return toRunDecorator.get();
}
catch (CallAbortedException e) {
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, e);
return fallback.apply(e);
}
catch (Exception e) {
return fallback.apply(e);
}
}
@Override
public void acquirePermission() {
invokeHandler.acquirePermission();
}
@Override
public void onSuccess(InvokeContext.ResponseContext responseContext) {
invokeHandler.onSuccess(responseContext);
}
@Override
public void onError(InvokeContext.ResponseContext responseContext) {
invokeHandler.onError(responseContext);
}
public PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration getConf() {
return conf;
}
public ConsumerAPI getConsumerAPI() {
return consumerAPI;
}
}

@ -0,0 +1,76 @@
/*
* 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.util.function.Function;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
/**
* PolarisCircuitBreakerFactory.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerFactory
extends CircuitBreakerFactory<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> {
private Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration =
id -> {
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
return new PolarisCircuitBreakerConfigBuilder()
.namespace(metadata[0])
.service(metadata[1])
.method(metadata[2])
.build();
};
private final CircuitBreakAPI circuitBreakAPI;
private final ConsumerAPI consumerAPI;
public PolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
this.circuitBreakAPI = circuitBreakAPI;
this.consumerAPI = consumerAPI;
}
@Override
public CircuitBreaker create(String id) {
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations()
.computeIfAbsent(id, defaultConfiguration);
return new PolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI);
}
@Override
protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) {
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2]);
}
@Override
public void configureDefault(Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
this.defaultConfiguration = defaultConfiguration;
}
}

@ -0,0 +1,91 @@
/*
* 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.util.function.Function;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode;
import com.tencent.cloud.polaris.circuitbreaker.reactor.PolarisCircuitBreakerReactorTransformer;
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest;
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
/**
* ReactivePolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class ReactivePolarisCircuitBreaker implements ReactiveCircuitBreaker {
private final InvokeHandler invokeHandler;
private final ConsumerAPI consumerAPI;
private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf;
public ReactivePolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
ConsumerAPI consumerAPI,
CircuitBreakAPI circuitBreakAPI) {
InvokeContext.RequestContext requestContext = new FunctionalDecoratorRequest(new ServiceKey(conf.getNamespace(), conf.getService()), conf.getMethod());
requestContext.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
requestContext.setResultToErrorCode(new PolarisResultToErrorCode());
this.consumerAPI = consumerAPI;
this.conf = conf;
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(requestContext);
}
@Override
public <T> Mono<T> run(Mono<T> toRun, Function<Throwable, Mono<T>> fallback) {
Mono<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
if (fallback != null) {
toReturn = toReturn.onErrorResume(throwable -> {
if (throwable instanceof CallAbortedException) {
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
}
return fallback.apply(throwable);
});
}
return toReturn;
}
@Override
public <T> Flux<T> run(Flux<T> toRun, Function<Throwable, Flux<T>> fallback) {
Flux<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
if (fallback != null) {
toReturn = toReturn.onErrorResume(throwable -> {
if (throwable instanceof CallAbortedException) {
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
}
return fallback.apply(throwable);
});
}
return toReturn;
}
}

@ -0,0 +1,76 @@
/*
* 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.util.function.Function;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
/**
* ReactivePolarisCircuitBreakerFactory.
*
* @author seanyu 2023-02-27
*/
public class ReactivePolarisCircuitBreakerFactory extends
ReactiveCircuitBreakerFactory<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> {
private Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration =
id -> {
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
return new PolarisCircuitBreakerConfigBuilder()
.namespace(metadata[0])
.service(metadata[1])
.method(metadata[2])
.build();
};
private final CircuitBreakAPI circuitBreakAPI;
private final ConsumerAPI consumerAPI;
public ReactivePolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
this.circuitBreakAPI = circuitBreakAPI;
this.consumerAPI = consumerAPI;
}
@Override
public ReactiveCircuitBreaker create(String id) {
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations()
.computeIfAbsent(id, defaultConfiguration);
return new ReactivePolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI);
}
@Override
protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) {
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2]);
}
@Override
public void configureDefault(
Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
this.defaultConfiguration = defaultConfiguration;
}
}

@ -0,0 +1,62 @@
/*
* 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.common;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.polaris.context.PolarisConfigModifier;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.polaris.api.config.consumer.ServiceRouterConfig;
import com.tencent.polaris.factory.config.ConfigurationImpl;
import com.tencent.polaris.plugins.router.healthy.RecoverRouterConfig;
/**
* CircuitBreakerConfigModifier.
*
* @author seanyu 2023-02-27
*/
public class CircuitBreakerConfigModifier implements PolarisConfigModifier {
private final RpcEnhancementReporterProperties properties;
public CircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
this.properties = properties;
}
@Override
public void modify(ConfigurationImpl configuration) {
properties.setEnabled(true);
// Turn on circuitbreaker configuration
configuration.getConsumer().getCircuitBreaker().setEnable(true);
// Set excludeCircuitBreakInstances to true
RecoverRouterConfig recoverRouterConfig = configuration.getConsumer().getServiceRouter()
.getPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, RecoverRouterConfig.class);
recoverRouterConfig.setExcludeCircuitBreakInstances(true);
// Update modified config to source properties
configuration.getConsumer().getServiceRouter()
.setPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, recoverRouterConfig);
}
@Override
public int getOrder() {
return ContextConstant.ModifierOrder.CIRCUIT_BREAKER_ORDER;
}
}

@ -0,0 +1,116 @@
/*
* 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.common;
import com.tencent.cloud.common.metadata.MetadataContext;
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
/**
* PolarisCircuitBreakerConfigBuilder.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerConfigBuilder implements ConfigBuilder<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> {
private String namespace = MetadataContext.LOCAL_NAMESPACE;
private String service;
private String method;
public PolarisCircuitBreakerConfigBuilder() {
}
public PolarisCircuitBreakerConfigBuilder(String namespace, String service, String method) {
this.namespace = namespace;
this.service = service;
this.method = method;
}
public PolarisCircuitBreakerConfigBuilder namespace(String namespace) {
this.namespace = namespace;
return this;
}
public PolarisCircuitBreakerConfigBuilder service(String service) {
this.service = service;
return this;
}
public PolarisCircuitBreakerConfigBuilder method(String method) {
this.method = method;
return this;
}
@Override
public PolarisCircuitBreakerConfiguration build() {
PolarisCircuitBreakerConfiguration conf = new PolarisCircuitBreakerConfiguration();
conf.setNamespace(namespace);
conf.setService(service);
conf.setMethod(method);
return conf;
}
public static class PolarisCircuitBreakerConfiguration {
private final String sourceNamespace = MetadataContext.LOCAL_NAMESPACE;
private final String sourceService = MetadataContext.LOCAL_SERVICE;
private String namespace;
private String service;
private String method;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getSourceNamespace() {
return sourceNamespace;
}
public String getSourceService() {
return sourceService;
}
}
}

@ -0,0 +1,69 @@
/*
* 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.common;
import com.tencent.cloud.polaris.circuitbreaker.zuul.PolarisCircuitBreakerPostZuulFilter;
import com.tencent.polaris.circuitbreak.api.pojo.ResultToErrorCode;
import feign.FeignException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/**
* PolarisResultToErrorCode.
*
* @author seanyu 2023-02-27
*/
public class PolarisResultToErrorCode implements ResultToErrorCode {
@Override
public int onSuccess(Object value) {
return 200;
}
@Override
public int onError(Throwable e) {
if (checkClassExist("org.springframework.web.client.RestClientResponseException")
&& e instanceof RestClientResponseException) {
return ((RestClientResponseException) e).getRawStatusCode();
}
else if (checkClassExist("feign.FeignException")
&& e instanceof FeignException) {
return ((FeignException) e).status();
}
else if (checkClassExist("org.springframework.web.reactive.function.client.WebClientResponseException")
&& e instanceof WebClientResponseException) {
return ((WebClientResponseException) e).getRawStatusCode();
}
else if (e instanceof PolarisCircuitBreakerPostZuulFilter.CircuitBreakerStatusCodeException) {
return ((PolarisCircuitBreakerPostZuulFilter.CircuitBreakerStatusCodeException) e).getRawStatusCode();
}
return -1;
}
private boolean checkClassExist(String clazzName) {
try {
Class.forName(clazzName, false, getClass().getClassLoader());
}
catch (ClassNotFoundException e) {
return false;
}
return true;
}
}

@ -0,0 +1,71 @@
/*
* 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.config;
import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory;
import com.tencent.cloud.polaris.circuitbreaker.gateway.PolarisCircuitBreakerFilterFactory;
import org.springframework.beans.factory.ObjectProvider;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.DispatcherHandler;
/**
* GatewayPolarisCircuitBreakerAutoConfiguration.
*
* @author seanyu 2023-02-27
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@AutoConfigureAfter({ReactivePolarisCircuitBreakerAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, ReactivePolarisCircuitBreakerAutoConfiguration.class,
ReactiveCircuitBreakerFactory.class, ReactivePolarisCircuitBreakerFactory.class, GatewayAutoConfiguration.class})
public class GatewayPolarisCircuitBreakerAutoConfiguration {
@Bean
@ConditionalOnBean(ReactiveCircuitBreakerFactory.class)
@ConditionalOnEnabledFilter
public PolarisCircuitBreakerFilterFactory polarisCircuitBreakerFilterFactory(
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
ObjectProvider<DispatcherHandler> dispatcherHandler,
@Autowired(required = false) ReactiveDiscoveryClient discoveryClient,
@Autowired(required = false) DiscoveryLocatorProperties properties
) {
return new PolarisCircuitBreakerFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler, discoveryClient, properties);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledFilter
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
return new FallbackHeadersGatewayFilterFactory();
}
}

@ -17,62 +17,134 @@
package com.tencent.cloud.polaris.circuitbreaker.config;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.polaris.context.PolarisConfigModifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory;
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter;
import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerRestTemplateBeanPostProcessor;
import com.tencent.cloud.polaris.circuitbreaker.zuul.PolarisCircuitBreakerPostZuulFilter;
import com.tencent.cloud.polaris.circuitbreaker.zuul.PolarisCircuitBreakerZuulFilter;
import com.tencent.cloud.polaris.circuitbreaker.zuul.PolarisZuulFallbackFactory;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.polaris.api.config.consumer.ServiceRouterConfig;
import com.tencent.polaris.factory.config.ConfigurationImpl;
import com.tencent.polaris.plugins.router.healthy.RecoverRouterConfig;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
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.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.cloud.openfeign.PolarisFeignCircuitBreakerTargeterAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
/**
* Autoconfiguration at bootstrap phase.
* Autoconfiguration for PolarisCircuitBreaker.
*
* @author lepdou 2022-03-29
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnPolarisCircuitBreakerEnabled
@AutoConfigureAfter(RpcEnhancementAutoConfiguration.class)
@Import({
ReactivePolarisCircuitBreakerAutoConfiguration.class,
PolarisFeignCircuitBreakerTargeterAutoConfiguration.class,
PolarisCircuitBreakerFeignClientAutoConfiguration.class,
GatewayPolarisCircuitBreakerAutoConfiguration.class
})
public class PolarisCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<PolarisCircuitBreakerFactory>> customizers = new ArrayList<>();
{
// close zuul hystrix
System.setProperty("feign.hystrix.enabled", "false");
System.setProperty("hystrix.command.default.circuitBreaker.enabled", "false");
}
@Bean
public CircuitBreakerConfigModifier circuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
return new CircuitBreakerConfigModifier(properties);
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
public static PolarisCircuitBreakerRestTemplateBeanPostProcessor polarisCircuitBreakerRestTemplateBeanPostProcessor(
ApplicationContext applicationContext) {
return new PolarisCircuitBreakerRestTemplateBeanPostProcessor(applicationContext);
}
public static class CircuitBreakerConfigModifier implements PolarisConfigModifier {
@Bean
@ConditionalOnMissingBean(CircuitBreakAPI.class)
public CircuitBreakAPI circuitBreakAPI(SDKContext polarisContext) {
return CircuitBreakAPIFactory.createCircuitBreakAPIByContext(polarisContext);
}
private final RpcEnhancementReporterProperties properties;
@Bean
@ConditionalOnMissingBean(SuccessCircuitBreakerReporter.class)
public SuccessCircuitBreakerReporter successCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
return new SuccessCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
}
public CircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
this.properties = properties;
@Bean
@ConditionalOnMissingBean(ExceptionCircuitBreakerReporter.class)
public ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
return new ExceptionCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
}
@Override
public void modify(ConfigurationImpl configuration) {
properties.setEnabled(true);
@Bean
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
public CircuitBreakerFactory polarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
PolarisCircuitBreakerFactory factory = new PolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
// Turn on circuitbreaker configuration
configuration.getConsumer().getCircuitBreaker().setEnable(true);
@Bean
@ConditionalOnBean(RpcEnhancementReporterProperties.class)
@ConditionalOnMissingBean(CircuitBreakerConfigModifier.class)
public CircuitBreakerConfigModifier circuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
return new CircuitBreakerConfigModifier(properties);
}
// Set excludeCircuitBreakInstances to true
RecoverRouterConfig recoverRouterConfig = configuration.getConsumer().getServiceRouter()
.getPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, RecoverRouterConfig.class);
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "com.netflix.zuul.http.ZuulServlet")
protected static class PolarisCircuitBreakerZuulFilterConfig {
recoverRouterConfig.setExcludeCircuitBreakInstances(true);
@Autowired(required = false)
private Set<FallbackProvider> zuulFallbackProviders = Collections.emptySet();
// Update modified config to source properties
configuration.getConsumer().getServiceRouter()
.setPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, recoverRouterConfig);
@Bean
public PolarisZuulFallbackFactory polarisZuulFallbackFactory() {
return new PolarisZuulFallbackFactory(zuulFallbackProviders);
}
@Override
public int getOrder() {
return ContextConstant.ModifierOrder.CIRCUIT_BREAKER_ORDER;
@Bean
public PolarisCircuitBreakerZuulFilter polarisCircuitBreakerZuulFilter(
CircuitBreakerFactory circuitBreakerFactory,
PolarisZuulFallbackFactory polarisZuulFallbackFactory,
Environment environment) {
return new PolarisCircuitBreakerZuulFilter(circuitBreakerFactory, polarisZuulFallbackFactory, environment);
}
@Bean
public PolarisCircuitBreakerPostZuulFilter polarisCircuitBreakerPostZuulFilter(
PolarisZuulFallbackFactory polarisZuulFallbackFactory,
Environment environment) {
return new PolarisCircuitBreakerPostZuulFilter(polarisZuulFallbackFactory, environment);
}
}
}

@ -17,7 +17,10 @@
package com.tencent.cloud.polaris.circuitbreaker.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -27,8 +30,8 @@ import org.springframework.context.annotation.Import;
* @author lepdou 2022-03-29
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("spring.cloud.polaris.enabled")
@Import(PolarisCircuitBreakerAutoConfiguration.class)
@ConditionalOnPolarisEnabled
@Import({PolarisContextAutoConfiguration.class, RpcEnhancementAutoConfiguration.class, PolarisCircuitBreakerAutoConfiguration.class})
public class PolarisCircuitBreakerBootstrapConfiguration {
}

@ -0,0 +1,56 @@
/*
* 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.config;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignCircuitBreaker;
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.FeignClientFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* PolarisCircuitBreakerFeignClientAutoConfiguration.
*
* @author seansyyu 2023-02-28
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Feign.class, FeignClientFactoryBean.class})
@ConditionalOnPolarisCircuitBreakerEnabled
public class PolarisCircuitBreakerFeignClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public PolarisCircuitBreakerNameResolver polarisCircuitBreakerNameResolver() {
return new PolarisCircuitBreakerNameResolver();
}
@Bean
@Scope("prototype")
@ConditionalOnBean(CircuitBreakerFactory.class)
@ConditionalOnMissingBean(Feign.Builder.class)
public Feign.Builder circuitBreakerFeignBuilder() {
return PolarisFeignCircuitBreaker.builder();
}
}

@ -0,0 +1,93 @@
/*
* 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.config;
import java.util.ArrayList;
import java.util.List;
import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory;
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter;
import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
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.Customizer;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AutoConfiguration for ReactivePolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = { "reactor.core.publisher.Mono", "reactor.core.publisher.Flux" })
@ConditionalOnPolarisCircuitBreakerEnabled
@AutoConfigureAfter(RpcEnhancementAutoConfiguration.class)
public class ReactivePolarisCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<ReactivePolarisCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean
@ConditionalOnMissingBean(CircuitBreakAPI.class)
public CircuitBreakAPI circuitBreakAPI(SDKContext polarisContext) {
return CircuitBreakAPIFactory.createCircuitBreakAPIByContext(polarisContext);
}
@Bean
@ConditionalOnMissingBean(SuccessCircuitBreakerReporter.class)
public SuccessCircuitBreakerReporter successCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
return new SuccessCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
}
@Bean
@ConditionalOnMissingBean(ExceptionCircuitBreakerReporter.class)
public ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
return new ExceptionCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
}
@Bean
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
@Bean
@ConditionalOnBean(RpcEnhancementReporterProperties.class)
@ConditionalOnMissingBean(CircuitBreakerConfigModifier.class)
public CircuitBreakerConfigModifier reactiveCircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
return new CircuitBreakerConfigModifier(properties);
}
}

@ -0,0 +1,97 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.feign;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import feign.Request;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import org.springframework.cloud.openfeign.FallbackFactory;
/**
* PolarisCircuitBreakerFallbackFactory.
*
* @author sean yu
*/
public class PolarisCircuitBreakerFallbackFactory implements FallbackFactory {
private final Decoder decoder;
public PolarisCircuitBreakerFallbackFactory(Decoder decoder) {
this.decoder = decoder;
}
@Override
public Object create(Throwable t) {
return new DefaultFallback(t, decoder);
}
public class DefaultFallback {
private final Throwable t;
private final Decoder decoder;
public DefaultFallback(Throwable t, Decoder decoder) {
this.t = t;
this.decoder = decoder;
}
public Object fallback(Method method) {
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
Response.Builder responseBuilder = Response.builder()
.status(fallbackInfo.getCode());
if (fallbackInfo.getHeaders() != null) {
Map<String, Collection<String>> headers = new HashMap<>();
fallbackInfo.getHeaders().forEach((k, v) -> headers.put(k, Collections.singleton(v)));
responseBuilder.headers(headers);
}
if (fallbackInfo.getBody() != null) {
responseBuilder.body(fallbackInfo.getBody(), StandardCharsets.UTF_8);
}
// Feign Response need a nonnull Request,
// which is not important in fallback response (no real request),
// so we create a fake one
Request fakeRequest = Request.create(Request.HttpMethod.GET, "/", new HashMap<>(), Request.Body.empty(), new RequestTemplate());
responseBuilder.request(fakeRequest);
try (Response response = responseBuilder.build()) {
return decoder.decode(response, method.getGenericReturnType());
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
throw new IllegalStateException(t);
}
}
}

@ -0,0 +1,49 @@
/*
* 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.Method;
import com.tencent.cloud.common.metadata.MetadataContext;
import feign.Target;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
/**
* PolarisCircuitBreakerNameResolver.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerNameResolver {
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
String serviceName = target.name();
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
String path = "";
if (requestMapping != null) {
path = requestMapping.path().length == 0 ?
requestMapping.value().length == 0 ? "" : requestMapping.value()[0] :
requestMapping.path()[0];
}
return "".equals(path) ?
MetadataContext.LOCAL_NAMESPACE + "#" + serviceName :
MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path;
}
}

@ -0,0 +1,99 @@
/*
* 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.Field;
import feign.Feign;
import feign.Target;
import feign.codec.Decoder;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignCircuitBreaker;
import org.springframework.util.ReflectionUtils;
/**
* PolarisFeignCircuitBreaker, mostly copy from {@link 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 Builder builder() {
return new Builder();
}
/**
* Builder for Feign CircuitBreaker integration.
*/
public static final class Builder extends Feign.Builder {
private CircuitBreakerFactory circuitBreakerFactory;
private String feignClientName;
private PolarisCircuitBreakerNameResolver circuitBreakerNameResolver;
public Builder() {
}
public Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
return this;
}
public Builder feignClientName(String feignClientName) {
this.feignClientName = feignClientName;
return this;
}
public Builder circuitBreakerNameResolver(PolarisCircuitBreakerNameResolver circuitBreakerNameResolver) {
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
return this;
}
public <T> T target(Target<T> target, T fallback) {
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
}
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
return build(fallbackFactory).newInstance(target);
}
@Override
public <T> T target(Target<T> target) {
return build(null).newInstance(target);
}
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
Field field = ReflectionUtils.findField(Feign.Builder.class, "decoder");
field.setAccessible(true);
Decoder decoder = (Decoder) ReflectionUtils.getField(field, this);
this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler(
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, decoder));
return this.build();
}
}
}

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

@ -0,0 +1,278 @@
/*
* 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.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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServiceUnavailableException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import static java.util.Optional.ofNullable;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset;
/**
* PolarisCircuitBreakerFilterFactory.
* mostly copy from SpringCloudCircuitBreakerFilterFactory, but create ReactiveCircuitBreaker per request to build method level CircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;
private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;
private String routeIdPrefix;
// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
private volatile DispatcherHandler dispatcherHandler;
public PolarisCircuitBreakerFilterFactory(
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider,
ReactiveDiscoveryClient discoveryClient,
DiscoveryLocatorProperties properties
) {
super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider);
this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory;
this.dispatcherHandlerProvider = dispatcherHandlerProvider;
if (discoveryClient != null && properties != null) {
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
routeIdPrefix = properties.getRouteIdPrefix();
}
else {
routeIdPrefix = discoveryClient.getClass().getSimpleName() + "_";
}
}
}
private void addExceptionDetails(Throwable t, ServerWebExchange exchange) {
ofNullable(t).ifPresent(
exception -> exchange.getAttributes().put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception));
}
private DispatcherHandler getDispatcherHandler() {
if (dispatcherHandler == null) {
dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
}
return dispatcherHandler;
}
private String getCircuitBreakerId(Config config) {
if (!StringUtils.hasText(config.getName()) && StringUtils.hasText(config.getRouteId())) {
if (routeIdPrefix != null && config.getRouteId().startsWith(routeIdPrefix)) {
return config.getRouteId().replace(routeIdPrefix, "");
}
return config.getRouteId();
}
return config.getName();
}
private boolean isNumeric(String statusString) {
try {
Integer.parseInt(statusString);
return true;
}
catch (NumberFormatException e) {
return false;
}
}
private List<HttpStatus> getSeriesStatus(String series) {
if (!Arrays.asList("1**", "2**", "3**", "4**", "5**").contains(series)) {
throw new InvalidPropertyException(Config.class, "statusCodes", "polaris circuit breaker status code can only be a numeric http status, or a http series pattern, e.g. [\"1**\",\"2**\",\"3**\",\"4**\",\"5**\"]");
}
HttpStatus[] allHttpStatus = HttpStatus.values();
if (series.startsWith("1")) {
return Arrays.stream(allHttpStatus).filter(HttpStatus::is1xxInformational).collect(Collectors.toList());
}
else if (series.startsWith("2")) {
return Arrays.stream(allHttpStatus).filter(HttpStatus::is2xxSuccessful).collect(Collectors.toList());
}
else if (series.startsWith("3")) {
return Arrays.stream(allHttpStatus).filter(HttpStatus::is3xxRedirection).collect(Collectors.toList());
}
else if (series.startsWith("4")) {
return Arrays.stream(allHttpStatus).filter(HttpStatus::is4xxClientError).collect(Collectors.toList());
}
else if (series.startsWith("5")) {
return Arrays.stream(allHttpStatus).filter(HttpStatus::is5xxServerError).collect(Collectors.toList());
}
return Arrays.asList(allHttpStatus);
}
private Set<HttpStatus> getDefaultStatus() {
return Arrays.stream(HttpStatus.values())
.filter(HttpStatus::is5xxServerError)
.collect(Collectors.toSet());
}
@Override
public GatewayFilter apply(Config config) {
Set<HttpStatus> statuses = config.getStatusCodes().stream()
.flatMap(statusCode -> {
List<HttpStatus> httpStatuses = new ArrayList<>();
if (isNumeric(statusCode)) {
httpStatuses.add(HttpStatusHolder.parse(statusCode).getHttpStatus());
}
else {
httpStatuses.addAll(getSeriesStatus(statusCode));
}
return httpStatuses.stream();
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(statuses)) {
statuses.addAll(getDefaultStatus());
}
String circuitBreakerId = getCircuitBreakerId(config);
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
String serviceName = circuitBreakerId;
if (route != null) {
serviceName = route.getUri().getHost();
}
String path = exchange.getRequest().getPath().value();
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<HttpStatus> statusNeedToCheck = new HashSet<>();
statusNeedToCheck.addAll(statuses);
statusNeedToCheck.addAll(getDefaultStatus());
HttpStatus status = exchange.getResponse().getStatusCode();
if (statusNeedToCheck.contains(status)) {
throw new CircuitBreakerStatusCodeException(status);
}
}),
t -> {
// pre-check CircuitBreakerStatusCodeException's status matches input status
if (t instanceof CircuitBreakerStatusCodeException) {
HttpStatus status = ((CircuitBreakerStatusCodeException) t).getStatusCode();
// no need to fallback
if (!statuses.contains(status)) {
return Mono.error(t);
}
}
// do fallback
if (config.getFallbackUri() == null) {
// polaris checking
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(fallbackInfo.getCode());
if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders()
.forEach((k, v) -> response.getHeaders().add(k, v));
}
DataBuffer bodyBuffer = null;
if (fallbackInfo.getBody() != null) {
byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8);
bodyBuffer = response.bufferFactory().wrap(bytes);
}
return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete();
}
}
return Mono.error(t);
}
exchange.getResponse().setStatusCode(null);
reset(exchange);
// TODO: copied from RouteToRequestUrlFilter
URI uri = exchange.getRequest().getURI();
// TODO: assume always?
boolean encoded = containsEncodedParts(uri);
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
.uri(config.getFallbackUri()).scheme(null).build(encoded).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
addExceptionDetails(t, exchange);
// Reset the exchange
reset(exchange);
ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build();
return getDispatcherHandler().handle(exchange.mutate().request(request).build());
})
.onErrorResume(t -> handleErrorWithoutFallback(t));
}
@Override
public String toString() {
return filterToStringCreator(PolarisCircuitBreakerFilterFactory.this)
.append("name", config.getName()).append("fallback", config.getFallbackUri()).toString();
}
};
}
@Override
protected Mono<Void> handleErrorWithoutFallback(Throwable t) {
if (t instanceof java.util.concurrent.TimeoutException) {
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
}
if (t instanceof CallAbortedException) {
return Mono.error(new ServiceUnavailableException());
}
if (t instanceof CircuitBreakerStatusCodeException) {
return Mono.empty();
}
return Mono.error(t);
}
}

@ -0,0 +1,57 @@
/*
* 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.reactor;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxOperator;
import reactor.core.publisher.Operators;
/**
* FluxOperator for PolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerFluxOperator<T> extends FluxOperator<T, T> {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerFluxOperator.class);
private final InvokeHandler invokeHandler;
protected PolarisCircuitBreakerFluxOperator(Flux<? extends T> source, InvokeHandler invokeHandler) {
super(source);
this.invokeHandler = invokeHandler;
}
@Override
public void subscribe(CoreSubscriber<? super T> actual) {
try {
invokeHandler.acquirePermission();
source.subscribe(new PolarisCircuitBreakerReactorSubscriber<>(invokeHandler, actual, false));
}
catch (CallAbortedException e) {
LOGGER.debug("ReactivePolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
Operators.error(actual, e);
}
}
}

@ -0,0 +1,57 @@
/*
* 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.reactor;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoOperator;
import reactor.core.publisher.Operators;
/**
* MonoOperator for PolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerMonoOperator<T> extends MonoOperator<T, T> {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerMonoOperator.class);
private final InvokeHandler invokeHandler;
protected PolarisCircuitBreakerMonoOperator(Mono<? extends T> source, InvokeHandler invokeHandler) {
super(source);
this.invokeHandler = invokeHandler;
}
@Override
public void subscribe(CoreSubscriber<? super T> actual) {
try {
invokeHandler.acquirePermission();
source.subscribe(new PolarisCircuitBreakerReactorSubscriber<>(invokeHandler, actual, true));
}
catch (CallAbortedException e) {
LOGGER.debug("ReactivePolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
Operators.error(actual, e);
}
}
}

@ -0,0 +1,119 @@
/*
* 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.reactor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
import reactor.util.context.Context;
/**
* Reactor Subscriber for PolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerReactorSubscriber<T> extends BaseSubscriber<T> {
private final InvokeHandler invokeHandler;
private final CoreSubscriber<? super T> downstreamSubscriber;
private final long startTimeMilli;
private final boolean singleProducer;
private final AtomicBoolean successSignaled = new AtomicBoolean(false);
private final AtomicBoolean eventWasEmitted = new AtomicBoolean(false);
public PolarisCircuitBreakerReactorSubscriber(InvokeHandler invokeHandler, CoreSubscriber<? super T> downstreamSubscriber, boolean singleProducer) {
this.invokeHandler = invokeHandler;
this.downstreamSubscriber = downstreamSubscriber;
this.singleProducer = singleProducer;
this.startTimeMilli = System.currentTimeMillis();
}
@Override
public Context currentContext() {
return downstreamSubscriber.currentContext();
}
@Override
protected void hookOnSubscribe(Subscription subscription) {
downstreamSubscriber.onSubscribe(this);
}
@Override
protected void hookOnNext(T value) {
if (!isDisposed()) {
if (singleProducer && successSignaled.compareAndSet(false, true)) {
long delay = System.currentTimeMillis() - startTimeMilli;
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
responseContext.setDuration(delay);
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
responseContext.setResult(value);
invokeHandler.onSuccess(responseContext);
}
eventWasEmitted.set(true);
downstreamSubscriber.onNext(value);
}
}
@Override
protected void hookOnComplete() {
if (successSignaled.compareAndSet(false, true)) {
long delay = System.currentTimeMillis() - startTimeMilli;
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
responseContext.setDuration(delay);
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
invokeHandler.onSuccess(responseContext);
}
downstreamSubscriber.onComplete();
}
@Override
public void hookOnCancel() {
if (!successSignaled.get()) {
if (eventWasEmitted.get()) {
long delay = System.currentTimeMillis() - startTimeMilli;
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
responseContext.setDuration(delay);
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
invokeHandler.onSuccess(responseContext);
}
}
}
@Override
protected void hookOnError(Throwable e) {
long delay = System.currentTimeMillis() - startTimeMilli;
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
responseContext.setDuration(delay);
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
responseContext.setError(e);
invokeHandler.onError(responseContext);
downstreamSubscriber.onError(e);
}
}

@ -0,0 +1,53 @@
/*
* 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.reactor;
import java.util.function.Function;
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Reactor Transformer for PolarisCircuitBreaker.
*
* @author seanyu 2023-02-27
*/
public class PolarisCircuitBreakerReactorTransformer<T> implements Function<Publisher<T>, Publisher<T>> {
private final InvokeHandler invokeHandler;
public PolarisCircuitBreakerReactorTransformer(InvokeHandler invokeHandler) {
this.invokeHandler = invokeHandler;
}
@Override
public Publisher<T> apply(Publisher<T> publisher) {
if (publisher instanceof Mono) {
return new PolarisCircuitBreakerMonoOperator<>((Mono<? extends T>) publisher, invokeHandler);
}
else if (publisher instanceof Flux) {
return new PolarisCircuitBreakerFluxOperator<>((Flux<? extends T>) publisher, invokeHandler);
}
else {
throw new IllegalStateException("Publisher type is not supported: " + publisher.getClass().getCanonicalName());
}
}
}

@ -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.reporter;
import java.util.Optional;
import com.tencent.cloud.rpc.enhancement.AbstractPolarisReporterAdapter;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.Ordered;
public class ExceptionCircuitBreakerReporter extends AbstractPolarisReporterAdapter implements EnhancedPlugin {
private static final Logger LOG = LoggerFactory.getLogger(ExceptionCircuitBreakerReporter.class);
private final CircuitBreakAPI circuitBreakAPI;
public ExceptionCircuitBreakerReporter(RpcEnhancementReporterProperties reportProperties,
SDKContext context,
CircuitBreakAPI circuitBreakAPI) {
super(reportProperties, context);
this.circuitBreakAPI = circuitBreakAPI;
}
@Override
public String getName() {
return ExceptionCircuitBreakerReporter.class.getName();
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.EXCEPTION;
}
@Override
public void run(EnhancedPluginContext context) throws Throwable {
if (!super.reportProperties.isEnabled()) {
return;
}
EnhancedRequestContext request = context.getRequest();
ServiceInstance serviceInstance = Optional.ofNullable(context.getServiceInstance()).orElse(new DefaultServiceInstance());
ResourceStat resourceStat = createInstanceResourceStat(
serviceInstance.getServiceId(),
serviceInstance.getHost(),
serviceInstance.getPort(),
request.getUrl(),
null,
context.getDelay(),
context.getThrowable()
);
LOG.debug("Will report CircuitBreaker ResourceStat of {}. Request=[{} {}]. Response=[{}]. Delay=[{}]ms.",
resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), context.getThrowable().getMessage(), context.getDelay());
circuitBreakAPI.report(resourceStat);
}
@Override
public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) {
LOG.error("ExceptionCircuitBreakerReporter runs failed. context=[{}].",
context, throwable);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 2;
}
}

@ -0,0 +1,99 @@
/*
* 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.reporter;
import java.util.Optional;
import com.tencent.cloud.rpc.enhancement.AbstractPolarisReporterAdapter;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter;
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.Ordered;
public class SuccessCircuitBreakerReporter extends AbstractPolarisReporterAdapter implements EnhancedPlugin {
private static final Logger LOG = LoggerFactory.getLogger(SuccessPolarisReporter.class);
private final CircuitBreakAPI circuitBreakAPI;
public SuccessCircuitBreakerReporter(RpcEnhancementReporterProperties reportProperties,
SDKContext context,
CircuitBreakAPI circuitBreakAPI) {
super(reportProperties, context);
this.circuitBreakAPI = circuitBreakAPI;
}
@Override
public String getName() {
return SuccessCircuitBreakerReporter.class.getName();
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.POST;
}
@Override
public void run(EnhancedPluginContext context) throws Throwable {
if (!super.reportProperties.isEnabled()) {
return;
}
EnhancedRequestContext request = context.getRequest();
EnhancedResponseContext response = context.getResponse();
ServiceInstance serviceInstance = Optional.ofNullable(context.getServiceInstance()).orElse(new DefaultServiceInstance());
ResourceStat resourceStat = createInstanceResourceStat(
serviceInstance.getServiceId(),
serviceInstance.getHost(),
serviceInstance.getPort(),
request.getUrl(),
response.getHttpStatus(),
context.getDelay(),
null
);
LOG.debug("Will report CircuitBreaker ResourceStat of {}. Request=[{} {}]. Response=[{}]. Delay=[{}]ms.",
resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), response.getHttpStatus(), context.getDelay());
circuitBreakAPI.report(resourceStat);
}
@Override
public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) {
LOG.error("SuccessCircuitBreakerReporter runs failed. context=[{}].",
context, throwable);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 2;
}
}

@ -0,0 +1,54 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* PolarisCircuitBreaker annotation.
* if coded fallback or fallbackClass provided, RestTemplate will always return fallback when any exception occurs,
* if none coded fallback or fallbackClass provided, RestTemplate will return fallback response from Polaris server when fallback occurs.
* fallback and fallbackClass cannot provide at same time.
*
* @author sean yu
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PolarisCircuitBreaker {
/**
* a fallback string, will return a response { status: 200, body: fallback string} when any exception occurs.
*
* @return fallback string
*/
String fallback() default "";
/**
* a fallback Class, will return a PolarisCircuitBreakerHttpResponse when any exception occurs.
* fallback Class must be a spring bean.
*
* @return PolarisCircuitBreakerFallback
*/
Class<? extends PolarisCircuitBreakerFallback> fallbackClass() default PolarisCircuitBreakerFallback.class;
}

@ -0,0 +1,29 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
/**
* PolarisCircuitBreakerFallback.
*
* @author sean yu
*/
public interface PolarisCircuitBreakerFallback {
PolarisCircuitBreakerHttpResponse fallback();
}

@ -0,0 +1,102 @@
/*
* 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;
/**
* PolarisCircuitBreakerHttpResponse.
*
* @author sean yu
*/
public class PolarisCircuitBreakerHttpResponse extends AbstractClientHttpResponse {
private final CircuitBreakerStatus.FallbackInfo fallbackInfo;
private final HttpHeaders headers = new HttpHeaders();
private InputStream body;
public PolarisCircuitBreakerHttpResponse(int code) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, null));
}
public PolarisCircuitBreakerHttpResponse(int code, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, body));
}
public PolarisCircuitBreakerHttpResponse(int code, Map<String, String> headers, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, headers, body));
}
public PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) {
this.fallbackInfo = fallbackInfo;
if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders().forEach(headers::add);
}
if (fallbackInfo.getBody() != null) {
body = new ByteArrayInputStream(fallbackInfo.getBody().getBytes());
}
}
@Override
public final int getRawStatusCode() {
return fallbackInfo.getCode();
}
@Override
public final String getStatusText() {
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
return (status != null ? status.getReasonPhrase() : "");
}
@Override
public final void close() {
if (this.body != null) {
try {
this.body.close();
}
catch (IOException e) {
// Ignore exception on close...
}
}
}
@Override
public final InputStream getBody() {
return this.body;
}
@Override
public final HttpHeaders getHeaders() {
return this.headers;
}
public CircuitBreakerStatus.FallbackInfo getFallbackInfo() {
return this.fallbackInfo;
}
}

@ -0,0 +1,125 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.StandardMethodMetadata;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* PolarisCircuitBreakerRestTemplateBeanPostProcessor.
*
* @author sean yu
*/
public class PolarisCircuitBreakerRestTemplateBeanPostProcessor implements MergedBeanDefinitionPostProcessor {
private final ApplicationContext applicationContext;
public PolarisCircuitBreakerRestTemplateBeanPostProcessor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private final ConcurrentHashMap<String, PolarisCircuitBreaker> cache = new ConcurrentHashMap<>();
private void checkPolarisCircuitBreakerRestTemplate(PolarisCircuitBreaker polarisCircuitBreaker) {
if (
StringUtils.hasText(polarisCircuitBreaker.fallback()) &&
!PolarisCircuitBreakerFallback.class.toGenericString().equals(polarisCircuitBreaker.fallbackClass().toGenericString())
) {
throw new IllegalArgumentException("PolarisCircuitBreaker's fallback and fallbackClass could not set at sametime !");
}
}
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
if (checkAnnotated(beanDefinition, beanType, beanName)) {
PolarisCircuitBreaker polarisCircuitBreaker;
if (beanDefinition.getSource() instanceof StandardMethodMetadata) {
polarisCircuitBreaker = ((StandardMethodMetadata) beanDefinition.getSource()).getIntrospectedMethod()
.getAnnotation(PolarisCircuitBreaker.class);
}
else {
polarisCircuitBreaker = beanDefinition.getResolvedFactoryMethod()
.getAnnotation(PolarisCircuitBreaker.class);
}
checkPolarisCircuitBreakerRestTemplate(polarisCircuitBreaker);
cache.put(beanName, polarisCircuitBreaker);
}
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (cache.containsKey(beanName)) {
// add interceptor for each RestTemplate with @PolarisCircuitBreaker annotation
StringBuilder interceptorBeanNamePrefix = new StringBuilder();
PolarisCircuitBreaker polarisCircuitBreaker = cache.get(beanName);
interceptorBeanNamePrefix
.append(StringUtils.uncapitalize(
PolarisCircuitBreaker.class.getSimpleName()))
.append("_")
.append(polarisCircuitBreaker.fallback())
.append("_")
.append(polarisCircuitBreaker.fallbackClass().getSimpleName());
RestTemplate restTemplate = (RestTemplate) bean;
String interceptorBeanName = interceptorBeanNamePrefix + "@" + bean;
CircuitBreakerFactory circuitBreakerFactory = this.applicationContext.getBean(CircuitBreakerFactory.class);
registerBean(interceptorBeanName, polarisCircuitBreaker, applicationContext, circuitBreakerFactory, restTemplate);
PolarisCircuitBreakerRestTemplateInterceptor polarisCircuitBreakerRestTemplateInterceptor = applicationContext
.getBean(interceptorBeanName, PolarisCircuitBreakerRestTemplateInterceptor.class);
restTemplate.getInterceptors().add(0, polarisCircuitBreakerRestTemplateInterceptor);
}
return bean;
}
private boolean checkAnnotated(RootBeanDefinition beanDefinition,
Class<?> beanType, String beanName) {
return beanName != null && beanType == RestTemplate.class
&& beanDefinition.getSource() instanceof MethodMetadata
&& ((MethodMetadata) beanDefinition.getSource())
.isAnnotated(PolarisCircuitBreaker.class.getName());
}
private void registerBean(String interceptorBeanName, PolarisCircuitBreaker polarisCircuitBreaker,
ApplicationContext applicationContext, CircuitBreakerFactory circuitBreakerFactory, RestTemplate restTemplate) {
// register PolarisCircuitBreakerRestTemplateInterceptor bean
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext
.getAutowireCapableBeanFactory();
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(PolarisCircuitBreakerRestTemplateInterceptor.class);
beanDefinitionBuilder.addConstructorArgValue(polarisCircuitBreaker);
beanDefinitionBuilder.addConstructorArgValue(applicationContext);
beanDefinitionBuilder.addConstructorArgValue(circuitBreakerFactory);
beanDefinitionBuilder.addConstructorArgValue(restTemplate);
BeanDefinition interceptorBeanDefinition = beanDefinitionBuilder
.getRawBeanDefinition();
beanFactory.registerBeanDefinition(interceptorBeanName,
interceptorBeanDefinition);
}
}

@ -0,0 +1,101 @@
/*
* 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.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
/**
* PolarisCircuitBreakerRestTemplateInterceptor.
*
* @author sean yu
*/
public class PolarisCircuitBreakerRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final PolarisCircuitBreaker polarisCircuitBreaker;
private final ApplicationContext applicationContext;
private final CircuitBreakerFactory circuitBreakerFactory;
private final RestTemplate restTemplate;
public PolarisCircuitBreakerRestTemplateInterceptor(
PolarisCircuitBreaker polarisCircuitBreaker,
ApplicationContext applicationContext,
CircuitBreakerFactory circuitBreakerFactory,
RestTemplate restTemplate
) {
this.polarisCircuitBreaker = polarisCircuitBreaker;
this.applicationContext = applicationContext;
this.circuitBreakerFactory = circuitBreakerFactory;
this.restTemplate = restTemplate;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
return circuitBreakerFactory.create(request.getURI().getHost() + "#" + request.getURI().getPath()).run(
() -> {
try {
ClientHttpResponse response = execution.execute(request, body);
ResponseErrorHandler errorHandler = restTemplate.getErrorHandler();
if (errorHandler.hasError(response)) {
errorHandler.handleError(request.getURI(), request.getMethod(), response);
}
return response;
}
catch (IOException e) {
throw new IllegalStateException(e);
}
},
t -> {
if (StringUtils.hasText(polarisCircuitBreaker.fallback())) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, null, polarisCircuitBreaker.fallback());
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
}
if (!PolarisCircuitBreakerFallback.class.toGenericString().equals(polarisCircuitBreaker.fallbackClass().toGenericString())) {
Method method = ReflectionUtils.findMethod(PolarisCircuitBreakerFallback.class, "fallback");
PolarisCircuitBreakerFallback polarisCircuitBreakerFallback = applicationContext.getBean(polarisCircuitBreaker.fallbackClass());
return (PolarisCircuitBreakerHttpResponse) ReflectionUtils.invokeMethod(method, polarisCircuitBreakerFallback);
}
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
}
}
throw new IllegalStateException(t);
}
);
}
}

@ -0,0 +1,87 @@
/*
* 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.util;
import java.util.Objects;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.RetStatus;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.api.rpc.ServiceCallResult;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
/**
* PolarisCircuitBreakerUtils.
*
* @author seanyu 2023-02-27
*/
public final class PolarisCircuitBreakerUtils {
private static final Logger LOG = LoggerFactory.getLogger(PolarisCircuitBreakerUtils.class);
private PolarisCircuitBreakerUtils() {
}
/**
*
* @param id CircuitBreakerId
* Format: namespace#service#method or service#method or service ,
* namespace set as default spring.cloud.polaris.namespace if absent
* @return String[]{namespace, service, method}
*/
public static String[] resolveCircuitBreakerId(String id) {
Assert.hasText(id, "A CircuitBreaker must have an id. Id could be : namespace#service#method or service#method or service");
String[] polarisCircuitBreakerMetaData = id.split("#");
if (polarisCircuitBreakerMetaData.length == 2) {
return new String[]{MetadataContext.LOCAL_NAMESPACE, polarisCircuitBreakerMetaData[0], polarisCircuitBreakerMetaData[1]};
}
if (polarisCircuitBreakerMetaData.length == 3) {
return new String[]{polarisCircuitBreakerMetaData[0], polarisCircuitBreakerMetaData[1], polarisCircuitBreakerMetaData[2]};
}
return new String[]{MetadataContext.LOCAL_NAMESPACE, id, ""};
}
public static void reportStatus(ConsumerAPI consumerAPI,
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf, CallAbortedException e) {
try {
ServiceCallResult result = new ServiceCallResult();
result.setMethod(conf.getMethod());
result.setNamespace(conf.getNamespace());
result.setService(conf.getService());
result.setRuleName(e.getRuleName());
result.setRetStatus(RetStatus.RetReject);
result.setCallerService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
if (Objects.nonNull(e.getFallbackInfo())) {
result.setRetCode(e.getFallbackInfo().getCode());
}
consumerAPI.updateServiceCallResult(result);
}
catch (Throwable ex) {
LOG.error("[CircuitBreaker] report circuitbreaker call result fail ", ex);
}
}
}

@ -0,0 +1,152 @@
/*
* 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.zuul;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletResponse;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.tencent.cloud.common.util.ZuulFilterUtils;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER;
/**
* Polaris circuit breaker post-processing. Including reporting.
*
* @author Haotian Zhang
*/
public class PolarisCircuitBreakerPostZuulFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerPostZuulFilter.class);
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
private final Environment environment;
public PolarisCircuitBreakerPostZuulFilter(PolarisZuulFallbackFactory polarisZuulFallbackFactory,
Environment environment) {
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
this.environment = environment;
}
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
return StringUtils.isEmpty(enabled) || enabled.equals("true");
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse response = context.getResponse();
HttpStatus status = HttpStatus.resolve(response.getStatus());
Object polarisCircuitBreakerObject = context.get(POLARIS_CIRCUIT_BREAKER);
Object startTimeMilliObject = context.get(POLARIS_PRE_ROUTE_TIME);
if (polarisCircuitBreakerObject != null && polarisCircuitBreakerObject instanceof PolarisCircuitBreaker
&& startTimeMilliObject != null && startTimeMilliObject instanceof Long) {
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) polarisCircuitBreakerObject;
Long startTimeMilli = (Long) startTimeMilliObject;
long delay = System.currentTimeMillis() - startTimeMilli;
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
responseContext.setDuration(delay);
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
if (status != null && status.is5xxServerError()) {
Throwable throwable = new CircuitBreakerStatusCodeException(status);
responseContext.setError(throwable);
// fallback if FallbackProvider is implemented.
String serviceId = ZuulFilterUtils.getServiceId(context);
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
if (fallbackProvider != null) {
ClientHttpResponse clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, throwable);
try {
// set status code
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
// set headers
HttpHeaders headers = clientHttpResponse.getHeaders();
for (String key : headers.keySet()) {
List<String> values = headers.get(key);
if (!CollectionUtils.isEmpty(values)) {
for (String value : values) {
context.addZuulResponseHeader(key, value);
}
}
}
// set body
context.getResponse().setCharacterEncoding("UTF-8");
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
}
catch (Exception e) {
LOGGER.error("error filter exception", e);
}
}
}
if (responseContext.getError() == null) {
polarisCircuitBreaker.onSuccess(responseContext);
}
else {
polarisCircuitBreaker.onError(responseContext);
}
}
return null;
}
public class CircuitBreakerStatusCodeException extends HttpStatusCodeException {
public CircuitBreakerStatusCodeException(HttpStatus statusCode) {
super(statusCode);
}
}
}

@ -0,0 +1,154 @@
/*
* 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.zuul;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ZuulFilterUtils;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_IS_IN_ROUTING_STATE;
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* Polaris circuit breaker implement in Zuul.
*
* @author Haotian Zhang
*/
public class PolarisCircuitBreakerZuulFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerZuulFilter.class);
private final CircuitBreakerFactory circuitBreakerFactory;
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
private final Environment environment;
public PolarisCircuitBreakerZuulFilter(
CircuitBreakerFactory circuitBreakerFactory,
PolarisZuulFallbackFactory polarisZuulFallbackFactory,
Environment environment) {
this.circuitBreakerFactory = circuitBreakerFactory;
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
this.environment = environment;
}
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* ServiceId is set after PreDecorationFilter.
*
* @return filter order
*/
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER + 2;
}
@Override
public boolean shouldFilter() {
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
return StringUtils.isEmpty(enabled) || enabled.equals("true");
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
String serviceId = ZuulFilterUtils.getServiceId(context);
String path = ZuulFilterUtils.getPath(context);
String circuitName = "".equals(path) ?
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId :
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId + "#" + path;
CircuitBreaker circuitBreaker = circuitBreakerFactory.create(circuitName);
if (circuitBreaker instanceof PolarisCircuitBreaker) {
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) circuitBreaker;
context.set(POLARIS_CIRCUIT_BREAKER, circuitBreaker);
try {
polarisCircuitBreaker.acquirePermission();
}
catch (CallAbortedException exception) {
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
ClientHttpResponse clientHttpResponse;
if (fallbackProvider != null) {
clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, exception);
}
else if (exception.getFallbackInfo() != null) {
clientHttpResponse = new PolarisCircuitBreakerHttpResponse(exception.getFallbackInfo());
}
else {
throw new IllegalStateException(exception);
}
try {
context.setSendZuulResponse(false);
// set status code
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
// set headers
HttpHeaders headers = clientHttpResponse.getHeaders();
for (String key : headers.keySet()) {
List<String> values = headers.get(key);
if (!CollectionUtils.isEmpty(values)) {
for (String value : values) {
context.addZuulResponseHeader(key, value);
}
}
}
// set body
context.getResponse().setCharacterEncoding("UTF-8");
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", exception.getMessage());
PolarisCircuitBreakerUtils.reportStatus(polarisCircuitBreaker.getConsumerAPI(), polarisCircuitBreaker.getConf(), exception);
}
catch (IOException e) {
LOGGER.error("Return circuit breaker fallback info failed: {}", e.getMessage());
throw new IllegalStateException(e);
}
}
context.set(POLARIS_PRE_ROUTE_TIME, Long.valueOf(System.currentTimeMillis()));
context.set(POLARIS_IS_IN_ROUTING_STATE, true);
}
return null;
}
}

@ -0,0 +1,57 @@
/*
* 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.zuul;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
/**
* Zuul fallback factory.
*
* @author Haotian Zhang
*/
public class PolarisZuulFallbackFactory {
private final Map<String, FallbackProvider> fallbackProviderCache;
private FallbackProvider defaultFallbackProvider = null;
public PolarisZuulFallbackFactory(Set<FallbackProvider> fallbackProviders) {
this.fallbackProviderCache = new HashMap<>();
for (FallbackProvider provider : fallbackProviders) {
String route = provider.getRoute();
if ("*".equals(route) || route == null) {
defaultFallbackProvider = provider;
}
else {
fallbackProviderCache.put(route, provider);
}
}
}
public FallbackProvider getFallbackProvider(String route) {
FallbackProvider provider = fallbackProviderCache.get(route);
if (provider == null) {
provider = defaultFallbackProvider;
}
return provider;
}
}

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

@ -0,0 +1,50 @@
/*
* 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 org.springframework.cloud.openfeign;
import com.tencent.cloud.polaris.circuitbreaker.config.ConditionalOnPolarisCircuitBreakerEnabled;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Auto configuration for PolarisFeignCircuitBreakerTargeter.
*
* @author Haotian Zhang
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Targeter.class})
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
@AutoConfigureBefore(FeignAutoConfiguration.class)
@ConditionalOnPolarisCircuitBreakerEnabled
public class PolarisFeignCircuitBreakerTargeterAutoConfiguration {
@Bean
@Primary
@ConditionalOnBean(CircuitBreakerFactory.class)
public Targeter polarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, PolarisCircuitBreakerNameResolver circuitBreakerNameResolver) {
return new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver);
}
}

@ -1,4 +1,8 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration,\
com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration,\
com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration,\
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration,\
org.springframework.cloud.openfeign.PolarisFeignCircuitBreakerTargeterAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerBootstrapConfiguration

@ -15,14 +15,21 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.config;
package com.tencent.cloud.polaris.circuitbreaker;
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
@ -32,21 +39,42 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Haotian Zhang
*/
public class PolarisCircuitBreakerAutoConfigurationTest {
public class CircuitBreakerPolarisAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
LoadBalancerAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
PolarisCircuitBreakerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
LoadBalancerAutoConfiguration.class,
ReactivePolarisCircuitBreakerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
@Test
public void testDefaultInitialization() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(
PolarisCircuitBreakerAutoConfiguration.CircuitBreakerConfigModifier.class);
assertThat(context).hasSingleBean(PolarisCircuitBreakerAutoConfiguration.class);
assertThat(context).hasSingleBean(CircuitBreakerFactory.class);
assertThat(context).hasSingleBean(CircuitBreakerConfigModifier.class);
assertThat(context).hasSingleBean(CircuitBreakAPI.class);
assertThat(context).hasSingleBean(PolarisCircuitBreakerNameResolver.class);
});
}
@Test
public void testReactiveInitialization() {
this.reactiveContextRunner.run(context -> {
assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class);
assertThat(context).hasSingleBean(ReactiveCircuitBreakerFactory.class);
assertThat(context).hasSingleBean(CircuitBreakerConfigModifier.class);
assertThat(context).hasSingleBean(CircuitBreakAPI.class);
});
}
}

@ -15,8 +15,12 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.config;
package com.tencent.cloud.polaris.circuitbreaker;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerBootstrapConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import org.junit.jupiter.api.Test;
@ -34,10 +38,10 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class PolarisCircuitBreakerBootstrapConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(PolarisContextAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
LoadBalancerAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
PolarisCircuitBreakerBootstrapConfiguration.class))
.withPropertyValues("spring.cloud.polaris.enabled=true")
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
@ -45,8 +49,9 @@ public class PolarisCircuitBreakerBootstrapConfigurationTest {
@Test
public void testDefaultInitialization() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(
PolarisCircuitBreakerAutoConfiguration.CircuitBreakerConfigModifier.class);
assertThat(context).hasSingleBean(PolarisCircuitBreakerAutoConfiguration.class);
assertThat(context).hasSingleBean(PolarisCircuitBreakerFeignClientAutoConfiguration.class);
assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class);
});
}
}

@ -0,0 +1,234 @@
/*
* 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.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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
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;
/**
* @author sean yu
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT,
classes = PolarisCircuitBreakerFeignIntegrationTest.TestConfig.class,
properties = {
"spring.cloud.gateway.enabled=false",
"feign.circuitbreaker.enabled=true",
"spring.cloud.polaris.namespace=default",
"spring.cloud.polaris.service=Test",
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
})
@DirtiesContext
public class PolarisCircuitBreakerFeignIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
private static NamingServer namingServer;
@Autowired
private EchoService echoService;
@Autowired
private FooService fooService;
@Autowired
private BarService barService;
@Autowired
private BazService bazService;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test
public void contextLoads() throws Exception {
assertThat(echoService).isNotNull();
assertThat(fooService).isNotNull();
}
@Test
public void testFeignClient() throws InvocationTargetException {
assertThat(echoService.echo("test")).isEqualTo("echo fallback");
Utils.sleepUninterrupted(2000);
assertThatThrownBy(() -> {
echoService.echo(null);
}).isInstanceOf(Exception.class);
assertThatThrownBy(() -> {
fooService.echo("test");
}).isInstanceOf(NoFallbackAvailableException.class);
Utils.sleepUninterrupted(2000);
assertThat(barService.bar()).isEqualTo("\"fallback from polaris server\"");
Utils.sleepUninterrupted(2000);
assertThat(bazService.baz()).isEqualTo("\"fallback from polaris server\"");
assertThat(fooService.toString()).isNotEqualTo(echoService.toString());
assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode());
assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE);
}
@FeignClient(value = TEST_SERVICE_NAME, contextId = "1", fallback = EchoServiceFallback.class)
public interface EchoService {
@RequestMapping(path = "echo/{str}")
String echo(@RequestParam("str") String param) throws InvocationTargetException;
}
@FeignClient(value = TEST_SERVICE_NAME, contextId = "2", fallbackFactory = CustomFallbackFactory.class)
public interface FooService {
@RequestMapping("echo/{str}")
String echo(@RequestParam("str") String param);
}
@FeignClient(value = TEST_SERVICE_NAME, contextId = "3")
public interface BarService {
@RequestMapping(path = "bar")
String bar();
}
public interface BazService {
@RequestMapping(path = "baz")
String baz();
}
@FeignClient(value = TEST_SERVICE_NAME, contextId = "4")
public interface BazClient extends BazService {
}
@Configuration
@EnableAutoConfiguration
@ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class})
@EnableFeignClients
public static class TestConfig {
@Bean
public EchoServiceFallback echoServiceFallback() {
return new EchoServiceFallback();
}
@Bean
public CustomFallbackFactory customFallbackFactory() {
return new CustomFallbackFactory();
}
@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);
}
}
public static class EchoServiceFallback implements EchoService {
@Override
public String echo(@RequestParam("str") String param) throws InvocationTargetException {
if (param == null) {
throw new InvocationTargetException(new Exception());
}
return "echo fallback";
}
}
public static class FooServiceFallback implements FooService {
@Override
public String echo(@RequestParam("str") String param) {
throw new NoFallbackAvailableException("fallback", new RuntimeException());
}
}
public static class CustomFallbackFactory
implements FallbackFactory<FooService> {
private final FooService fooService = new FooServiceFallback();
@Override
public FooService create(Throwable throwable) {
return fooService;
}
}
}

@ -0,0 +1,231 @@
/*
* 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.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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.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;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author sean yu
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"spring.cloud.gateway.enabled=true",
"spring.cloud.polaris.namespace=default",
"spring.cloud.polaris.service=Test",
"spring.main.web-application-type=reactive",
"httpbin=http://localhost:${wiremock.server.port}",
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
},
classes = PolarisCircuitBreakerGatewayIntegrationTest.TestApplication.class
)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test-gateway")
@AutoConfigureWebTestClient(timeout = "1000000")
public class PolarisCircuitBreakerGatewayIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
private static NamingServer namingServer;
@Autowired
private WebTestClient webClient;
@Autowired
private ApplicationContext applicationContext;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test
public void fallback() throws Exception {
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config();
applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString();
webClient
.get().uri("/err")
.header("Host", "www.circuitbreaker.com")
.exchange()
.expectStatus().isOk()
.expectBody()
.consumeWith(
response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
Utils.sleepUninterrupted(2000);
webClient
.get().uri("/err-skip-fallback")
.header("Host", "www.circuitbreaker-skip-fallback.com")
.exchange()
.expectStatus();
Utils.sleepUninterrupted(2000);
// this should be 200, but for some unknown reason, GitHub action run failed in windows, so we skip this check
webClient
.get().uri("/err-skip-fallback")
.header("Host", "www.circuitbreaker-skip-fallback.com")
.exchange()
.expectStatus();
Utils.sleepUninterrupted(2000);
webClient
.get().uri("/err-no-fallback")
.header("Host", "www.circuitbreaker-no-fallback.com")
.exchange()
.expectStatus();
Utils.sleepUninterrupted(2000);
webClient
.get().uri("/err-no-fallback")
.header("Host", "www.circuitbreaker-no-fallback.com")
.exchange()
.expectStatus();
Utils.sleepUninterrupted(2000);
webClient
.get().uri("/err-no-fallback")
.header("Host", "www.circuitbreaker-no-fallback.com")
.exchange()
.expectStatus();
}
@Configuration
@EnableAutoConfiguration
public static class TestApplication {
@Bean
public 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) {
Set<String> codeSets = new HashSet<>();
codeSets.add("4**");
codeSets.add("5**");
return builder.routes()
.route(p -> p
.host("*.circuitbreaker.com")
.filters(f -> f
.circuitBreaker(config -> config
.setStatusCodes(codeSets)
.setFallbackUri("forward:/fallback")
.setName(TEST_SERVICE_NAME)
))
.uri("http://httpbin.org:80"))
.route(p -> p
.host("*.circuitbreaker-skip-fallback.com")
.filters(f -> f
.circuitBreaker(config -> config
.setStatusCodes(Collections.singleton("5**"))
.setName(TEST_SERVICE_NAME)
))
.uri("http://httpbin.org:80"))
.route(p -> p
.host("*.circuitbreaker-no-fallback.com")
.filters(f -> f
.circuitBreaker(config -> config
.setName(TEST_SERVICE_NAME)
))
.uri("lb://" + TEST_SERVICE_NAME))
.build();
}
@RestController
static class Controller {
@RequestMapping("/fallback")
public Mono<String> fallback() {
return Mono.just("fallback");
}
}
}
}

@ -0,0 +1,309 @@
/*
* 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.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.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",
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
})
@DirtiesContext
public class PolarisCircuitBreakerIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
private static NamingServer namingServer;
@Autowired
@Qualifier("defaultRestTemplate")
private RestTemplate defaultRestTemplate;
@Autowired
@Qualifier("restTemplateFallbackFromPolaris")
private RestTemplate restTemplateFallbackFromPolaris;
@Autowired
@Qualifier("restTemplateFallbackFromCode")
private RestTemplate restTemplateFallbackFromCode;
@Autowired
@Qualifier("restTemplateFallbackFromCode2")
private RestTemplate restTemplateFallbackFromCode2;
@Autowired
@Qualifier("restTemplateFallbackFromCode3")
private RestTemplate restTemplateFallbackFromCode3;
@Autowired
@Qualifier("restTemplateFallbackFromCode4")
private RestTemplate restTemplateFallbackFromCode4;
@Autowired
private ApplicationContext applicationContext;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test
public void testRestTemplate() throws URISyntaxException {
MockRestServiceServer mockServer = MockRestServiceServer.createServer(defaultRestTemplate);
mockServer
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK).body("OK"));
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("OK");
mockServer.verify();
mockServer.reset();
HttpHeaders headers = new HttpHeaders();
mockServer
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.BAD_GATEWAY).headers(headers).body("BAD_GATEWAY"));
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("fallback");
mockServer.verify();
mockServer.reset();
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() {
return new RestTemplate();
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker
public RestTemplate restTemplateFallbackFromPolaris() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback.class)
public RestTemplate restTemplateFallbackFromCode() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback2.class)
public RestTemplate restTemplateFallbackFromCode2() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback3.class)
public RestTemplate restTemplateFallbackFromCode3() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallback = "fallback")
public RestTemplate restTemplateFallbackFromCode4() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
public CustomPolarisCircuitBreakerFallback customPolarisCircuitBreakerFallback() {
return new CustomPolarisCircuitBreakerFallback();
}
@Bean
public CustomPolarisCircuitBreakerFallback2 customPolarisCircuitBreakerFallback2() {
return new CustomPolarisCircuitBreakerFallback2();
}
@Bean
public CustomPolarisCircuitBreakerFallback3 customPolarisCircuitBreakerFallback3() {
return new CustomPolarisCircuitBreakerFallback3();
}
@Bean
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
try {
namingServer = NamingServer.startNamingServer(10081);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader()
.getResourceAsStream("circuitBreakerRule.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
.addRules(circuitBreakerRule).build();
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
}
@RestController
@RequestMapping("/example/service/b")
public class ServiceBController {
/**
* Get service information.
*
* @return service information
*/
@GetMapping("/info")
public String info() {
return "hello world ! I'm a service B1";
}
}
}
public static class CustomPolarisCircuitBreakerFallback implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200,
new HashMap<String, String>() {{
put("xxx", "xxx");
}},
"\"this is a fallback class\"");
}
}
public static class CustomPolarisCircuitBreakerFallback2 implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200,
"\"this is a fallback class\""
);
}
}
public static class CustomPolarisCircuitBreakerFallback3 implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200
);
}
}
}

@ -0,0 +1,152 @@
/*
* 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.polaris.api.config.Configuration;
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.factory.api.DiscoveryAPIFactory;
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.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link PolarisCircuitBreaker} and {@link ReactivePolarisCircuitBreaker} using real mock server.
*
* @author sean yu
*/
@ExtendWith(MockitoExtension.class)
public class PolarisCircuitBreakerMockServerTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static NamingServer namingServer;
@BeforeAll
public static void beforeAll() throws IOException {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
.thenReturn(NAMESPACE_TEST);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
.thenReturn(SERVICE_CIRCUIT_BREAKER);
try {
namingServer = NamingServer.startNamingServer(-1);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, SERVICE_CIRCUIT_BREAKER);
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);
}
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Test
public void testCircuitBreaker() {
Configuration configuration = TestUtils.configWithEnvAddress();
CircuitBreakAPI circuitBreakAPI = CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
ConsumerAPI consumerAPI = DiscoveryAPIFactory.createConsumerAPIByConfig(configuration);
PolarisCircuitBreakerFactory polarisCircuitBreakerFactory = new PolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
CircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
// trigger fallback for 5 times
List<String> resList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int finalI = i;
String res = cb.run(() -> {
if (finalI % 2 == 1) {
throw new IllegalArgumentException("invoke failed");
}
else {
return "invoke success";
}
}, t -> "fallback");
resList.add(res);
Utils.sleepUninterrupted(2000);
}
assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback"));
// always fallback
ReactivePolarisCircuitBreakerFactory reactivePolarisCircuitBreakerFactory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
ReactiveCircuitBreaker rcb = reactivePolarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
assertThat(Mono.just("foobar").transform(it -> rcb.run(it, t -> Mono.just("fallback")))
.block()).isEqualTo("fallback");
assertThat(Mono.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Mono.just("fallback")))
.block()).isEqualTo("fallback");
assertThat(Flux.just("foobar", "hello world").transform(it -> rcb.run(it, t -> Flux.just("fallback", "fallback")))
.collectList().block())
.isEqualTo(Arrays.asList("fallback", "fallback"));
assertThat(Flux.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Flux.just("fallback")))
.collectList().block())
.isEqualTo(Collections.singletonList("fallback"));
}
}

@ -0,0 +1,98 @@
/*
* 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 com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link PolarisCircuitBreaker}.
*
* @author sean yu
*/
@ExtendWith(MockitoExtension.class)
public class PolarisCircuitBreakerTest {
private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
LoadBalancerAutoConfiguration.class,
PolarisCircuitBreakerFeignClientAutoConfiguration.class,
PolarisCircuitBreakerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@BeforeAll
public static void beforeClass() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
.thenReturn(NAMESPACE_TEST);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
.thenReturn(SERVICE_CIRCUIT_BREAKER);
}
@AfterAll
public static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@Test
public void run() {
contextRunner.run(context -> {
PolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(PolarisCircuitBreakerFactory.class);
CircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration =
polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build();
polarisCircuitBreakerFactory.configureDefault(id -> configuration);
assertThat(cb.run(() -> "foobar")).isEqualTo("foobar");
assertThat((String) cb.run(() -> {
throw new RuntimeException("boom");
}, t -> "fallback")).isEqualTo("fallback");
});
}
}

@ -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;
import java.util.Arrays;
import java.util.Collections;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link ReactivePolarisCircuitBreaker}.
*
* @author sean yu
*/
@ExtendWith(MockitoExtension.class)
public class ReactivePolarisCircuitBreakerTest {
private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
RpcEnhancementAutoConfiguration.class,
LoadBalancerAutoConfiguration.class,
ReactivePolarisCircuitBreakerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@BeforeAll
public static void beforeClass() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
.thenReturn(NAMESPACE_TEST);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
.thenReturn(SERVICE_CIRCUIT_BREAKER);
}
@AfterAll
public static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@Test
public void run() {
this.reactiveContextRunner.run(context -> {
ReactivePolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(ReactivePolarisCircuitBreakerFactory.class);
ReactiveCircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration =
polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build();
polarisCircuitBreakerFactory.configureDefault(id -> configuration);
assertThat(Mono.just("foobar").transform(cb::run).block()).isEqualTo("foobar");
assertThat(Mono.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Mono.just("fallback")))
.block()).isEqualTo("fallback");
assertThat(Flux.just("foobar", "hello world").transform(cb::run).collectList().block())
.isEqualTo(Arrays.asList("foobar", "hello world"));
assertThat(Flux.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Flux.just("fallback")))
.collectList().block()).isEqualTo(Collections.singletonList("fallback"));
});
}
}

@ -0,0 +1,145 @@
/*
* 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.reporter;
import java.net.URI;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* ExceptionCircuitBreakerReporterTest.
*
* @author sean yu
*/
@ExtendWith(MockitoExtension.class)
public class ExceptionCircuitBreakerReporterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@Mock
private RpcEnhancementReporterProperties reporterProperties;
@Mock
private SDKContext sdkContext;
@InjectMocks
private ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter;
@Mock
private CircuitBreakAPI circuitBreakAPI;
@BeforeAll
static void beforeAll() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@BeforeEach
void setUp() {
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
}
@Test
public void testGetName() {
assertThat(exceptionCircuitBreakerReporter.getName()).isEqualTo(ExceptionCircuitBreakerReporter.class.getName());
}
@Test
public void testType() {
assertThat(exceptionCircuitBreakerReporter.getType()).isEqualTo(EnhancedPluginType.EXCEPTION);
}
@Test
public void testRun() throws Throwable {
EnhancedPluginContext context = mock(EnhancedPluginContext.class);
// test not report
exceptionCircuitBreakerReporter.run(context);
verify(context, times(0)).getRequest();
doReturn(true).when(reporterProperties).isEnabled();
EnhancedPluginContext pluginContext = new EnhancedPluginContext();
EnhancedRequestContext request = EnhancedRequestContext.builder()
.httpMethod(HttpMethod.GET)
.url(URI.create("http://0.0.0.0/"))
.httpHeaders(new HttpHeaders())
.build();
EnhancedResponseContext response = EnhancedResponseContext.builder()
.httpStatus(200)
.build();
DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
serviceInstance.setServiceId(SERVICE_PROVIDER);
pluginContext.setRequest(request);
pluginContext.setResponse(response);
pluginContext.setServiceInstance(serviceInstance);
pluginContext.setThrowable(new RuntimeException());
exceptionCircuitBreakerReporter.run(pluginContext);
exceptionCircuitBreakerReporter.getOrder();
exceptionCircuitBreakerReporter.getName();
exceptionCircuitBreakerReporter.getType();
}
@Test
public void testHandlerThrowable() {
// mock request
EnhancedRequestContext request = mock(EnhancedRequestContext.class);
// mock response
EnhancedResponseContext response = mock(EnhancedResponseContext.class);
EnhancedPluginContext context = new EnhancedPluginContext();
context.setRequest(request);
context.setResponse(response);
exceptionCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception."));
}
}

@ -0,0 +1,143 @@
/*
* 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.reporter;
import java.net.URI;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.http.HttpMethod;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* SuccessCircuitBreakerReporterTest.
*
* @author sean yu
*/
@ExtendWith(MockitoExtension.class)
public class SuccessCircuitBreakerReporterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@Mock
private SDKContext sdkContext;
@Mock
private RpcEnhancementReporterProperties reporterProperties;
@InjectMocks
private SuccessCircuitBreakerReporter successCircuitBreakerReporter;
@Mock
private CircuitBreakAPI circuitBreakAPI;
@BeforeAll
static void beforeAll() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@BeforeEach
void setUp() {
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
}
@Test
public void testGetName() {
assertThat(successCircuitBreakerReporter.getName()).isEqualTo(SuccessCircuitBreakerReporter.class.getName());
}
@Test
public void testType() {
assertThat(successCircuitBreakerReporter.getType()).isEqualTo(EnhancedPluginType.POST);
}
@Test
public void testRun() throws Throwable {
EnhancedPluginContext context = mock(EnhancedPluginContext.class);
// test not report
successCircuitBreakerReporter.run(context);
verify(context, times(0)).getRequest();
doReturn(true).when(reporterProperties).isEnabled();
EnhancedPluginContext pluginContext = new EnhancedPluginContext();
EnhancedRequestContext request = EnhancedRequestContext.builder()
.httpMethod(HttpMethod.GET)
.url(URI.create("http://0.0.0.0/"))
.build();
EnhancedResponseContext response = EnhancedResponseContext.builder()
.httpStatus(200)
.build();
DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
serviceInstance.setServiceId(SERVICE_PROVIDER);
pluginContext.setRequest(request);
pluginContext.setResponse(response);
pluginContext.setServiceInstance(serviceInstance);
successCircuitBreakerReporter.run(pluginContext);
successCircuitBreakerReporter.getOrder();
successCircuitBreakerReporter.getName();
successCircuitBreakerReporter.getType();
}
@Test
public void testHandlerThrowable() {
// mock request
EnhancedRequestContext request = mock(EnhancedRequestContext.class);
// mock response
EnhancedResponseContext response = mock(EnhancedResponseContext.class);
EnhancedPluginContext context = new EnhancedPluginContext();
context.setRequest(request);
context.setResponse(response);
successCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception."));
}
}

@ -0,0 +1,71 @@
/*
* 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.util;
import java.util.HashMap;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.api.rpc.ServiceCallResult;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
import static org.mockito.ArgumentMatchers.anyString;
public class PolarisCircuitBreakerUtilsTests {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@BeforeAll
static void beforeAll() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@BeforeEach
void setUp() {
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
}
@Test
public void testReportStatus() {
ConsumerAPI consumerAPI = Mockito.mock(ConsumerAPI.class);
Mockito.doNothing().when(consumerAPI).updateServiceCallResult(new ServiceCallResult());
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = new PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration();
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, new CallAbortedException("mock", new CircuitBreakerStatus.FallbackInfo(0, new HashMap<>(), "")));
}
}

@ -0,0 +1,36 @@
spring:
application:
name: GatewayScgService
cloud:
tencent:
plugin:
scg:
staining:
enabled: true
rule-staining:
enabled: true
router:
feature-env:
enabled: true
polaris:
address: grpc://127.0.0.1:8091
namespace: default
enabled: true
gateway:
routes:
- id: cb-test
uri: http://localhost:${server.port}/hello/1
predicates:
- Path=/cb-test/**
filters:
- name: CircuitBreaker
args:
statusCodes: 5**,4**,3**,2**,1**,500,400
fallbackUri: forward:/polaris-fallback
logging:
level:
root: info
com.tencent.polaris.discovery.client.flow.RegisterFlow: off
com.tencent.polaris.plugins.registry.memory.CacheObject: off
com.tencent.cloud.polaris.circuitbreaker: debug

@ -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\""
}
}
}

@ -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": "SERVICE",
"ruleMatcher": {
"source": {
"service": "*",
"namespace": "*"
},
"destination": {
"service": "*",
"namespace": "*",
"method": null
}
},
"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\""
}
}
}

@ -32,6 +32,18 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>connector-consul</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>connector-nacos</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tencent.polaris</groupId>
<artifactId>polaris-test-mock-discovery</artifactId>

@ -31,6 +31,8 @@ import com.tencent.polaris.factory.config.global.ServerConnectorConfigImpl;
import com.tencent.polaris.factory.config.provider.RegisterConfigImpl;
import com.tencent.polaris.plugins.connector.common.constant.ConsulConstant;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
@ -38,6 +40,9 @@ import org.springframework.util.CollectionUtils;
* @author lingxiao.wlx
*/
public class ConsulConfigModifier implements PolarisConfigModifier {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsulConfigModifier.class);
private static final String ID = "consul";
private final ConsulContextProperties consulContextProperties;
@ -49,6 +54,23 @@ public class ConsulConfigModifier implements PolarisConfigModifier {
@Override
public void modify(ConfigurationImpl configuration) {
if (consulContextProperties != null && consulContextProperties.isEnabled()) {
// Check if Consul client Available
boolean consulAvailable = false;
try {
consulAvailable = null != Class.forName("com.ecwid.consul.v1.ConsulClient");
}
catch (Throwable ignored) {
}
if (!consulAvailable) {
LOGGER.error("Please import \"connector-consul\" dependency when enabling consul service registration and discovery.\n"
+ "Add dependency configuration below to pom.xml:\n"
+ "<dependency>\n"
+ "\t<groupId>com.tencent.polaris</groupId>\n"
+ "\t<artifactId>connector-consul</artifactId>\n"
+ "</dependency>");
throw new RuntimeException("Dependency \"connector-consul\" not found.");
}
if (CollectionUtils.isEmpty(configuration.getGlobal().getServerConnectors())) {
configuration.getGlobal().setServerConnectors(new ArrayList<>());
}

@ -30,6 +30,8 @@ import com.tencent.polaris.factory.config.consumer.DiscoveryConfigImpl;
import com.tencent.polaris.factory.config.global.ServerConnectorConfigImpl;
import com.tencent.polaris.factory.config.provider.RegisterConfigImpl;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
@ -40,7 +42,6 @@ import org.springframework.util.CollectionUtils;
*/
public class NacosConfigModifier implements PolarisConfigModifier {
private static final String ID = "nacos";
/**
* nacos username.
*/
@ -53,7 +54,8 @@ public class NacosConfigModifier implements PolarisConfigModifier {
* nacos contextPath.
*/
public static final String CONTEXT_PATH = "contextPath";
private static final Logger LOGGER = LoggerFactory.getLogger(NacosConfigModifier.class);
private static final String ID = "nacos";
private final NacosContextProperties nacosContextProperties;
public NacosConfigModifier(NacosContextProperties nacosContextProperties) {
@ -65,6 +67,23 @@ public class NacosConfigModifier implements PolarisConfigModifier {
if (Objects.isNull(nacosContextProperties) || !nacosContextProperties.isEnabled()) {
return;
}
// Check if Nacos Available
boolean nacosAvailable = false;
try {
nacosAvailable = null != Class.forName("com.alibaba.nacos.api.naming.NamingService");
}
catch (Throwable ignored) {
}
if (!nacosAvailable) {
LOGGER.error("Please import \"connector-nacos\" dependency when enabling nacos service registration and discovery.\n"
+ "Add dependency configuration below to pom.xml:\n"
+ "<dependency>\n"
+ "\t<groupId>com.tencent.polaris</groupId>\n"
+ "\t<artifactId>connector-nacos</artifactId>\n"
+ "</dependency>");
throw new RuntimeException("Dependency \"connector-nacos\" not found.");
}
if (CollectionUtils.isEmpty(configuration.getGlobal().getServerConnectors())) {
configuration.getGlobal().setServerConnectors(new ArrayList<>());
}

@ -41,6 +41,7 @@ import com.tencent.polaris.api.rpc.InstanceRegisterRequest;
import com.tencent.polaris.api.rpc.InstanceRegisterResponse;
import com.tencent.polaris.api.rpc.InstancesResponse;
import com.tencent.polaris.client.util.NamedThreadFactory;
import com.tencent.polaris.factory.config.provider.ServiceConfigImpl;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -144,6 +145,11 @@ public class PolarisServiceRegistry implements ServiceRegistry<PolarisRegistrati
LOGGER.warn("Stat server started error, ", e);
}
}
ServiceConfigImpl serviceConfig = (ServiceConfigImpl) polarisDiscoveryHandler.getSdkContext().getConfig()
.getProvider().getService();
serviceConfig.setNamespace(polarisDiscoveryProperties.getNamespace());
serviceConfig.setName(serviceId);
}
catch (Exception e) {
LOGGER.error("polaris registry, {} register failed...{},", registration.getServiceId(), registration, e);
@ -197,7 +203,7 @@ public class PolarisServiceRegistry implements ServiceRegistry<PolarisRegistrati
String serviceName = registration.getServiceId();
InstancesResponse instancesResponse = polarisDiscoveryHandler.getInstances(serviceName);
Instance[] instances = instancesResponse.getInstances();
if (null == instances || instances.length == 0) {
if (null == instances) {
return null;
}
for (Instance instance : instances) {

@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import static com.tencent.polaris.test.common.Consts.PORT;
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
import static org.assertj.core.api.Assertions.assertThat;
@ -44,6 +45,9 @@ public class PolarisRibbonServerListConfigurationTest {
this.applicationContextRunner
.withConfiguration(AutoConfigurations.of(
TestApplication.class, PolarisRibbonServerListConfiguration.class))
.withPropertyValues("spring.application.name=" + SERVICE_PROVIDER)
.withPropertyValues("server.port=" + PORT)
.withPropertyValues("spring.cloud.polaris.address=grpc://127.0.0.1:10081")
.run(context -> {
assertThat(context).hasSingleBean(PolarisRibbonServerListConfiguration.class);
assertThat(context).hasSingleBean(ServerList.class);

@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Haotian Zhang
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = OkHttpUtilTest.TestApplication.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = OkHttpUtilTest.TestApplication.class, properties = {"spring.application.name=test", "spring.cloud.polaris.discovery.register=false"})
public class OkHttpUtilTest {
@LocalServerPort

@ -17,7 +17,7 @@
<!-- Spring Cloud Tencent dependencies start -->
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-context</artifactId>
<artifactId>spring-cloud-tencent-rpc-enhancement</artifactId>
</dependency>
<!-- Spring Cloud Tencent dependencies end -->

@ -34,8 +34,9 @@ import org.springframework.util.CollectionUtils;
/**
* resolve labels from rate limit rule.
*
* @author lepdou 2022-05-13
*@author lepdou 2022-05-13
*/
@Deprecated
public class RateLimitRuleLabelResolver {
private final ServiceRuleManager serviceRuleManager;

@ -20,10 +20,10 @@ package com.tencent.cloud.polaris.ratelimit.config;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckReactiveFilter;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
@ -31,6 +31,7 @@ import com.tencent.polaris.client.api.SDKContext;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.factory.LimitAPIFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
@ -39,6 +40,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.FILTER_ORDER;
import static com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter.QUOTA_FILTER_BEAN_NAME;
import static javax.servlet.DispatcherType.ASYNC;
import static javax.servlet.DispatcherType.ERROR;
@ -62,11 +64,6 @@ public class PolarisRateLimitAutoConfiguration {
return LimitAPIFactory.createLimitAPIByContext(polarisContext);
}
@Bean
public RateLimitRuleLabelResolver rateLimitRuleLabelService(ServiceRuleManager serviceRuleManager) {
return new RateLimitRuleLabelResolver(serviceRuleManager);
}
/**
* Create when web application type is SERVLET.
*/
@ -74,15 +71,19 @@ public class PolarisRateLimitAutoConfiguration {
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
protected static class QuotaCheckFilterConfig {
@Bean
public RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver(ServiceRuleManager serviceRuleManager,
@Autowired(required = false) PolarisRateLimiterLabelServletResolver labelResolver) {
return new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolver);
}
@Bean
@ConditionalOnMissingBean
public QuotaCheckServletFilter quotaCheckFilter(LimitAPI limitAPI,
@Nullable PolarisRateLimiterLabelServletResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
return new QuotaCheckServletFilter(limitAPI, labelResolver,
polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver,
@Autowired(required = false) PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
return new QuotaCheckServletFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentResolver, polarisRateLimiterLimitedFallback);
}
@Bean
@ -92,9 +93,10 @@ public class PolarisRateLimitAutoConfiguration {
quotaCheckServletFilter);
registrationBean.setDispatcherTypes(ASYNC, ERROR, FORWARD, INCLUDE, REQUEST);
registrationBean.setName(QUOTA_FILTER_BEAN_NAME);
registrationBean.setOrder(RateLimitConstant.FILTER_ORDER);
registrationBean.setOrder(FILTER_ORDER);
return registrationBean;
}
}
/**
@ -104,14 +106,18 @@ public class PolarisRateLimitAutoConfiguration {
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
protected static class MetadataReactiveFilterConfig {
@Bean
public RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver(ServiceRuleManager serviceRuleManager,
@Autowired(required = false) PolarisRateLimiterLabelReactiveResolver labelResolver) {
return new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolver);
}
@Bean
public QuotaCheckReactiveFilter quotaCheckReactiveFilter(LimitAPI limitAPI,
@Nullable PolarisRateLimiterLabelReactiveResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver,
RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
return new QuotaCheckReactiveFilter(limitAPI, labelResolver,
polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
return new QuotaCheckReactiveFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentResolver, polarisRateLimiterLimitedFallback);
}
}
}

@ -20,27 +20,22 @@ package com.tencent.cloud.polaris.ratelimit.filter;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.PostConstruct;
import com.google.common.collect.Maps;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils;
import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
@ -54,8 +49,6 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LABEL_METHOD;
/**
* Reactive filter to check quota.
*
@ -67,11 +60,9 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
private final LimitAPI limitAPI;
private final PolarisRateLimiterLabelReactiveResolver labelResolver;
private final PolarisRateLimitProperties polarisRateLimitProperties;
private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver;
private final RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver;
private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
@ -79,14 +70,12 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
private String rejectTips;
public QuotaCheckReactiveFilter(LimitAPI limitAPI,
PolarisRateLimiterLabelReactiveResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver,
RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
this.limitAPI = limitAPI;
this.labelResolver = labelResolver;
this.polarisRateLimitProperties = polarisRateLimitProperties;
this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver;
this.rateLimitRuleArgumentResolver = rateLimitRuleArgumentResolver;
this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback;
}
@ -105,12 +94,12 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
String localNamespace = MetadataContext.LOCAL_NAMESPACE;
String localService = MetadataContext.LOCAL_SERVICE;
Map<String, String> labels = getRequestLabels(exchange, localNamespace, localService);
Set<Argument> arguments = rateLimitRuleArgumentResolver.getArguments(exchange, localNamespace, localService);
long waitMs = -1;
try {
String path = exchange.getRequest().getURI().getPath();
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI,
localNamespace, localService, 1, labels, path);
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(
limitAPI, localNamespace, localService, 1, arguments, path);
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
ServerHttpResponse response = exchange.getResponse();
@ -119,7 +108,8 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
response.setRawStatusCode(polarisRateLimiterLimitedFallback.rejectHttpCode());
response.getHeaders().setContentType(polarisRateLimiterLimitedFallback.mediaType());
dataBuffer = response.bufferFactory().allocateBuffer()
.write(polarisRateLimiterLimitedFallback.rejectTips().getBytes(polarisRateLimiterLimitedFallback.charset()));
.write(polarisRateLimiterLimitedFallback.rejectTips()
.getBytes(polarisRateLimiterLimitedFallback.charset()));
}
else {
response.setRawStatusCode(polarisRateLimitProperties.getRejectHttpCode());
@ -149,40 +139,4 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
}
}
private Map<String, String> getRequestLabels(ServerWebExchange exchange, String localNamespace, String localService) {
Map<String, String> labels = new HashMap<>();
// add build in labels
String path = exchange.getRequest().getURI().getPath();
if (StringUtils.isNotBlank(path)) {
labels.put(LABEL_METHOD, path);
}
// add rule expression labels
Map<String, String> expressionLabels = getRuleExpressionLabels(exchange, localNamespace, localService);
labels.putAll(expressionLabels);
// add custom labels
Map<String, String> customResolvedLabels = getCustomResolvedLabels(exchange);
labels.putAll(customResolvedLabels);
return labels;
}
private Map<String, String> getCustomResolvedLabels(ServerWebExchange exchange) {
if (labelResolver != null) {
try {
return labelResolver.resolve(exchange);
}
catch (Throwable e) {
LOGGER.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
}
}
return Maps.newHashMap();
}
private Map<String, String> getRuleExpressionLabels(ServerWebExchange exchange, String namespace, String service) {
Set<String> expressionLabels = rateLimitRuleLabelResolver.getExpressionLabelKeys(namespace, service);
return SpringWebExpressionLabelUtils.resolve(exchange, expressionLabels);
}
}

@ -19,9 +19,6 @@
package com.tencent.cloud.polaris.ratelimit.filter;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -31,19 +28,19 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.tencent.cloud.common.constant.HeaderConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.expresstion.ServletExpressionLabelUtils;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils;
import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils;
import com.tencent.polaris.api.pojo.RetStatus;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -53,8 +50,6 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.filter.OncePerRequestFilter;
import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LABEL_METHOD;
/**
* Servlet filter to check quota.
*
@ -70,25 +65,21 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class);
private final LimitAPI limitAPI;
private final PolarisRateLimiterLabelServletResolver labelResolver;
private final PolarisRateLimitProperties polarisRateLimitProperties;
private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver;
private final RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver;
private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
private String rejectTips;
public QuotaCheckServletFilter(LimitAPI limitAPI,
PolarisRateLimiterLabelServletResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver,
RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
this.limitAPI = limitAPI;
this.labelResolver = labelResolver;
this.polarisRateLimitProperties = polarisRateLimitProperties;
this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver;
this.rateLimitRuleArgumentResolver = rateLimitRuleArgumentResolver;
this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback;
}
@ -104,11 +95,11 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
String localNamespace = MetadataContext.LOCAL_NAMESPACE;
String localService = MetadataContext.LOCAL_SERVICE;
Map<String, String> labels = getRequestLabels(request, localNamespace, localService);
Set<Argument> arguments = rateLimitRuleArgumentResolver.getArguments(request, localNamespace, localService);
try {
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI,
localNamespace, localService, 1, labels, request.getRequestURI());
localNamespace, localService, 1, arguments, request.getRequestURI());
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
if (!Objects.isNull(polarisRateLimiterLimitedFallback)) {
@ -122,6 +113,11 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(rejectTips);
}
response.addHeader(HeaderConstant.INTERNAL_CALLEE_RET_STATUS, RetStatus.RetFlowControl.getDesc());
if (Objects.nonNull(quotaResponse.getActiveRule())) {
response.addHeader(HeaderConstant.INTERNAL_ACTIVE_RULE_NAME, quotaResponse.getActiveRule().getName()
.getValue());
}
return;
}
// Unirate
@ -140,40 +136,4 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
private Map<String, String> getRequestLabels(HttpServletRequest request, String localNamespace, String localService) {
Map<String, String> labels = new HashMap<>();
// add build in labels
String path = request.getRequestURI();
if (StringUtils.isNotBlank(path)) {
labels.put(LABEL_METHOD, path);
}
// add rule expression labels
Map<String, String> expressionLabels = getRuleExpressionLabels(request, localNamespace, localService);
labels.putAll(expressionLabels);
// add custom resolved labels
Map<String, String> customLabels = getCustomResolvedLabels(request);
labels.putAll(customLabels);
return labels;
}
private Map<String, String> getCustomResolvedLabels(HttpServletRequest request) {
if (labelResolver != null) {
try {
return labelResolver.resolve(request);
}
catch (Throwable e) {
LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
}
}
return Collections.emptyMap();
}
private Map<String, String> getRuleExpressionLabels(HttpServletRequest request, String namespace, String service) {
Set<String> expressionLabels = rateLimitRuleLabelResolver.getExpressionLabelKeys(namespace, service);
return ServletExpressionLabelUtils.resolve(request, expressionLabels);
}
}

@ -0,0 +1,128 @@
/*
* 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.ratelimit.resolver;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
/**
* resolve arguments from rate limit rule for Reactive.
*
* @author seansyyu 2023-03-09
*/
public class RateLimitRuleArgumentReactiveResolver {
private static final Logger LOG = LoggerFactory.getLogger(RateLimitRuleArgumentReactiveResolver.class);
private final ServiceRuleManager serviceRuleManager;
private final PolarisRateLimiterLabelReactiveResolver labelResolver;
public RateLimitRuleArgumentReactiveResolver(ServiceRuleManager serviceRuleManager, PolarisRateLimiterLabelReactiveResolver labelResolver) {
this.serviceRuleManager = serviceRuleManager;
this.labelResolver = labelResolver;
}
public Set<Argument> getArguments(ServerWebExchange request, String namespace, String service) {
RateLimitProto.RateLimit rateLimitRule = serviceRuleManager.getServiceRateLimitRule(namespace, service);
if (rateLimitRule == null) {
return Collections.emptySet();
}
List<RateLimitProto.Rule> rules = rateLimitRule.getRulesList();
if (CollectionUtils.isEmpty(rules)) {
return Collections.emptySet();
}
return rules.stream()
.flatMap(rule -> rule.getArgumentsList().stream())
.map(matchArgument -> {
String matchKey = matchArgument.getKey();
Argument argument = null;
switch (matchArgument.getType()) {
case CUSTOM:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildCustom(matchKey, Optional.ofNullable(getCustomResolvedLabels(request).get(matchKey))
.orElse(StringUtils.EMPTY));
break;
case METHOD:
argument = Argument.buildMethod(request.getRequest().getMethodValue());
break;
case HEADER:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildHeader(matchKey, Optional.ofNullable(request.getRequest().getHeaders()
.getFirst(matchKey)).orElse(StringUtils.EMPTY));
break;
case QUERY:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildQuery(matchKey, Optional.ofNullable(request.getRequest().getQueryParams()
.getFirst(matchKey)).orElse(StringUtils.EMPTY));
break;
case CALLER_SERVICE:
String sourceServiceNamespace = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, true)
.orElse(StringUtils.EMPTY);
String sourceServiceName = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAME, true)
.orElse(StringUtils.EMPTY);
if (!StringUtils.isEmpty(sourceServiceNamespace) && !StringUtils.isEmpty(sourceServiceName)) {
argument = Argument.buildCallerService(sourceServiceNamespace, sourceServiceName);
}
break;
case CALLER_IP:
InetSocketAddress remoteAddress = request.getRequest().getRemoteAddress();
argument = Argument.buildCallerIP(remoteAddress != null ? remoteAddress.getAddress()
.getHostAddress() : StringUtils.EMPTY);
break;
default:
break;
}
return argument;
}).filter(Objects::nonNull).collect(Collectors.toSet());
}
private Map<String, String> getCustomResolvedLabels(ServerWebExchange request) {
if (labelResolver != null) {
try {
return labelResolver.resolve(request);
}
catch (Throwable e) {
LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
}
}
return Collections.emptyMap();
}
}

@ -0,0 +1,123 @@
/*
* 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.ratelimit.resolver;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
/**
* resolve arguments from rate limit rule for Servlet.
*
* @author seansyyu 2023-03-09
*/
public class RateLimitRuleArgumentServletResolver {
private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class);
private final ServiceRuleManager serviceRuleManager;
private final PolarisRateLimiterLabelServletResolver labelResolver;
public RateLimitRuleArgumentServletResolver(ServiceRuleManager serviceRuleManager, PolarisRateLimiterLabelServletResolver labelResolver) {
this.serviceRuleManager = serviceRuleManager;
this.labelResolver = labelResolver;
}
public Set<Argument> getArguments(HttpServletRequest request, String namespace, String service) {
RateLimitProto.RateLimit rateLimitRule = serviceRuleManager.getServiceRateLimitRule(namespace, service);
if (rateLimitRule == null) {
return Collections.emptySet();
}
List<RateLimitProto.Rule> rules = rateLimitRule.getRulesList();
if (CollectionUtils.isEmpty(rules)) {
return Collections.emptySet();
}
return rules.stream()
.flatMap(rule -> rule.getArgumentsList().stream())
.map(matchArgument -> {
String matchKey = matchArgument.getKey();
Argument argument = null;
switch (matchArgument.getType()) {
case CUSTOM:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildCustom(matchKey, Optional.ofNullable(getCustomResolvedLabels(request).get(matchKey)).orElse(StringUtils.EMPTY));
break;
case METHOD:
argument = Argument.buildMethod(request.getMethod());
break;
case HEADER:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildHeader(matchKey, Optional.ofNullable(request.getHeader(matchKey)).orElse(StringUtils.EMPTY));
break;
case QUERY:
argument = StringUtils.isBlank(matchKey) ? null :
Argument.buildQuery(matchKey, Optional.ofNullable(request.getParameter(matchKey)).orElse(StringUtils.EMPTY));
break;
case CALLER_SERVICE:
String sourceServiceNamespace = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, true).orElse(StringUtils.EMPTY);
String sourceServiceName = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAME, true).orElse(StringUtils.EMPTY);
if (!StringUtils.isEmpty(sourceServiceNamespace) && !StringUtils.isEmpty(sourceServiceName)) {
argument = Argument.buildCallerService(sourceServiceNamespace, sourceServiceName);
}
break;
case CALLER_IP:
argument = Argument.buildCallerIP(Optional.ofNullable(request.getRemoteAddr()).orElse(StringUtils.EMPTY));
break;
default:
break;
}
return argument;
}).filter(Objects::nonNull).collect(Collectors.toSet());
}
private Map<String, String> getCustomResolvedLabels(HttpServletRequest request) {
if (labelResolver != null) {
try {
return labelResolver.resolve(request);
}
catch (Throwable e) {
LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
}
}
return Collections.emptyMap();
}
}

@ -18,9 +18,11 @@
package com.tencent.cloud.polaris.ratelimit.utils;
import java.util.Map;
import java.util.Set;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import org.slf4j.Logger;
@ -38,8 +40,9 @@ public final class QuotaCheckUtils {
private QuotaCheckUtils() {
}
public static QuotaResponse getQuota(LimitAPI limitAPI, String namespace,
String service, int count, Map<String, String> labels, String method) {
@Deprecated
public static QuotaResponse getQuota(LimitAPI limitAPI, String namespace, String service, int count,
Map<String, String> labels, String method) {
// build quota request
QuotaRequest quotaRequest = new QuotaRequest();
quotaRequest.setNamespace(namespace);
@ -52,10 +55,27 @@ public final class QuotaCheckUtils {
return limitAPI.getQuota(quotaRequest);
}
catch (Throwable throwable) {
LOG.error("fail to invoke getQuota of LimitAPI with QuotaRequest[{}].",
quotaRequest, throwable);
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0,
"get quota failed"));
LOG.error("fail to invoke getQuota of LimitAPI with QuotaRequest[{}].", quotaRequest, throwable);
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "get quota failed"));
}
}
public static QuotaResponse getQuota(LimitAPI limitAPI, String namespace, String service, int count,
Set<Argument> arguments, String method) {
// build quota request
QuotaRequest quotaRequest = new QuotaRequest();
quotaRequest.setNamespace(namespace);
quotaRequest.setService(service);
quotaRequest.setCount(count);
quotaRequest.setArguments(arguments);
quotaRequest.setMethod(method);
try {
return limitAPI.getQuota(quotaRequest);
}
catch (Throwable throwable) {
LOG.error("fail to invoke getQuota of LimitAPI with QuotaRequest[{}].", quotaRequest, throwable);
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "get quota failed"));
}
}
}

@ -18,9 +18,10 @@
package com.tencent.cloud.polaris.ratelimit.config;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckReactiveFilter;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentServletResolver;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import org.junit.jupiter.api.Test;
@ -43,7 +44,8 @@ public class PolarisRateLimitAutoConfigurationTest {
private final WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner();
private final ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner = new ReactiveWebApplicationContextRunner();
private final ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner =
new ReactiveWebApplicationContextRunner();
@Test
public void testNoWebApplication() {
@ -54,12 +56,12 @@ public class PolarisRateLimitAutoConfigurationTest {
PolarisRateLimitAutoConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class);
assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class);
assertThat(context).doesNotHaveBean(RateLimitRuleArgumentServletResolver.class);
assertThat(context).doesNotHaveBean(RateLimitRuleArgumentReactiveResolver.class);
assertThat(context).doesNotHaveBean(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class);
assertThat(context).doesNotHaveBean(FilterRegistrationBean.class);
assertThat(context).doesNotHaveBean(
PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).doesNotHaveBean(PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckReactiveFilter.class);
});
}
@ -67,37 +69,35 @@ public class PolarisRateLimitAutoConfigurationTest {
@Test
public void testServletWebApplication() {
this.webApplicationContextRunner
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(PolarisContextAutoConfiguration.class,
PolarisRateLimitProperties.class,
PolarisRateLimitAutoConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class);
assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class);
assertThat(context).hasSingleBean(RateLimitRuleArgumentServletResolver.class);
assertThat(context).hasSingleBean(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).hasSingleBean(QuotaCheckServletFilter.class);
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
assertThat(context).doesNotHaveBean(
PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).doesNotHaveBean(PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckReactiveFilter.class);
assertThat(context).doesNotHaveBean(RateLimitRuleArgumentReactiveResolver.class);
});
}
@Test
public void testReactiveWebApplication() {
this.reactiveWebApplicationContextRunner
.withConfiguration(AutoConfigurations.of(
PolarisContextAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(PolarisContextAutoConfiguration.class,
PolarisRateLimitProperties.class,
PolarisRateLimitAutoConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class);
assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class);
assertThat(context).doesNotHaveBean(RateLimitRuleArgumentServletResolver.class);
assertThat(context).hasSingleBean(RateLimitRuleArgumentReactiveResolver.class);
assertThat(context).doesNotHaveBean(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class);
assertThat(context).doesNotHaveBean(FilterRegistrationBean.class);
assertThat(context).hasSingleBean(
PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).hasSingleBean(PolarisRateLimitAutoConfiguration.MetadataReactiveFilterConfig.class);
assertThat(context).hasSingleBean(QuotaCheckReactiveFilter.class);
});
}

@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Haotian Zhang
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = PolarisRateLimitPropertiesTest.TestApplication.class)
@SpringBootTest(classes = PolarisRateLimitPropertiesTest.TestApplication.class, properties = "spring.application.name=test")
@ActiveProfiles("test")
public class PolarisRateLimitPropertiesTest {

@ -17,32 +17,32 @@
package com.tencent.cloud.polaris.ratelimit.filter;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ -60,10 +60,8 @@ import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
@ -76,36 +74,15 @@ import static org.mockito.Mockito.when;
@SpringBootTest(classes = QuotaCheckReactiveFilterTest.TestApplication.class,
properties = {"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"})
public class QuotaCheckReactiveFilterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<SpringWebExpressionLabelUtils> expressionLabelUtilsMockedStatic;
private final PolarisRateLimiterLabelReactiveResolver labelResolver =
exchange -> Collections.singletonMap("ReactiveResolver", "ReactiveResolver");
exchange -> Collections.singletonMap("xxx", "xxx");
private QuotaCheckReactiveFilter quotaCheckReactiveFilter;
private QuotaCheckReactiveFilter quotaCheckWithRateLimiterLimitedFallbackReactiveFilter;
private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
@BeforeAll
static void beforeAll() {
expressionLabelUtilsMockedStatic = mockStatic(SpringWebExpressionLabelUtils.class);
when(SpringWebExpressionLabelUtils.resolve(any(ServerWebExchange.class), anySet()))
.thenReturn(Collections.singletonMap("RuleLabelResolver", "RuleLabelResolver"));
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
expressionLabelUtilsMockedStatic.close();
}
@BeforeEach
void setUp() {
void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST";
polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback();
LimitAPI limitAPI = mock(LimitAPI.class);
when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> {
@ -117,10 +94,12 @@ public class QuotaCheckReactiveFilterTest {
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 1000, "QuotaResultOk"));
}
else if (serviceName.equals("TestApp3")) {
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited"));
QuotaResponse response = new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited"));
response.setActiveRule(RateLimitProto.Rule.newBuilder().build());
return response;
}
else {
return new QuotaResponse(new QuotaResult(null, 0, null));
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, null));
}
});
@ -128,14 +107,26 @@ public class QuotaCheckReactiveFilterTest {
polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips提示消息");
polarisRateLimitProperties.setRejectHttpCode(419);
RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class);
when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString()))
.thenReturn(Collections.emptySet());
PolarisRateLimitProperties polarisRateLimitWithHtmlRejectTipsProperties = new PolarisRateLimitProperties();
polarisRateLimitWithHtmlRejectTipsProperties.setRejectRequestTips("<h1>RejectRequestTips提示消息</h1>");
polarisRateLimitWithHtmlRejectTipsProperties.setRejectHttpCode(419);
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
this.quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(
limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null);
this.quotaCheckWithRateLimiterLimitedFallbackReactiveFilter = new QuotaCheckReactiveFilter(
limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader()
.getResourceAsStream("ratelimit.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolver);
this.quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentReactiveResolver, null);
this.polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback();
this.quotaCheckWithRateLimiterLimitedFallbackReactiveFilter = new QuotaCheckReactiveFilter(limitAPI, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleArgumentReactiveResolver, polarisRateLimiterLimitedFallback);
}
@Test
@ -156,42 +147,6 @@ public class QuotaCheckReactiveFilterTest {
}
}
@Test
public void testGetRuleExpressionLabels() {
try {
Method getCustomResolvedLabels =
QuotaCheckReactiveFilter.class.getDeclaredMethod("getCustomResolvedLabels", ServerWebExchange.class);
getCustomResolvedLabels.setAccessible(true);
// Mock request
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
// labelResolver != null
Map<String, String> result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get("ReactiveResolver")).isEqualTo("ReactiveResolver");
// throw exception
PolarisRateLimiterLabelReactiveResolver exceptionLabelResolver = exchange1 -> {
throw new RuntimeException("Mock exception.");
};
quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, exceptionLabelResolver, null, null, null);
result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange);
assertThat(result.size()).isEqualTo(0);
// labelResolver == null
quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, null, null, null, null);
result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange);
assertThat(result.size()).isEqualTo(0);
getCustomResolvedLabels.setAccessible(false);
}
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
fail("Exception encountered.", e);
}
}
@Test
public void testFilter() {
// Create mock WebFilterChain
@ -199,19 +154,20 @@ public class QuotaCheckReactiveFilterTest {
// Mock request
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.init();
// Pass
MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckReactiveFilter.filter(exchange, webFilterChain);
ServerWebExchange testApp1Exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.filter(testApp1Exchange, webFilterChain);
// Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2";
ServerWebExchange testApp2Exchange = MockServerWebExchange.from(request);
long startTimestamp = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(1);
quotaCheckReactiveFilter.filter(exchange, webFilterChain).subscribe(e -> {
quotaCheckReactiveFilter.filter(testApp2Exchange, webFilterChain).subscribe(e -> {
}, t -> {
}, countDownLatch::countDown);
try {
@ -224,14 +180,16 @@ public class QuotaCheckReactiveFilterTest {
// Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3";
quotaCheckReactiveFilter.filter(exchange, webFilterChain);
ServerHttpResponse response = exchange.getResponse();
ServerWebExchange testApp3Exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.filter(testApp3Exchange, webFilterChain);
ServerHttpResponse response = testApp3Exchange.getResponse();
assertThat(response.getRawStatusCode()).isEqualTo(419);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE);
// Exception
MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckReactiveFilter.filter(exchange, webFilterChain);
ServerWebExchange testApp4Exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.filter(testApp4Exchange, webFilterChain);
}
@Test

@ -17,53 +17,47 @@
package com.tencent.cloud.polaris.ratelimit.filter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils;
import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.resolver.RateLimitRuleArgumentServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
@ -71,41 +65,23 @@ import static org.mockito.Mockito.when;
*
* @author Haotian Zhang, cheese8
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@SpringBootTest(classes = QuotaCheckServletFilterTest.TestApplication.class, properties = {
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = QuotaCheckServletFilterTest.TestApplication.class,
properties = {
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
})
})
public class QuotaCheckServletFilterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<SpringWebExpressionLabelUtils> expressionLabelUtilsMockedStatic;
private PolarisRateLimiterLabelServletResolver labelResolver =
exchange -> Collections.singletonMap("ServletResolver", "ServletResolver");
private final PolarisRateLimiterLabelServletResolver labelResolver =
exchange -> Collections.singletonMap("xxx", "xxx");
private QuotaCheckServletFilter quotaCheckServletFilter;
private QuotaCheckServletFilter quotaCheckWithHtmlRejectTipsServletFilter;
private QuotaCheckServletFilter quotaCheckWithRateLimiterLimitedFallbackFilter;
private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
@BeforeAll
static void beforeAll() {
expressionLabelUtilsMockedStatic = mockStatic(SpringWebExpressionLabelUtils.class);
when(SpringWebExpressionLabelUtils.resolve(any(ServerWebExchange.class), anySet()))
.thenReturn(Collections.singletonMap("RuleLabelResolver", "RuleLabelResolver"));
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
expressionLabelUtilsMockedStatic.close();
}
@BeforeEach
void setUp() {
void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST";
LimitAPI limitAPI = mock(LimitAPI.class);
@ -118,7 +94,9 @@ public class QuotaCheckServletFilterTest {
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 1000, "QuotaResultOk"));
}
else if (serviceName.equals("TestApp3")) {
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited"));
QuotaResponse response = new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited"));
response.setActiveRule(RateLimitProto.Rule.newBuilder().build());
return response;
}
else {
return new QuotaResponse(new QuotaResult(null, 0, null));
@ -133,15 +111,23 @@ public class QuotaCheckServletFilterTest {
polarisRateLimitWithHtmlRejectTipsProperties.setRejectRequestTips("<h1>RejectRequestTips提示消息</h1>");
polarisRateLimitWithHtmlRejectTipsProperties.setRejectHttpCode(419);
RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class);
when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())).thenReturn(Collections.emptySet());
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader()
.getResourceAsStream("ratelimit.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null);
this.quotaCheckWithHtmlRejectTipsServletFilter = new QuotaCheckServletFilter(
limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, null);
polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback();
this.quotaCheckWithRateLimiterLimitedFallbackFilter = new QuotaCheckServletFilter(
limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver = new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolver);
this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentServletResolver, null);
this.quotaCheckWithHtmlRejectTipsServletFilter = new QuotaCheckServletFilter(limitAPI, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleArgumentServletResolver, null);
this.polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback();
this.quotaCheckWithRateLimiterLimitedFallbackFilter = new QuotaCheckServletFilter(limitAPI, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleArgumentServletResolver, polarisRateLimiterLimitedFallback);
}
@Test
@ -167,40 +153,6 @@ public class QuotaCheckServletFilterTest {
quotaCheckWithRateLimiterLimitedFallbackFilter.init();
}
@Test
public void testGetRuleExpressionLabels() {
try {
Method getCustomResolvedLabels = QuotaCheckServletFilter.class.getDeclaredMethod("getCustomResolvedLabels", HttpServletRequest.class);
getCustomResolvedLabels.setAccessible(true);
// Mock request
MockHttpServletRequest request = new MockHttpServletRequest();
// labelResolver != null
Map<String, String> result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get("ServletResolver")).isEqualTo("ServletResolver");
// throw exception
PolarisRateLimiterLabelServletResolver exceptionLabelResolver = request1 -> {
throw new RuntimeException("Mock exception.");
};
quotaCheckServletFilter = new QuotaCheckServletFilter(null, exceptionLabelResolver, null, null, null);
result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request);
assertThat(result.size()).isEqualTo(0);
// labelResolver == null
quotaCheckServletFilter = new QuotaCheckServletFilter(null, null, null, null, null);
result = (Map<String, String>) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request);
assertThat(result.size()).isEqualTo(0);
getCustomResolvedLabels.setAccessible(false);
}
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
fail("Exception encountered.", e);
}
}
@Test
public void testDoFilterInternal() {
// Create mock FilterChain
@ -210,34 +162,39 @@ public class QuotaCheckServletFilterTest {
// Mock request
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
quotaCheckServletFilter.init();
quotaCheckWithHtmlRejectTipsServletFilter.init();
try {
// Pass
MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain);
MockHttpServletResponse testApp1Response = new MockHttpServletResponse();
quotaCheckServletFilter.doFilterInternal(request, testApp1Response, filterChain);
// Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2";
MockHttpServletResponse testApp2Response = new MockHttpServletResponse();
long startTimestamp = System.currentTimeMillis();
quotaCheckServletFilter.doFilterInternal(request, response, filterChain);
quotaCheckServletFilter.doFilterInternal(request, testApp2Response, filterChain);
assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L);
// Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain);
assertThat(response.getStatus()).isEqualTo(419);
assertThat(response.getContentAsString()).isEqualTo("RejectRequestTips提示消息");
MockHttpServletResponse testApp3Response = new MockHttpServletResponse();
quotaCheckServletFilter.doFilterInternal(request, testApp3Response, filterChain);
assertThat(testApp3Response.getStatus()).isEqualTo(419);
assertThat(testApp3Response.getContentAsString()).isEqualTo("RejectRequestTips提示消息");
quotaCheckWithHtmlRejectTipsServletFilter.doFilterInternal(request, response, filterChain);
assertThat(response.getStatus()).isEqualTo(419);
assertThat(response.getContentAsString()).isEqualTo("RejectRequestTips提示消息");
MockHttpServletResponse testApp3Response2 = new MockHttpServletResponse();
quotaCheckWithHtmlRejectTipsServletFilter.doFilterInternal(request, testApp3Response2, filterChain);
assertThat(testApp3Response2.getStatus()).isEqualTo(419);
assertThat(testApp3Response2.getContentAsString()).isEqualTo("<h1>RejectRequestTips提示消息</h1>");
// Exception
MockHttpServletResponse testApp4Response = new MockHttpServletResponse();
MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain);
quotaCheckServletFilter.doFilterInternal(request, testApp4Response, filterChain);
}
catch (ServletException | IOException e) {
fail("Exception encountered.", e);
@ -252,31 +209,34 @@ public class QuotaCheckServletFilterTest {
// Mock request
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.init();
try {
// Pass
MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain);
MockHttpServletResponse testApp1response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp1response, filterChain);
// Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2";
MockHttpServletResponse testApp2response = new MockHttpServletResponse();
long startTimestamp = System.currentTimeMillis();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain);
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp2response, filterChain);
assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L);
// Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3";
MockHttpServletResponse testApp3response = new MockHttpServletResponse();
String contentType = new MediaType(polarisRateLimiterLimitedFallback.mediaType(), polarisRateLimiterLimitedFallback.charset()).toString();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain);
assertThat(response.getStatus()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode());
assertThat(response.getContentAsString()).isEqualTo(polarisRateLimiterLimitedFallback.rejectTips());
assertThat(response.getContentType()).isEqualTo(contentType);
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp3response, filterChain);
assertThat(testApp3response.getStatus()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode());
assertThat(testApp3response.getContentAsString()).isEqualTo(polarisRateLimiterLimitedFallback.rejectTips());
assertThat(testApp3response.getContentType()).isEqualTo(contentType);
// Exception
MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain);
MockHttpServletResponse testApp4response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp4response, filterChain);
}
catch (ServletException | IOException e) {
fail("Exception encountered.", e);

@ -0,0 +1,144 @@
/*
* 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.ratelimit.resolver;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
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.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilterTest;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.server.ServerWebExchange;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = RateLimitRuleArgumentReactiveResolverTest.TestApplication.class,
properties = {
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
})
public class RateLimitRuleArgumentReactiveResolverTest {
private final PolarisRateLimiterLabelReactiveResolver labelResolver =
exchange -> Collections.singletonMap("xxx", "xxx");
private final PolarisRateLimiterLabelReactiveResolver labelResolverEx =
exchange -> {
throw new RuntimeException();
};
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver1;
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver2;
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver3;
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver4;
@BeforeEach
void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST";
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader().getResourceAsStream("ratelimit.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
// normal
this.rateLimitRuleArgumentReactiveResolver1 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolver);
// ex
this.rateLimitRuleArgumentReactiveResolver2 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolverEx);
// null
ServiceRuleManager serviceRuleManager1 = mock(ServiceRuleManager.class);
when(serviceRuleManager1.getServiceRateLimitRule(anyString(), anyString())).thenReturn(null);
this.rateLimitRuleArgumentReactiveResolver3 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager1, labelResolver);
// null 2
ServiceRuleManager serviceRuleManager2 = mock(ServiceRuleManager.class);
RateLimitProto.RateLimit rateLimit2 = RateLimitProto.RateLimit.newBuilder().build();
when(serviceRuleManager2.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit2);
this.rateLimitRuleArgumentReactiveResolver4 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager2, labelResolver);
}
@Test
public void testGetRuleArguments() {
// Mock request
MetadataContext.LOCAL_SERVICE = "Test";
// Mock request
MockServerHttpRequest request = MockServerHttpRequest.get("http://127.0.0.1:8080/test")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.header("xxx", "xxx")
.queryParam("yyy", "yyy")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
MetadataContext metadataContext = new MetadataContext();
metadataContext.setUpstreamDisposableMetadata(new HashMap<String, String>() {{
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, MetadataContext.LOCAL_SERVICE);
}});
MetadataContextHolder.set(metadataContext);
Set<Argument> arguments = rateLimitRuleArgumentReactiveResolver1.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
Set<Argument> exceptRes = new HashSet<>();
exceptRes.add(Argument.buildMethod("GET"));
exceptRes.add(Argument.buildHeader("xxx", "xxx"));
exceptRes.add(Argument.buildQuery("yyy", "yyy"));
exceptRes.add(Argument.buildCallerIP("127.0.0.1"));
exceptRes.add(Argument.buildCustom("xxx", "xxx"));
exceptRes.add(Argument.buildCallerService(MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE));
assertThat(arguments).isEqualTo(exceptRes);
rateLimitRuleArgumentReactiveResolver2.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
rateLimitRuleArgumentReactiveResolver3.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
rateLimitRuleArgumentReactiveResolver4.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
}
@SpringBootApplication
protected static class TestApplication {
}
}

@ -0,0 +1,135 @@
/*
* 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.ratelimit.resolver;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
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.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.context.ServiceRuleManager;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilterTest;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.polaris.ratelimit.api.rpc.Argument;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = RateLimitRuleArgumentServletResolverTest.TestApplication.class,
properties = {
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
})
public class RateLimitRuleArgumentServletResolverTest {
private final PolarisRateLimiterLabelServletResolver labelResolver =
exchange -> Collections.singletonMap("xxx", "xxx");
private final PolarisRateLimiterLabelServletResolver labelResolverEx =
exchange -> {
throw new RuntimeException();
};
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver1;
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver2;
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver3;
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver4;
@BeforeEach
void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST";
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader().getResourceAsStream("ratelimit.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
// normal
this.rateLimitRuleArgumentServletResolver1 = new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolver);
// ex
this.rateLimitRuleArgumentServletResolver2 = new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolverEx);
// null
ServiceRuleManager serviceRuleManager1 = mock(ServiceRuleManager.class);
when(serviceRuleManager1.getServiceRateLimitRule(anyString(), anyString())).thenReturn(null);
this.rateLimitRuleArgumentServletResolver3 = new RateLimitRuleArgumentServletResolver(serviceRuleManager1, labelResolver);
// null 2
ServiceRuleManager serviceRuleManager2 = mock(ServiceRuleManager.class);
RateLimitProto.RateLimit rateLimit2 = RateLimitProto.RateLimit.newBuilder().build();
when(serviceRuleManager2.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit2);
this.rateLimitRuleArgumentServletResolver4 = new RateLimitRuleArgumentServletResolver(serviceRuleManager2, labelResolver);
}
@Test
public void testGetRuleArguments() {
// Mock request
MetadataContext.LOCAL_SERVICE = "Test";
MockHttpServletRequest request = new MockHttpServletRequest(null, "GET", "/xxx");
request.setParameter("yyy", "yyy");
request.addHeader("xxx", "xxx");
MetadataContext metadataContext = new MetadataContext();
metadataContext.setUpstreamDisposableMetadata(new HashMap<String, String>() {{
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, MetadataContext.LOCAL_SERVICE);
}});
MetadataContextHolder.set(metadataContext);
Set<Argument> arguments = rateLimitRuleArgumentServletResolver1.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
Set<Argument> exceptRes = new HashSet<>();
exceptRes.add(Argument.buildMethod("GET"));
exceptRes.add(Argument.buildHeader("xxx", "xxx"));
exceptRes.add(Argument.buildQuery("yyy", "yyy"));
exceptRes.add(Argument.buildCallerIP("127.0.0.1"));
exceptRes.add(Argument.buildCustom("xxx", "xxx"));
exceptRes.add(Argument.buildCallerService(MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE));
assertThat(arguments).isEqualTo(exceptRes);
rateLimitRuleArgumentServletResolver2.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
rateLimitRuleArgumentServletResolver3.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
rateLimitRuleArgumentServletResolver4.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
}
@SpringBootApplication
protected static class TestApplication {
}
}

@ -17,6 +17,9 @@
package com.tencent.cloud.polaris.ratelimit.utils;
import java.util.HashMap;
import java.util.HashSet;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
@ -66,28 +69,59 @@ public class QuotaCheckUtilsTest {
public void testGetQuota() {
// Pass
String serviceName = "TestApp1";
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null);
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashMap<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Unirate waiting 1000ms
serviceName = "TestApp2";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashMap<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(1000);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Rate limited
serviceName = "TestApp3";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashMap<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultLimited);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultLimited");
// Exception
serviceName = "TestApp4";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashMap<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("get quota failed");
}
@Test
public void testGetQuota2() {
// Pass
String serviceName = "TestApp1";
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashSet<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Unirate waiting 1000ms
serviceName = "TestApp2";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null);
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashSet<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(1000);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Rate limited
serviceName = "TestApp3";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null);
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashSet<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultLimited);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultLimited");
// Exception
serviceName = "TestApp4";
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null);
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, new HashSet<>(), null);
assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("get quota failed");

@ -0,0 +1,95 @@
{
"id": "f7560cce829e4d4c8556a6be63539af5",
"service": "xxx",
"namespace": "default",
"subset": {},
"priority": 0,
"resource": "QPS",
"type": "GLOBAL",
"labels": {},
"amounts": [
{
"maxAmount": 1,
"validDuration": "1s",
"precision": null,
"startAmount": null,
"minAmount": null
}
],
"action": "REJECT",
"disable": false,
"report": null,
"ctime": "2022-12-11 21:56:59",
"mtime": "2023-03-10 15:40:33",
"revision": "6eec6f416bee40ecbf664c93add61358",
"service_token": null,
"adjuster": null,
"regex_combine": true,
"amountMode": "GLOBAL_TOTAL",
"failover": "FAILOVER_LOCAL",
"cluster": null,
"method": {
"type": "EXACT",
"value": "/xxx",
"value_type": "TEXT"
},
"arguments": [
{
"type": "CALLER_SERVICE",
"key": "default",
"value": {
"type": "EXACT",
"value": "xxx",
"value_type": "TEXT"
}
},
{
"type": "HEADER",
"key": "xxx",
"value": {
"type": "EXACT",
"value": "xxx",
"value_type": "TEXT"
}
},
{
"type": "QUERY",
"key": "yyy",
"value": {
"type": "EXACT",
"value": "yyy",
"value_type": "TEXT"
}
},
{
"type": "METHOD",
"key": "$method",
"value": {
"type": "EXACT",
"value": "GET",
"value_type": "TEXT"
}
},
{
"type": "CALLER_IP",
"key": "$caller_ip",
"value": {
"type": "EXACT",
"value": "127.0.0.1",
"value_type": "TEXT"
}
},
{
"type": "CUSTOM",
"key": "xxx",
"value": {
"type": "EXACT",
"value": "xxx",
"value_type": "TEXT"
}
}
],
"name": "xxx",
"etime": "2023-03-10 15:40:33",
"max_queue_delay": 30
}

@ -76,7 +76,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-zuul</artifactId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<optional>true</optional>
</dependency>

@ -136,6 +136,7 @@ public class PolarisLoadBalancerCompositeRule extends AbstractLoadBalancerRule {
// 2. filter by router
List<Server> serversAfterRouter;
if (key != null) {
if (key instanceof PolarisRouterContext) {
// router implement for Feign and scg
PolarisRouterContext routerContext = (PolarisRouterContext) key;
@ -159,6 +160,10 @@ public class PolarisLoadBalancerCompositeRule extends AbstractLoadBalancerRule {
else {
serversAfterRouter = allServers;
}
}
else {
serversAfterRouter = allServers;
}
// 3. put filtered servers to thread local, so delegate rule choose servers from filtered servers.
polarisLoadBalancer.addServers(serversAfterRouter);

@ -79,4 +79,29 @@ public final class ContextConstant {
*/
public static Integer STAT_REPORTER_ORDER = 1;
}
public static final class Zuul {
/**
* polaris circuit breaker.
*/
public static final String POLARIS_CIRCUIT_BREAKER = "PolarisCircuitBreaker";
/**
* timestamp before route.
*/
public static final String POLARIS_PRE_ROUTE_TIME = "PolarisPreRouteTime";
/**
* is in routing state.
*/
public static final String POLARIS_IS_IN_ROUTING_STATE = "PolarisIsInRoutingState";
/**
* polaris circuit breaker.
*/
public static final String ENHANCED_PLUGIN_CONTEXT = "EnhancedPluginContext";
private Zuul() {
}
}
}

@ -0,0 +1,52 @@
/*
* 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.common.constant;
/**
* Built-in system http header fields.
*/
public final class HeaderConstant {
/**
* The called service returns the real call result of its own processing request.
*/
public static final String INTERNAL_CALLEE_RET_STATUS = "internal-callee-retstatus";
/**
* The name of the rule that the current limit/circiutbreaker rule takes effect.
*/
public static final String INTERNAL_ACTIVE_RULE_NAME = "internal-callee-activerule";
/**
* The name information of the called service.
*/
public static final String INTERNAL_CALLEE_SERVICE_ID = "internal-callee-serviceid";
/**
* The name information of the called instance host.
*/
public static final String INTERNAL_CALLEE_INSTANCE_HOST = "internal-callee-instance-host";
/**
* The name information of the called instance port.
*/
public static final String INTERNAL_CALLEE_INSTANCE_PORT = "internal-callee-instance-port";
private HeaderConstant() {
}
}

@ -26,17 +26,10 @@ import org.springframework.core.Ordered;
*/
public final class MetadataConstant {
/**
* Default Private Constructor.
*/
private MetadataConstant() {
}
/**
* sct transitive header prefix.
*/
public static final String SCT_TRANSITIVE_HEADER_PREFIX = "X-SCT-Metadata-Transitive-";
/**
* sct transitive header prefix length.
*/
@ -46,12 +39,15 @@ public final class MetadataConstant {
* polaris transitive header prefix.
*/
public static final String POLARIS_TRANSITIVE_HEADER_PREFIX = "X-Polaris-Metadata-Transitive-";
/**
* polaris transitive header prefix length.
*/
public static final int POLARIS_TRANSITIVE_HEADER_PREFIX_LENGTH = POLARIS_TRANSITIVE_HEADER_PREFIX.length();
private MetadataConstant() {
}
/**
* Order of filter, interceptor, ...
*/
@ -60,7 +56,7 @@ public final class MetadataConstant {
/**
* Order of filter.
*/
public static final int WEB_FILTER_ORDER = Ordered.HIGHEST_PRECEDENCE + 13;
public static final int WEB_FILTER_ORDER = Ordered.HIGHEST_PRECEDENCE + 9;
/**
* Order of MetadataFirstFeignInterceptor.
@ -98,4 +94,19 @@ public final class MetadataConstant {
*/
public static final String METADATA_CONTEXT = "SCT-METADATA-CONTEXT";
}
public static class DefaultMetadata {
/**
* Default Metadata Source Service Namespace Key.
*/
public static final String DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE = "source_service_namespace";
/**
* Default Metadata Source Service Name Key.
*/
public static final String DEFAULT_METADATA_SOURCE_SERVICE_NAME = "source_service_name";
}
}

@ -29,8 +29,6 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import static com.tencent.cloud.common.metadata.MetadataContext.FRAGMENT_DISPOSABLE;
import static com.tencent.cloud.common.metadata.MetadataContext.FRAGMENT_RAW_TRANSHEADERS;
import static com.tencent.cloud.common.metadata.MetadataContext.FRAGMENT_TRANSITIVE;
import static com.tencent.cloud.common.metadata.MetadataContext.FRAGMENT_UPSTREAM_DISPOSABLE;
/**
@ -67,11 +65,11 @@ public final class MetadataContextHolder {
// init static transitive metadata
MetadataContext metadataContext = new MetadataContext();
metadataContext.putFragmentContext(FRAGMENT_TRANSITIVE, staticMetadataManager.getMergedStaticTransitiveMetadata());
metadataContext.putFragmentContext(FRAGMENT_DISPOSABLE, staticMetadataManager.getMergedStaticDisposableMetadata());
metadataContext.setTransitiveMetadata(staticMetadataManager.getMergedStaticTransitiveMetadata());
metadataContext.setDisposableMetadata(staticMetadataManager.getMergedStaticDisposableMetadata());
if (StringUtils.hasText(staticMetadataManager.getTransHeaderFromEnv())) {
metadataContext.putContext(FRAGMENT_RAW_TRANSHEADERS, staticMetadataManager.getTransHeaderFromEnv(), "");
metadataContext.setTransHeaders(staticMetadataManager.getTransHeaderFromEnv(), "");
}
METADATA_CONTEXT.set(metadataContext);
@ -132,18 +130,19 @@ public final class MetadataContextHolder {
// Save transitive metadata to ThreadLocal.
if (!CollectionUtils.isEmpty(dynamicTransitiveMetadata)) {
Map<String, String> staticTransitiveMetadata = metadataContext.getFragmentContext(FRAGMENT_TRANSITIVE);
Map<String, String> staticTransitiveMetadata = metadataContext.getTransitiveMetadata();
Map<String, String> mergedTransitiveMetadata = new HashMap<>();
mergedTransitiveMetadata.putAll(staticTransitiveMetadata);
mergedTransitiveMetadata.putAll(dynamicTransitiveMetadata);
metadataContext.putFragmentContext(FRAGMENT_TRANSITIVE, Collections.unmodifiableMap(mergedTransitiveMetadata));
Map<String, String> mergedDisposableMetadata = new HashMap<>(dynamicDisposableMetadata);
metadataContext.putFragmentContext(FRAGMENT_UPSTREAM_DISPOSABLE, Collections.unmodifiableMap(mergedDisposableMetadata));
metadataContext.setTransitiveMetadata(Collections.unmodifiableMap(mergedTransitiveMetadata));
}
if (!CollectionUtils.isEmpty(dynamicDisposableMetadata)) {
Map<String, String> mergedUpstreamDisposableMetadata = new HashMap<>(dynamicDisposableMetadata);
metadataContext.setUpstreamDisposableMetadata(Collections.unmodifiableMap(mergedUpstreamDisposableMetadata));
}
Map<String, String> staticDisposableMetadata = metadataContext.getFragmentContext(FRAGMENT_DISPOSABLE);
metadataContext.putFragmentContext(FRAGMENT_DISPOSABLE, Collections.unmodifiableMap(staticDisposableMetadata));
}
metadataContext.setDisposableMetadata(Collections.unmodifiableMap(staticDisposableMetadata));
MetadataContextHolder.set(metadataContext);
}

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
import com.tencent.cloud.common.spi.InstanceMetadataProvider;
@ -34,7 +35,7 @@ import org.springframework.util.CollectionUtils;
/**
* manage metadata from env/config file/custom spi.
*
* @author lepdou 2022-05-20
* @author lepdou, Haotian Zhang
*/
public class StaticMetadataManager {
/**
@ -53,7 +54,6 @@ public class StaticMetadataManager {
private static final String ENV_METADATA_PREFIX = "SCT_METADATA_CONTENT_";
private static final int ENV_METADATA_PREFIX_LENGTH = ENV_METADATA_PREFIX.length();
private static final String ENV_METADATA_CONTENT_TRANSITIVE = "SCT_METADATA_CONTENT_TRANSITIVE";
private static final String ENV_METADATA_CONTENT_DISPOSABLE = "SCT_METADATA_CONTENT_DISPOSABLE";
/**
* This is the key of the header's key list needed to be transmitted. The list is a string split with ,.
@ -82,14 +82,14 @@ public class StaticMetadataManager {
private String campus;
public StaticMetadataManager(MetadataLocalProperties metadataLocalProperties,
InstanceMetadataProvider instanceMetadataProvider) {
List<InstanceMetadataProvider> instanceMetadataProviders) {
parseConfigMetadata(metadataLocalProperties);
parseEnvMetadata();
parseCustomMetadata(instanceMetadataProvider);
parseCustomMetadata(instanceMetadataProviders);
parseLocationMetadata(metadataLocalProperties, instanceMetadataProvider);
parseLocationMetadata(metadataLocalProperties, instanceMetadataProviders);
merge();
@ -183,21 +183,25 @@ public class StaticMetadataManager {
}
@SuppressWarnings("DuplicatedCode")
private void parseCustomMetadata(InstanceMetadataProvider instanceMetadataProvider) {
if (instanceMetadataProvider == null) {
customSPIMetadata = Collections.emptyMap();
customSPITransitiveMetadata = Collections.emptyMap();
customSPIDisposableMetadata = Collections.emptyMap();
return;
private void parseCustomMetadata(List<InstanceMetadataProvider> instanceMetadataProviders) {
// init customSPIMetadata
customSPIMetadata = new HashMap<>();
customSPITransitiveMetadata = new HashMap<>();
customSPIDisposableMetadata = new HashMap<>();
if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
instanceMetadataProviders.forEach(this::parseCustomMetadata);
}
customSPIMetadata = Collections.unmodifiableMap(customSPIMetadata);
customSPITransitiveMetadata = Collections.unmodifiableMap(customSPITransitiveMetadata);
customSPIDisposableMetadata = Collections.unmodifiableMap(customSPIDisposableMetadata);
}
@SuppressWarnings("DuplicatedCode")
private void parseCustomMetadata(InstanceMetadataProvider instanceMetadataProvider) {
// resolve all metadata
Map<String, String> allMetadata = instanceMetadataProvider.getMetadata();
if (allMetadata == null) {
customSPIMetadata = Collections.emptyMap();
}
else {
customSPIMetadata = Collections.unmodifiableMap(allMetadata);
if (!CollectionUtils.isEmpty(allMetadata)) {
customSPIMetadata.putAll(allMetadata);
}
// resolve transitive metadata
@ -210,7 +214,7 @@ public class StaticMetadataManager {
}
}
}
customSPITransitiveMetadata = Collections.unmodifiableMap(transitiveMetadata);
customSPITransitiveMetadata.putAll(transitiveMetadata);
Set<String> disposableKeys = instanceMetadataProvider.getDisposableMetadataKeys();
Map<String, String> disposableMetadata = new HashMap<>();
@ -221,7 +225,7 @@ public class StaticMetadataManager {
}
}
}
customSPIDisposableMetadata = Collections.unmodifiableMap(disposableMetadata);
customSPIDisposableMetadata.putAll(disposableMetadata);
}
private void merge() {
@ -231,6 +235,7 @@ public class StaticMetadataManager {
mergedMetadataResult.putAll(configMetadata);
mergedMetadataResult.putAll(envMetadata);
mergedMetadataResult.putAll(customSPIMetadata);
this.mergedStaticMetadata = Collections.unmodifiableMap(mergedMetadataResult);
Map<String, String> mergedTransitiveMetadataResult = new HashMap<>();
@ -247,10 +252,16 @@ public class StaticMetadataManager {
}
private void parseLocationMetadata(MetadataLocalProperties metadataLocalProperties,
InstanceMetadataProvider instanceMetadataProvider) {
List<InstanceMetadataProvider> instanceMetadataProviders) {
// resolve region info
if (instanceMetadataProvider != null) {
region = instanceMetadataProvider.getRegion();
if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
Set<String> providerRegions = instanceMetadataProviders.stream().map(InstanceMetadataProvider::getRegion).filter(region -> !StringUtils.isBlank(region)).collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(providerRegions)) {
if (providerRegions.size() > 1) {
throw new IllegalArgumentException("Multiple Regions Provided in InstanceMetadataProviders");
}
region = providerRegions.iterator().next();
}
}
if (StringUtils.isBlank(region)) {
region = System.getenv(ENV_METADATA_REGION);
@ -260,8 +271,14 @@ public class StaticMetadataManager {
}
// resolve zone info
if (instanceMetadataProvider != null) {
zone = instanceMetadataProvider.getZone();
if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
Set<String> providerZones = instanceMetadataProviders.stream().map(InstanceMetadataProvider::getZone).filter(zone -> !StringUtils.isBlank(zone)).collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(providerZones)) {
if (providerZones.size() > 1) {
throw new IllegalArgumentException("Multiple Zones Provided in InstanceMetadataProviders");
}
zone = providerZones.iterator().next();
}
}
if (StringUtils.isBlank(zone)) {
zone = System.getenv(ENV_METADATA_ZONE);
@ -271,8 +288,14 @@ public class StaticMetadataManager {
}
// resolve campus info
if (instanceMetadataProvider != null) {
campus = instanceMetadataProvider.getCampus();
if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
Set<String> providerCampus = instanceMetadataProviders.stream().map(InstanceMetadataProvider::getCampus).filter(campus -> !StringUtils.isBlank(campus)).collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(providerCampus)) {
if (providerCampus.size() > 1) {
throw new IllegalArgumentException("Multiple Campus Provided in InstanceMetadataProviders");
}
campus = providerCampus.iterator().next();
}
}
if (StringUtils.isBlank(campus)) {
campus = System.getenv(ENV_METADATA_CAMPUS);

@ -18,8 +18,12 @@
package com.tencent.cloud.common.metadata.config;
import java.util.List;
import com.tencent.cloud.common.metadata.StaticMetadataManager;
import com.tencent.cloud.common.spi.InstanceMetadataProvider;
import com.tencent.cloud.common.spi.impl.DefaultInstanceMetadataProvider;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -42,10 +46,15 @@ public class MetadataAutoConfiguration {
return new MetadataLocalProperties();
}
@Bean
public InstanceMetadataProvider defaultInstanceMetadataProvider(ApplicationContextAwareUtils applicationContextAwareUtils) {
return new DefaultInstanceMetadataProvider(applicationContextAwareUtils);
}
@Bean
public StaticMetadataManager metadataManager(MetadataLocalProperties metadataLocalProperties,
@Nullable InstanceMetadataProvider instanceMetadataProvider) {
return new StaticMetadataManager(metadataLocalProperties, instanceMetadataProvider);
@Nullable List<InstanceMetadataProvider> instanceMetadataProviders) {
return new StaticMetadataManager(metadataLocalProperties, instanceMetadataProviders);
}
}

@ -0,0 +1,63 @@
/*
* 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.common.spi.impl;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.tencent.cloud.common.spi.InstanceMetadataProvider;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
import static com.tencent.cloud.common.metadata.MetadataContext.LOCAL_NAMESPACE;
import static com.tencent.cloud.common.metadata.MetadataContext.LOCAL_SERVICE;
/**
* DefaultInstanceMetadataProvider.
* provide DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, DEFAULT_METADATA_SOURCE_SERVICE_NAME
*
* @author sean yu
*/
public class DefaultInstanceMetadataProvider implements InstanceMetadataProvider {
private final ApplicationContextAwareUtils applicationContextAwareUtils;
// ensure ApplicationContextAwareUtils init before
public DefaultInstanceMetadataProvider(ApplicationContextAwareUtils applicationContextAwareUtils) {
this.applicationContextAwareUtils = applicationContextAwareUtils;
}
@Override
public Map<String, String> getMetadata() {
return new HashMap<String, String>() {{
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, LOCAL_NAMESPACE);
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, LOCAL_SERVICE);
}};
}
@Override
public Set<String> getDisposableMetadataKeys() {
return new HashSet<>(Arrays.asList(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, DEFAULT_METADATA_SOURCE_SERVICE_NAME));
}
}

@ -111,14 +111,4 @@ public final class JacksonUtils {
throw new RuntimeException("Json to map failed.", e);
}
}
public static <T> T json2JavaBean(String content, Class<T> valueType) {
try {
return OM.readValue(content, valueType);
}
catch (Exception e) {
LOG.error("json {} to class {} failed. ", content, valueType, e);
throw new RuntimeException("json to class failed.", e);
}
}
}

@ -0,0 +1,34 @@
/*
* 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.common.util;
/**
* Request Label Utils.
*/
public final class RequestLabelUtils {
private RequestLabelUtils() {
}
public static String convertLabel(String label) {
label = label.replaceAll("\"|\\{|\\}", "")
.replaceAll(",", "|");
return label;
}
}

@ -0,0 +1,80 @@
/*
* 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.common.util;
import java.net.URL;
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.REQUEST_URI_KEY;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;
/**
* Utils for Zuul filter.
*
* @author Haotian Zhang
*/
public final class ZuulFilterUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(ZuulFilterUtils.class);
private ZuulFilterUtils() {
}
public static String getServiceId(RequestContext context) {
String serviceId = (String) context.get(SERVICE_ID_KEY);
if (StringUtils.isBlank(serviceId)) {
URL url = context.getRouteHost();
if (url != null) {
serviceId = url.getAuthority();
context.set(SERVICE_ID_KEY, serviceId);
}
}
return serviceId;
}
public static String getPath(RequestContext context) {
HttpServletRequest request = context.getRequest();
String uri = request.getRequestURI();
String contextURI = (String) context.get(REQUEST_URI_KEY);
if (contextURI != null) {
try {
uri = UriUtils.encodePath(contextURI, characterEncoding(request));
}
catch (Exception e) {
LOGGER.debug("unable to encode uri path from context, falling back to uri from request", e);
}
}
// remove double slashes
uri = uri.replace("//", "/");
return uri;
}
private static String characterEncoding(HttpServletRequest request) {
return request.getCharacterEncoding() != null ? request.getCharacterEncoding()
: WebUtils.DEFAULT_CHARACTER_ENCODING;
}
}

@ -18,6 +18,7 @@
package com.tencent.cloud.common.metadata;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -26,20 +27,33 @@ import java.util.Set;
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
import com.tencent.cloud.common.spi.InstanceMetadataProvider;
import com.tencent.cloud.common.spi.impl.DefaultInstanceMetadataProvider;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import uk.org.webcompere.systemstubs.jupiter.SystemStub;
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* test for {@link StaticMetadataManager}.
*@author lepdou 2022-06-27
*
* @author lepdou 2022-06-27
*/
@ExtendWith({MockitoExtension.class, SystemStubsExtension.class})
public class StaticMetadataManagerTest {
@ -52,6 +66,26 @@ public class StaticMetadataManagerTest {
@Mock
private MetadataLocalProperties metadataLocalProperties;
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@BeforeAll
static void beforeAll() {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test");
}
@AfterAll
static void afterAll() {
mockedApplicationContextAwareUtils.close();
}
@BeforeEach
void setUp() {
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
}
@Test
public void testParseConfigMetadata() {
Map<String, String> content = new HashMap<>();
@ -62,6 +96,7 @@ public class StaticMetadataManagerTest {
when(metadataLocalProperties.getContent()).thenReturn(content);
when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1"));
when(metadataLocalProperties.getDisposable()).thenReturn(Collections.singletonList("k1"));
StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties, null);
@ -74,6 +109,10 @@ public class StaticMetadataManagerTest {
assertThat(transitiveMetadata.size()).isEqualTo(1);
assertThat(transitiveMetadata.get("k1")).isEqualTo("v1");
Map<String, String> disposableMetadata = metadataManager.getConfigDisposableMetadata();
assertThat(disposableMetadata.size()).isEqualTo(1);
assertThat(disposableMetadata.get("k1")).isEqualTo("v1");
assertThat(metadataManager.getZone()).isEqualTo("zone1");
assertThat(metadataManager.getRegion()).isEqualTo("region1");
@ -92,17 +131,25 @@ public class StaticMetadataManagerTest {
when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1"));
StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties,
new MockedMetadataProvider());
Arrays.asList(new MockedMetadataProvider(), new DefaultInstanceMetadataProvider(null)));
Map<String, String> metadata = metadataManager.getAllCustomMetadata();
assertThat(metadata.size()).isEqualTo(3);
assertThat(metadata.size()).isEqualTo(5);
assertThat(metadata.get("k1")).isEqualTo("v1");
assertThat(metadata.get("k2")).isEqualTo("v22");
assertThat(metadata.get("k3")).isEqualTo("v33");
assertThat(metadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE)).isEqualTo(NAMESPACE_TEST);
assertThat(metadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAME)).isEqualTo(SERVICE_PROVIDER);
Map<String, String> transitiveMetadata = metadataManager.getCustomSPITransitiveMetadata();
assertThat(transitiveMetadata.size()).isEqualTo(1);
assertThat(metadata.get("k2")).isEqualTo("v22");
assertThat(transitiveMetadata.get("k2")).isEqualTo("v22");
Map<String, String> disposableMetadata = metadataManager.getCustomSPIDisposableMetadata();
assertThat(disposableMetadata.size()).isEqualTo(3);
assertThat(disposableMetadata.get("k3")).isEqualTo("v33");
assertThat(disposableMetadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE)).isEqualTo(NAMESPACE_TEST);
assertThat(disposableMetadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAME)).isEqualTo(SERVICE_PROVIDER);
assertThat(metadataManager.getZone()).isEqualTo("zone2");
assertThat(metadataManager.getRegion()).isEqualTo("region1");
@ -125,29 +172,38 @@ public class StaticMetadataManagerTest {
when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1"));
StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties,
new MockedMetadataProvider());
Arrays.asList(new MockedMetadataProvider(), new DefaultInstanceMetadataProvider(null)));
Map<String, String> metadata = metadataManager.getMergedStaticMetadata();
assertThat(metadata.size()).isEqualTo(6);
assertThat(metadata.size()).isEqualTo(8);
assertThat(metadata.get("k1")).isEqualTo("v1");
assertThat(metadata.get("k2")).isEqualTo("v22");
assertThat(metadata.get("k3")).isEqualTo("v33");
assertThat(metadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE)).isEqualTo(NAMESPACE_TEST);
assertThat(metadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAME)).isEqualTo(SERVICE_PROVIDER);
Map<String, String> transitiveMetadata = metadataManager.getMergedStaticTransitiveMetadata();
assertThat(transitiveMetadata.size()).isEqualTo(2);
assertThat(metadata.get("k1")).isEqualTo("v1");
assertThat(metadata.get("k2")).isEqualTo("v22");
assertThat(transitiveMetadata.get("k1")).isEqualTo("v1");
assertThat(transitiveMetadata.get("k2")).isEqualTo("v22");
Map<String, String> disposableMetadata = metadataManager.getMergedStaticDisposableMetadata();
assertThat(disposableMetadata.size()).isEqualTo(3);
assertThat(disposableMetadata.get("k3")).isEqualTo("v33");
assertThat(disposableMetadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE)).isEqualTo(NAMESPACE_TEST);
assertThat(disposableMetadata.get(DEFAULT_METADATA_SOURCE_SERVICE_NAME)).isEqualTo(SERVICE_PROVIDER);
assertThat(metadataManager.getAllEnvMetadata()).isEmpty();
assertThat(metadataManager.getEnvTransitiveMetadata()).isEmpty();
assertThat(metadataManager.getZone()).isEqualTo("zone2");
assertThat(metadataManager.getRegion()).isEqualTo("region1");
assertThat(metadataManager.getCampus()).isEqualTo("campus2");
Map<String, String> locationInfo = metadataManager.getLocationMetadata();
assertThat(locationInfo.get("zone")).isEqualTo("zone2");
assertThat(locationInfo.get("region")).isEqualTo("region1");
assertThat(locationInfo.get("campus")).isEqualTo("campus1");
assertThat(locationInfo.get("campus")).isEqualTo("campus2");
}
@Test
@ -194,6 +250,13 @@ public class StaticMetadataManagerTest {
return transitiveKeys;
}
@Override
public Set<String> getDisposableMetadataKeys() {
Set<String> transitiveKeys = new HashSet<>();
transitiveKeys.add("k3");
return transitiveKeys;
}
@Override
public String getRegion() {
return "region1";
@ -206,7 +269,7 @@ public class StaticMetadataManagerTest {
@Override
public String getCampus() {
return null;
return "campus2";
}
}
}

@ -18,6 +18,8 @@
package com.tencent.cloud.common.metadata.config;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
@ -33,12 +35,17 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
*/
public class MetadataAutoConfigurationTest {
private final ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner();
private final ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner().withPropertyValues(
"spring.application.name=test"
);
private final WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner();
private final WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner().withPropertyValues(
"spring.application.name=test"
);
private final ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner =
new ReactiveWebApplicationContextRunner();
private final ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner = new ReactiveWebApplicationContextRunner().withPropertyValues(
"spring.application.name=test"
);
/**
* No any web application.
@ -46,7 +53,7 @@ public class MetadataAutoConfigurationTest {
@Test
public void test1() {
this.applicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
});
@ -58,7 +65,7 @@ public class MetadataAutoConfigurationTest {
@Test
public void test2() {
this.webApplicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
});
@ -70,7 +77,7 @@ public class MetadataAutoConfigurationTest {
@Test
public void test3() {
this.reactiveWebApplicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
});

@ -17,6 +17,7 @@
package com.tencent.cloud.common.pojo;
import com.tencent.polaris.api.pojo.DefaultInstance;
import com.tencent.polaris.api.pojo.Instance;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@ -38,7 +39,7 @@ public class PolarisServiceInstanceTest {
@Test
@DisplayName("test getters and setters.")
void test() {
public void test1() {
Instance secureInstance = mock(Instance.class);
doReturn("test-ID").when(secureInstance).getId();
doReturn(SERVICE_PROVIDER).when(secureInstance).getService();
@ -53,6 +54,7 @@ public class PolarisServiceInstanceTest {
assertThat(securePolarisServiceInstance.getPort()).isEqualTo(8080);
assertThat(securePolarisServiceInstance.isSecure()).isTrue();
assertThat(securePolarisServiceInstance.getScheme()).isEqualTo("https");
assertThat(securePolarisServiceInstance.getUri().toString()).isEqualTo("https://1.1.1.1:8080");
Instance insecureInstance = mock(Instance.class);
doReturn("http").when(insecureInstance).getProtocol();
@ -60,4 +62,37 @@ public class PolarisServiceInstanceTest {
assertThat(insecurePolarisServiceInstance.isSecure()).isFalse();
assertThat(insecurePolarisServiceInstance.getScheme()).isEqualTo("http");
}
@Test
@DisplayName("test equals().")
public void test2() {
DefaultInstance instance1 = new DefaultInstance();
instance1.setId("test-1");
instance1.setProtocol("http");
PolarisServiceInstance polarisServiceInstance1 = new PolarisServiceInstance(instance1);
DefaultInstance instance2 = new DefaultInstance();
instance2.setId("test-1");
instance2.setProtocol("http");
PolarisServiceInstance polarisServiceInstance2 = new PolarisServiceInstance(instance2);
assertThat(polarisServiceInstance1.equals(polarisServiceInstance2)).isTrue();
}
@Test
@DisplayName("test hashCode().")
public void test3() {
DefaultInstance instance1 = new DefaultInstance();
instance1.setId("test-1");
instance1.setProtocol("http");
PolarisServiceInstance polarisServiceInstance1 = new PolarisServiceInstance(instance1);
DefaultInstance instance2 = new DefaultInstance();
instance2.setId("test-1");
instance2.setProtocol("http");
PolarisServiceInstance polarisServiceInstance2 = new PolarisServiceInstance(instance2);
assertThat(polarisServiceInstance1.hashCode()).isEqualTo(polarisServiceInstance2.hashCode());
}
}

@ -19,10 +19,16 @@ package com.tencent.cloud.common.util;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Test for {@link BeanFactoryUtils}.
@ -41,6 +47,11 @@ public class BeanFactoryUtilsTest {
assertThat(childBeanFactory.getBeansOfType(Foo.class)).isEmpty();
assertThat(BeanFactoryUtils.getBeans(childBeanFactory, Foo.class).size()).isEqualTo(1);
assertThat(BeanFactoryUtils.getBeans(childBeanFactory, Bar.class)).isEmpty();
MockBeanFactory mockBeanFactory = new MockBeanFactory();
assertThatThrownBy(() -> BeanFactoryUtils.getBeans(mockBeanFactory, Bar.class))
.isExactlyInstanceOf(RuntimeException.class)
.hasMessageContaining("bean factory not support get list bean.");
}
static class Foo {
@ -50,4 +61,82 @@ public class BeanFactoryUtilsTest {
static class Bar {
}
static class MockBeanFactory implements BeanFactory {
@Override
public Object getBean(String s) throws BeansException {
return null;
}
@Override
public <T> T getBean(String s, Class<T> aClass) throws BeansException {
return null;
}
@Override
public Object getBean(String s, Object... objects) throws BeansException {
return null;
}
@Override
public <T> T getBean(Class<T> aClass) throws BeansException {
return null;
}
@Override
public <T> T getBean(Class<T> aClass, Object... objects) throws BeansException {
return null;
}
@Override
public <T> ObjectProvider<T> getBeanProvider(Class<T> aClass) {
return null;
}
@Override
public <T> ObjectProvider<T> getBeanProvider(ResolvableType resolvableType) {
return null;
}
@Override
public boolean containsBean(String s) {
return false;
}
@Override
public boolean isSingleton(String s) throws NoSuchBeanDefinitionException {
return false;
}
@Override
public boolean isPrototype(String s) throws NoSuchBeanDefinitionException {
return false;
}
@Override
public boolean isTypeMatch(String s, ResolvableType resolvableType) throws NoSuchBeanDefinitionException {
return false;
}
@Override
public boolean isTypeMatch(String s, Class<?> aClass) throws NoSuchBeanDefinitionException {
return false;
}
@Override
public Class<?> getType(String s) throws NoSuchBeanDefinitionException {
return null;
}
@Override
public Class<?> getType(String s, boolean b) throws NoSuchBeanDefinitionException {
return null;
}
@Override
public String[] getAliases(String s) {
return new String[0];
}
}
}

@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -43,6 +45,7 @@ public class JacksonUtilsTest {
sourceMap.put("k2", "v2");
sourceMap.put("k3", "v3");
assertThat(JacksonUtils.serialize2Json(sourceMap)).isEqualTo("{\"k1\":\"v1\",\"k2\":\"v2\",\"k3\":\"v3\"}");
assertThat(StringUtils.trimAllWhitespace(JacksonUtils.serialize2Json(sourceMap, true))).isEqualTo("{\"k1\":\"v1\",\"k2\":\"v2\",\"k3\":\"v3\"}");
}
@Test

@ -70,10 +70,10 @@
</developers>
<properties>
<revision>1.11.1-Hoxton.SR12-SNAPSHOT</revision>
<revision>1.11.2-Hoxton.SR12-SNAPSHOT</revision>
<!-- Dependencies -->
<polaris.version>1.12.0</polaris.version>
<polaris.version>1.12.2</polaris.version>
<guava.version>31.0.1-jre</guava.version>
<logback.version>1.2.11</logback.version>
<mocktio.version>4.5.1</mocktio.version>

@ -1,27 +1,24 @@
# Spring Cloud Polaris CircuitBreaker Example
# Spring Cloud Polaris Circuitbreaker example
## 样例简介
本样例将介绍如何在Spring Cloud项目中使用```spring-cloud-starter-tencent-polaris-circuitbreaker```以使用其各项功能。
该样例分为两个微服务,即
1. ```polaris-circuitbreaker-example-a```
2. ```polaris-circuitbreaker-example-b``` 有两个实例 B默认正常服务和 B2模拟异常服务
``` polaris-circuitbreaker-example-a``` 对 ```polaris-circuitbreaker-example-b```发生调用。
本样例包括被调方```polaris-circuitbreaker-callee-service```、```polaris-circuitbreaker-callee-service2```和主调方```polaris-circuitbreaker-feign-example```、```polaris-circuitbreaker-gateway-example```、```polaris-circuitbreaker-webclient-example```。
## 使用说明
### 修改配置
修改 resource/bootstrap.yml 中北极星的服务端地址
配置如下所示。其中,${ip}和${port}为Polaris后端服务的IP地址与端口号。
```yaml
spring:
application:
name: ${application.name}
cloud:
polaris:
address: grpc://${ip}:8091
address: ${ip}:${port}
```
### 启动样例
@ -30,43 +27,40 @@ spring:
参考[Polaris Getting Started](https://github.com/PolarisMesh/polaris#getting-started)。
### 启动应用
#### 启动被调应用
- IDEA启动
分别启动```polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service```、```polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2```
分别启动
1. ```spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-a```下的```ServiceA```
2. ```spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-b```下的```ServiceB```
3. ```spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-b2```下的```ServiceB2```
#### 启动主调应用
##### 启动Feign并验证
## 验证
启动```polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example```。
### Feign调用
发送请求`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromPolaris'`, 验证熔断和Polaris-server远程拉取降级。
执行以下命令发起Feign调用其逻辑为```ServiceB```抛出一个异常
发送请求`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromCode'`, 验证熔断和代码降级。
```shell
curl -L -X GET 'localhost:48080/example/service/a/getBServiceInfo'
```
##### 启动RestTemplate并验证
预期返回情况:
启动```polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example```。
在出现
```
hello world ! I'm a service B1
```
发送请求`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromPolaris'`, 验证熔断和Polaris-server远程拉取降级。
发送请求`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromCode'`, 验证熔断和代码降级。
##### 启动WebClient并验证
启动```polaris-circuitbreaker-example/polaris-circuitbreaker-webclient-example```。
发送请求`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo'`, 验证熔断和代码降级。
时,表示 B2 已经被熔断了,请求只会打到 B1。
##### 启动SCG并验证
### 验证更多场景
启动```polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example```。
您也可以调整 ```example-b``` 和 ```example-b2``` 中 ```resource/bootstrap.yml``` is-throw-runtime-exception
参数调整服务是否抛出异常。
发送请求`curl --location --request GET 'http://127.0.0.1:48080/polaris-circuitbreaker-callee-service/example/service/b/info'`, 验证熔断和代码降级。
例如测试以下场景:
1. 两个实例都是正常的,这时候预期是 B1 和 B2 都能正常被调用到
2. 一个实例正常一个实例不正常,这时候预期是不正常实例被熔断,请求只会打到正常的实例
3. 两个实例都不正常,这时候 Feign 会自动 Fallback 到 ProviderBFallback.java 的实现类
修改```polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/resources/bootstrap.yml```。删除本地fallback方法并重启验证熔断和Polaris-server远程拉取降级。

@ -1,10 +1,10 @@
# Spring Cloud Polaris CircuitBreaker Example
# Spring Cloud Polaris Circuitbreaker example
## Example Introduction
This example shows how to use```spring-cloud-starter-tencent-polaris-circuitbreaker``` in Spring Cloud project and other features
This example shows how to use```spring-cloud-starter-tencent-polaris-circuitbreaker```in Spring Cloud project for its features.
This example is divided to two microservice, ```polaris-circuitbreaker-example-a``` and ```polaris-circuitbreaker-example-b```. In these two microservices, ```polaris-circuitbreaker-example-a``` invokes ```polaris-circuitbreaker-example-b```.
This example contains callee-service```polaris-circuitbreaker-callee-service```、```polaris-circuitbreaker-callee-service2```and caller-service```polaris-circuitbreaker-feign-example```、```polaris-circuitbreaker-gateway-example```、```polaris-circuitbreaker-webclient-example```.
## Instruction
@ -21,58 +21,46 @@ spring:
address: ${ip}:${port}
```
###Launching Example
### Launching Example
###Launching Polaris Backend Service
#### Launching Polaris Backend Service
Reference to [Polaris Getting Started](https://github.com/PolarisMesh/polaris#getting-started)
####Launching Application
#### Launching callee service
Note, because verification is needed for circuit-break feature, therefore, one needs to deploy more than two invoked services (two deployment in this example)
Launching```polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service```、```polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service2```
Launching```spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-a```'s ServiceA and ```spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-b```'s ServiceB
note, Service B needs to launch two. One can adjust the port on the same machine.
#### Launching caller service
Two Services B's ```com.tencent.cloud.polaris.circuitbreaker.example.ServiceBController.info``` logics are different. One returns normally, one is abnormal.
##### Launching Feign and Verify
- Maven Package Launching
Launching```polaris-circuitbreaker-example/polaris-circuitbreaker-feign-example```.
Execute under ```spring-cloud-tencent-examples/polaris-discovery-example```
Sending request`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromPolaris'`, Verify circuit breaker and fallback from Polaris-server.
note, Service B needs to launch two. One can adjust the port on the same machine.
Sending request`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromCode'`, Verify circuit breaker and fallback from code.
Two Services B's com.tencent.cloud.polaris.circuitbreaker.example.ServiceBController.info logics are different. One returns normally, one is abnormal.
##### Launching RestTemplate and Verify
```sh
mvn clean package
```
Launching```polaris-circuitbreaker-example/polaris-circuitbreaker-resttemplate-example```.
Then under ``polaris-circuitbreaker-example-a``` and ``polaris-circuitbreaker-example-b``` find the package that generated jar, and run it
Sending request`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromPolaris'`, Verify circuit breaker and fallback from Polaris-server.
```
java -jar ${app.jar}
```
Sending request`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo/fallbackFromCode'`, Verify circuit breaker and fallback from code.
Launch application, change ${app.jar} to jar's package name
##### Launching WebClient and Verify
##Verify
Launching```polaris-circuitbreaker-example/polaris-circuitbreaker-webclient-example```。
####Feign Invoke
Sending request`curl --location --request GET 'http://127.0.0.1:48080/example/service/a/getBServiceInfo'`, Verify circuit breaker and fallback from code.
Execute the following orders to invoke Feign, the logic is ```ServiceB``` has an abnormal signal
##### Launching SCG and Verify
```shell
curl -L -X GET 'localhost:48080/example/service/a/getBServiceInfo'
```
Launching```polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example```。
Expected return condition:
Sending request`curl --location --request GET 'http://127.0.0.1:48080/polaris-circuitbreaker-callee-service/example/service/b/info'`, Verify circuit breaker and fallback from code.
when appear
```
trigger the refuse for service b
```
Changing```polaris-circuitbreaker-example/polaris-circuitbreaker-gateway-example/resources/bootstrap.yml```, delete local fallback and restart, Verify circuit breaker and fallback from Polaris-server.
it means the request signals abnormal ServiceB, and will ciruitbreak this instance, the later requests will return normally.

@ -10,8 +10,8 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>polaris-circuitbreaker-example-b</artifactId>
<name>Polaris Circuit Breaker Example B</name>
<artifactId>polaris-circuitbreaker-callee-service</artifactId>
<name>Polaris Circuit Breaker Callee Example</name>
<dependencies>
<dependency>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save