Merge branch '2022.0' into 2022/update_polaris_java

pull/922/head
Haotian Zhang 3 years ago committed by GitHub
commit 2b231f664c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,6 +3,8 @@
- [fix: fix log feign response stream close bug.](https://github.com/Tencent/spring-cloud-tencent/pull/898) - [fix: fix log feign response stream close bug.](https://github.com/Tencent/spring-cloud-tencent/pull/898)
- [fix:remove the secondary report.](https://github.com/Tencent/spring-cloud-tencent/pull/901) - [fix:remove the secondary report.](https://github.com/Tencent/spring-cloud-tencent/pull/901)
- [feature: add ratelimit provider info and refactor ratelimit use arguments.](https://github.com/Tencent/spring-cloud-tencent/pull/904)
- [fix:optimize instance circuit beaker.](https://github.com/Tencent/spring-cloud-tencent/pull/910) - [fix:optimize instance circuit beaker.](https://github.com/Tencent/spring-cloud-tencent/pull/910)
- [fix:optimize multi service registration and discovery.](https://github.com/Tencent/spring-cloud-tencent/pull/915) - [fix:optimize multi service registration and discovery.](https://github.com/Tencent/spring-cloud-tencent/pull/915)
- [feature: improve circuit breaker usage.](https://github.com/Tencent/spring-cloud-tencent/pull/917)
- [fix:fix nacos and consul registration.](https://github.com/Tencent/spring-cloud-tencent/pull/922) - [fix:fix nacos and consul registration.](https://github.com/Tencent/spring-cloud-tencent/pull/922)

@ -28,6 +28,7 @@ import com.tencent.cloud.metadata.core.DecodeTransferMetadataServletFilter;
import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataScgFilter; import com.tencent.cloud.metadata.core.EncodeTransferMedataScgFilter;
import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientFilter;
import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -39,6 +40,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import static jakarta.servlet.DispatcherType.ASYNC; import static jakarta.servlet.DispatcherType.ASYNC;
import static jakarta.servlet.DispatcherType.ERROR; import static jakarta.servlet.DispatcherType.ERROR;
@ -140,4 +142,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,6 +26,7 @@ import java.util.stream.Collectors;
import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor;
import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientFilter;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
@ -61,6 +62,7 @@ public class MetadataTransferAutoConfigurationTest {
assertThat(context).hasSingleBean(EncodeTransferMedataRestTemplateInterceptor.class); assertThat(context).hasSingleBean(EncodeTransferMedataRestTemplateInterceptor.class);
assertThat(context).hasSingleBean(MetadataTransferAutoConfiguration.MetadataTransferScgFilterConfig.class); assertThat(context).hasSingleBean(MetadataTransferAutoConfiguration.MetadataTransferScgFilterConfig.class);
assertThat(context).hasSingleBean(GlobalFilter.class); assertThat(context).hasSingleBean(GlobalFilter.class);
assertThat(context).hasSingleBean(EncodeTransferMedataWebClientFilter.class);
}); });
} }

@ -0,0 +1,84 @@
/*
* 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.test.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"})
public class EncodeTransferMedataWebClientFilterTest {
@Autowired
private WebClient.Builder webClientBuilder;
@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");
}
@LocalServerPort
private int localServerPort;
@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");
}
}
}

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

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

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

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

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

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

@ -0,0 +1,94 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.feign;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
/**
* PolarisFeignCircuitBreaker, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreaker}, but giving Polaris modification.
*
* @author sean yu
*/
public final class PolarisFeignCircuitBreaker {
private PolarisFeignCircuitBreaker() {
throw new IllegalStateException("Don't instantiate a utility class");
}
/**
* @return builder for Feign CircuitBreaker integration
*/
public static PolarisFeignCircuitBreaker.Builder builder() {
return new PolarisFeignCircuitBreaker.Builder();
}
/**
* Builder for Feign CircuitBreaker integration.
*/
public static final class Builder extends Feign.Builder {
public Builder() {
}
private CircuitBreakerFactory circuitBreakerFactory;
private String feignClientName;
private CircuitBreakerNameResolver circuitBreakerNameResolver;
public PolarisFeignCircuitBreaker.Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
return this;
}
public PolarisFeignCircuitBreaker.Builder feignClientName(String feignClientName) {
this.feignClientName = feignClientName;
return this;
}
public PolarisFeignCircuitBreaker.Builder circuitBreakerNameResolver(CircuitBreakerNameResolver circuitBreakerNameResolver) {
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
return this;
}
public <T> T target(Target<T> target, T fallback) {
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
}
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
return build(fallbackFactory).newInstance(target);
}
@Override
public <T> T target(Target<T> target) {
return build(null).newInstance(target);
}
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler(
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, this.decoder));
return this.build();
}
}
}

@ -0,0 +1,200 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.feign;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.codec.Decoder;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import static feign.Util.checkNotNull;
/**
* PolarisFeignCircuitBreakerInvocationHandler, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler}, but giving Polaris modification.
*
* @author sean yu
*/
public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final CircuitBreakerFactory factory;
private final String feignClientName;
private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
private final FallbackFactory<?> nullableFallbackFactory;
private final Map<Method, Method> fallbackMethodMap;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
private final Decoder decoder;
public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory,
CircuitBreakerNameResolver circuitBreakerNameResolver, Decoder decoder) {
this.factory = factory;
this.feignClientName = feignClientName;
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.nullableFallbackFactory = nullableFallbackFactory;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
this.decoder = decoder;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
// early exit if the invoked method is from java.lang.Object
// code is the same as ReflectiveFeign.FeignInvocationHandler
if ("equals".equals(method.getName())) {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
}
catch (IllegalArgumentException e) {
return false;
}
}
else if ("hashCode".equals(method.getName())) {
return hashCode();
}
else if ("toString".equals(method.getName())) {
return toString();
}
String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method);
CircuitBreaker circuitBreaker = factory.create(circuitName);
Supplier<Object> supplier = asSupplier(method, args);
Function<Throwable, Object> fallbackFunction;
if (this.nullableFallbackFactory != null) {
fallbackFunction = throwable -> {
Object fallback = this.nullableFallbackFactory.create(throwable);
try {
return this.fallbackMethodMap.get(method).invoke(fallback, args);
}
catch (Exception exception) {
unwrapAndRethrow(exception);
}
return null;
};
}
else {
fallbackFunction = throwable -> {
PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback =
(PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(throwable);
return fallback.fallback(method);
};
}
return circuitBreaker.run(supplier, fallbackFunction);
}
private void unwrapAndRethrow(Exception exception) {
if (exception instanceof InvocationTargetException || exception instanceof NoFallbackAvailableException) {
Throwable underlyingException = exception.getCause();
if (underlyingException instanceof RuntimeException) {
throw (RuntimeException) underlyingException;
}
if (underlyingException != null) {
throw new IllegalStateException(underlyingException);
}
throw new IllegalStateException(exception);
}
}
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
final Thread caller = Thread.currentThread();
return () -> {
boolean isAsync = caller != Thread.currentThread();
try {
if (isAsync) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
return dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
finally {
if (isAsync) {
RequestContextHolder.resetRequestAttributes();
}
}
};
}
/**
* If the method param of {@link InvocationHandler#invoke(Object, Method, Object[])}
* is not accessible, i.e in a package-private interface, the fallback call will cause
* of access restrictions. But methods in dispatch are copied methods. So setting
* access to dispatch method doesn't take effect to the method in
* InvocationHandler.invoke. Use map to store a copy of method to invoke the fallback
* to bypass this and reducing the count of reflection calls.
* @return cached methods map for fallback invoking
*/
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
Map<Method, Method> result = new LinkedHashMap<>();
for (Method method : dispatch.keySet()) {
method.setAccessible(true);
result.put(method, method);
}
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) {
PolarisFeignCircuitBreakerInvocationHandler other = (PolarisFeignCircuitBreakerInvocationHandler) obj;
return this.target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return this.target.hashCode();
}
@Override
public String toString() {
return this.target.toString();
}
}

@ -0,0 +1,103 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.feign;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClientFactory;
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.cloud.openfeign.Targeter;
import org.springframework.util.StringUtils;
/**
* PolarisFeignCircuitBreakerTargeter, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerTargeter}, but giving Polaris modification.
*
* @author sean yu
*/
public class PolarisFeignCircuitBreakerTargeter implements Targeter {
private final CircuitBreakerFactory circuitBreakerFactory;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
public PolarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) {
this.circuitBreakerFactory = circuitBreakerFactory;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
}
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory 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, FeignClientFactory 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, FeignClientFactory 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, FeignClientFactory context,
Class<?> beanType, Class<T> targetType) {
Object fallbackInstance = context.getInstance(feignClientName, beanType);
if (fallbackInstance == null) {
throw new IllegalStateException(
String.format("No " + fallbackMechanism + " instance of type %s found for feign client %s",
beanType, feignClientName));
}
if (!targetType.isAssignableFrom(beanType)) {
throw new IllegalStateException(String.format("Incompatible " + fallbackMechanism
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
beanType, targetType, feignClientName));
}
return (T) fallbackInstance;
}
private PolarisFeignCircuitBreaker.Builder builder(String feignClientName, PolarisFeignCircuitBreaker.Builder builder) {
return builder
.circuitBreakerFactory(circuitBreakerFactory)
.feignClientName(feignClientName)
.circuitBreakerNameResolver(circuitBreakerNameResolver);
}
}

@ -19,16 +19,18 @@ package com.tencent.cloud.polaris.circuitbreaker.gateway;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger; import reactor.core.publisher.Flux;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.InvalidPropertyException;
@ -40,11 +42,15 @@ import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.HttpStatusHolder; import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServiceUnavailableException; import org.springframework.cloud.gateway.support.ServiceUnavailableException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@ -55,6 +61,7 @@ import static java.util.Optional.ofNullable;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset;
@ -66,8 +73,6 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.r
*/ */
public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory { public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerFilterFactory.class);
private String routeIdPrefix; private String routeIdPrefix;
private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;
@ -151,6 +156,12 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
return Arrays.asList(allHttpStatus); return Arrays.asList(allHttpStatus);
} }
private Set<HttpStatus> getDefaultStatus() {
return Arrays.stream(HttpStatus.values())
.filter(HttpStatus::is5xxServerError)
.collect(Collectors.toSet());
}
@Override @Override
public GatewayFilter apply(Config config) { public GatewayFilter apply(Config config) {
Set<HttpStatus> statuses = config.getStatusCodes().stream() Set<HttpStatus> statuses = config.getStatusCodes().stream()
@ -166,40 +177,82 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (CollectionUtils.isEmpty(statuses)) {
statuses.addAll(getDefaultStatus());
}
String circuitBreakerId = getCircuitBreakerId(config); String circuitBreakerId = getCircuitBreakerId(config);
return new GatewayFilter() { return new GatewayFilter() {
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
String serviceName = circuitBreakerId;
if (route != null) {
serviceName = route.getUri().getHost();
}
String path = exchange.getRequest().getPath().value(); String path = exchange.getRequest().getPath().value();
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(circuitBreakerId + "#" + path); ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(serviceName + "#" + path);
return cb.run(chain.filter(exchange).doOnSuccess(v -> { return cb.run(
if (statuses.contains(exchange.getResponse().getStatusCode())) { chain.filter(exchange)
HttpStatusCode status = exchange.getResponse().getStatusCode(); .doOnSuccess(v -> {
throw new CircuitBreakerStatusCodeException(status); // throw CircuitBreakerStatusCodeException by default for all need checking status
} // so polaris can report right error status
}), t -> { Set<HttpStatus> statusNeedToCheck = new HashSet<>();
if (config.getFallbackUri() == null) { statusNeedToCheck.addAll(statuses);
return Mono.error(t); statusNeedToCheck.addAll(getDefaultStatus());
} HttpStatusCode status = exchange.getResponse().getStatusCode();
if (statusNeedToCheck.contains(HttpStatus.resolve(status.value()))) {
exchange.getResponse().setStatusCode(null); throw new CircuitBreakerStatusCodeException(status);
reset(exchange); }
}),
t -> {
// pre-check CircuitBreakerStatusCodeException's status matches input status
if (t instanceof CircuitBreakerStatusCodeException) {
HttpStatusCode status = ((CircuitBreakerStatusCodeException) t).getStatusCode();
// no need to fallback
if (!statuses.contains(HttpStatus.resolve(status.value()))) {
return Mono.error(t);
}
}
// do fallback
if (config.getFallbackUri() == null) {
// polaris checking
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(fallbackInfo.getCode());
if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders().forEach((k, v) -> response.getHeaders().add(k, v));
}
DataBuffer bodyBuffer = null;
if (fallbackInfo.getBody() != null) {
byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8);
bodyBuffer = response.bufferFactory().wrap(bytes);
}
return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete();
}
}
return Mono.error(t);
}
exchange.getResponse().setStatusCode(null);
reset(exchange);
// TODO: copied from RouteToRequestUrlFilter // TODO: copied from RouteToRequestUrlFilter
URI uri = exchange.getRequest().getURI(); URI uri = exchange.getRequest().getURI();
// TODO: assume always? // TODO: assume always?
boolean encoded = containsEncodedParts(uri); boolean encoded = containsEncodedParts(uri);
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null) URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
.uri(config.getFallbackUri()).scheme(null).build(encoded).toUri(); .uri(config.getFallbackUri()).scheme(null).build(encoded).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
addExceptionDetails(t, exchange); addExceptionDetails(t, exchange);
// Reset the exchange // Reset the exchange
reset(exchange); reset(exchange);
ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build(); ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build();
return getDispatcherHandler().handle(exchange.mutate().request(request).build()); return getDispatcherHandler().handle(exchange.mutate().request(request).build());
}).onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError())); })
.onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError()));
} }
@Override @Override
@ -217,9 +270,11 @@ public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreake
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t)); return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
} }
if (t instanceof CallAbortedException) { if (t instanceof CallAbortedException) {
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", t.getMessage());
return Mono.error(new ServiceUnavailableException()); return Mono.error(new ServiceUnavailableException());
} }
if (t instanceof CircuitBreakerStatusCodeException) {
return Mono.empty();
}
if (resumeWithoutError) { if (resumeWithoutError) {
return Mono.empty(); return Mono.empty();
} }

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

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

@ -0,0 +1,105 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.AbstractClientHttpResponse;
import static com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER;
/**
* PolarisCircuitBreakerHttpResponse.
*
* @author sean yu
*/
public class PolarisCircuitBreakerHttpResponse extends AbstractClientHttpResponse {
private final CircuitBreakerStatus.FallbackInfo fallbackInfo;
private HttpHeaders headers = new HttpHeaders();
private InputStream body;
public PolarisCircuitBreakerHttpResponse(int code) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, null));
}
public PolarisCircuitBreakerHttpResponse(int code, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, null, body));
}
public PolarisCircuitBreakerHttpResponse(int code, Map<String, String> headers, String body) {
this(new CircuitBreakerStatus.FallbackInfo(code, headers, body));
}
PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) {
this.fallbackInfo = fallbackInfo;
headers.add(POLARIS_CIRCUIT_BREAKER_FALLBACK_HEADER, "true");
if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders().forEach(headers::add);
}
if (fallbackInfo.getBody() != null) {
body = new ByteArrayInputStream(fallbackInfo.getBody().getBytes());
}
}
@Override
public final int getRawStatusCode() {
return fallbackInfo.getCode();
}
@Override
public final String getStatusText() {
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
return (status != null ? status.getReasonPhrase() : "");
}
@Override
public final void close() {
if (this.body != null) {
try {
this.body.close();
}
catch (IOException e) {
// Ignore exception on close...
}
}
}
@Override
public final InputStream getBody() {
return this.body;
}
@Override
public final HttpHeaders getHeaders() {
return this.headers;
}
public CircuitBreakerStatus.FallbackInfo getFallbackInfo() {
return this.fallbackInfo;
}
}

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

@ -0,0 +1,111 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.resttemplate;
import java.io.IOException;
import java.lang.reflect.Method;
import com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter;
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import static com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.HEADER_HAS_ERROR;
/**
* PolarisCircuitBreakerRestTemplateInterceptor.
*
* @author sean yu
*/
public class PolarisCircuitBreakerRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final PolarisCircuitBreaker polarisCircuitBreaker;
private final ApplicationContext applicationContext;
private final CircuitBreakerFactory circuitBreakerFactory;
private final RestTemplate restTemplate;
public PolarisCircuitBreakerRestTemplateInterceptor(
PolarisCircuitBreaker polarisCircuitBreaker,
ApplicationContext applicationContext,
CircuitBreakerFactory circuitBreakerFactory,
RestTemplate restTemplate
) {
this.polarisCircuitBreaker = polarisCircuitBreaker;
this.applicationContext = applicationContext;
this.circuitBreakerFactory = circuitBreakerFactory;
this.restTemplate = restTemplate;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
return circuitBreakerFactory.create(request.getURI().getHost() + "#" + request.getURI().getPath()).run(
() -> {
try {
ClientHttpResponse response = execution.execute(request, body);
// pre handle response error
// EnhancedRestTemplateReporter always return true,
// so we need to check header set by EnhancedRestTemplateReporter
ResponseErrorHandler errorHandler = restTemplate.getErrorHandler();
boolean hasError = errorHandler.hasError(response);
if (errorHandler instanceof EnhancedRestTemplateReporter) {
hasError = Boolean.parseBoolean(response.getHeaders().getFirst(HEADER_HAS_ERROR));
}
if (hasError) {
errorHandler.handleError(request.getURI(), request.getMethod(), response);
}
return response;
}
catch (IOException e) {
throw new IllegalStateException(e);
}
},
t -> {
if (StringUtils.hasText(polarisCircuitBreaker.fallback())) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, null, polarisCircuitBreaker.fallback());
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
}
if (!PolarisCircuitBreakerFallback.class.toGenericString().equals(polarisCircuitBreaker.fallbackClass().toGenericString())) {
Method method = ReflectionUtils.findMethod(PolarisCircuitBreakerFallback.class, "fallback");
PolarisCircuitBreakerFallback polarisCircuitBreakerFallback = applicationContext.getBean(polarisCircuitBreaker.fallbackClass());
return (PolarisCircuitBreakerHttpResponse) ReflectionUtils.invokeMethod(method, polarisCircuitBreakerFallback);
}
if (t instanceof CallAbortedException) {
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
if (fallbackInfo != null) {
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
}
}
throw new IllegalStateException(t);
}
);
}
}

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

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

@ -0,0 +1,322 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.stream.Collectors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerFallback;
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter;
import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
import com.tencent.polaris.client.util.Utils;
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
import com.tencent.polaris.test.common.TestUtils;
import com.tencent.polaris.test.mock.discovery.NamingServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
import static com.tencent.cloud.rpc.enhancement.resttemplate.EnhancedRestTemplateReporter.HEADER_HAS_ERROR;
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
/**
* @author sean yu
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT,
classes = PolarisCircuitBreakerIntegrationTest.TestConfig.class,
properties = {
"spring.cloud.gateway.enabled=false",
"feign.circuitbreaker.enabled=true",
"spring.cloud.polaris.namespace=default",
"spring.cloud.polaris.service=test"
})
@DirtiesContext
public class PolarisCircuitBreakerIntegrationTest {
private static final String TEST_SERVICE_NAME = "test-service-callee";
private static NamingServer namingServer;
@AfterAll
public static void afterAll() {
if (null != namingServer) {
namingServer.terminate();
}
}
@Autowired
@Qualifier("defaultRestTemplate")
private RestTemplate defaultRestTemplate;
@Autowired
@Qualifier("restTemplateFallbackFromPolaris")
private RestTemplate restTemplateFallbackFromPolaris;
@Autowired
@Qualifier("restTemplateFallbackFromCode")
private RestTemplate restTemplateFallbackFromCode;
@Autowired
@Qualifier("restTemplateFallbackFromCode2")
private RestTemplate restTemplateFallbackFromCode2;
@Autowired
@Qualifier("restTemplateFallbackFromCode3")
private RestTemplate restTemplateFallbackFromCode3;
@Autowired
@Qualifier("restTemplateFallbackFromCode4")
private RestTemplate restTemplateFallbackFromCode4;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testRestTemplate() throws URISyntaxException {
MockRestServiceServer mockServer = MockRestServiceServer.createServer(defaultRestTemplate);
mockServer
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK).body("OK"));
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("OK");
mockServer.verify();
mockServer.reset();
HttpHeaders headers = new HttpHeaders();
headers.add(HEADER_HAS_ERROR, "true");
// no delegateHandler in EnhancedRestTemplateReporter, so this will except err
mockServer
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.BAD_GATEWAY).headers(headers).body("BAD_GATEWAY"));
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("BAD_GATEWAY");
mockServer.verify();
mockServer.reset();
assertThat(restTemplateFallbackFromCode.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
Utils.sleepUninterrupted(2000);
assertThat(restTemplateFallbackFromCode2.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
Utils.sleepUninterrupted(2000);
assertThat(restTemplateFallbackFromCode3.getForEntity("/example/service/b/info", String.class).getStatusCode()).isEqualTo(HttpStatus.OK);
Utils.sleepUninterrupted(2000);
assertThat(restTemplateFallbackFromCode4.getForObject("/example/service/b/info", String.class)).isEqualTo("fallback");
Utils.sleepUninterrupted(2000);
assertThat(restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class)).isEqualTo("\"fallback from polaris server\"");
// just for code coverage
PolarisCircuitBreakerHttpResponse response = ((CustomPolarisCircuitBreakerFallback) applicationContext.getBean("customPolarisCircuitBreakerFallback")).fallback();
assertThat(response.getStatusText()).isEqualTo("OK");
assertThat(response.getFallbackInfo().getCode()).isEqualTo(200);
}
@Configuration
@EnableAutoConfiguration
@ImportAutoConfiguration({ PolarisCircuitBreakerFeignClientAutoConfiguration.class })
@EnableFeignClients
public static class TestConfig {
@Bean
@PolarisCircuitBreaker(fallback = "fallback")
public RestTemplate defaultRestTemplate(RpcEnhancementReporterProperties properties, ConsumerAPI consumerAPI) {
RestTemplate defaultRestTemplate = new RestTemplate();
EnhancedRestTemplateReporter enhancedRestTemplateReporter = new EnhancedRestTemplateReporter(properties, consumerAPI);
defaultRestTemplate.setErrorHandler(enhancedRestTemplateReporter);
return defaultRestTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker
public RestTemplate restTemplateFallbackFromPolaris() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback.class)
public RestTemplate restTemplateFallbackFromCode() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback2.class)
public RestTemplate restTemplateFallbackFromCode2() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback3.class)
public RestTemplate restTemplateFallbackFromCode3() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
@LoadBalanced
@PolarisCircuitBreaker(fallback = "fallback")
public RestTemplate restTemplateFallbackFromCode4() {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(uriBuilderFactory);
return restTemplate;
}
@Bean
public CustomPolarisCircuitBreakerFallback customPolarisCircuitBreakerFallback() {
return new CustomPolarisCircuitBreakerFallback();
}
@Bean
public CustomPolarisCircuitBreakerFallback2 customPolarisCircuitBreakerFallback2() {
return new CustomPolarisCircuitBreakerFallback2();
}
@Bean
public CustomPolarisCircuitBreakerFallback3 customPolarisCircuitBreakerFallback3() {
return new CustomPolarisCircuitBreakerFallback3();
}
@Bean
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
try {
namingServer = NamingServer.startNamingServer(10081);
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
}
catch (IOException e) {
}
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader().getResourceAsStream("circuitBreakerRule.json");
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder().addRules(circuitBreakerRule).build();
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
}
@RestController
@RequestMapping("/example/service/b")
public class ServiceBController {
/**
* Get service information.
*
* @return service information
*/
@GetMapping("/info")
public String info() {
return "hello world ! I'm a service B1";
}
}
}
public static class CustomPolarisCircuitBreakerFallback implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200,
new HashMap<String, String>() {{
put("xxx", "xxx");
}},
"\"this is a fallback class\"");
}
}
public static class CustomPolarisCircuitBreakerFallback2 implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200,
"\"this is a fallback class\""
);
}
}
public static class CustomPolarisCircuitBreakerFallback3 implements PolarisCircuitBreakerFallback {
@Override
public PolarisCircuitBreakerHttpResponse fallback() {
return new PolarisCircuitBreakerHttpResponse(
200
);
}
}
}

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

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

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

@ -40,7 +40,7 @@ public class PolarisRouterAutoConfigurationTest {
PolarisLoadBalancerTest.class, PolarisLoadBalancerTest.class,
PolarisContextAutoConfiguration.class, PolarisContextAutoConfiguration.class,
PolarisLoadBalancerAutoConfiguration.class)) PolarisLoadBalancerAutoConfiguration.class))
.withPropertyValues("spring.cloud.loadbalancer.configurations=polaris"); .withPropertyValues("spring.cloud.loadbalancer.configurations=polaris", "spring.application.name=test");
private final ApplicationContextRunner noPolarisContextRunner = new ApplicationContextRunner() private final ApplicationContextRunner noPolarisContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of( .withConfiguration(AutoConfigurations.of(

@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Haotian Zhang * @author Haotian Zhang
*/ */
@ExtendWith(SpringExtension.class) @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 { public class OkHttpUtilTest {
@LocalServerPort @LocalServerPort

@ -19,6 +19,11 @@
<groupId>com.tencent.cloud</groupId> <groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-rpc-enhancement</artifactId> <artifactId>spring-cloud-tencent-rpc-enhancement</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-metadata-transfer</artifactId>
</dependency>
<!-- Spring Cloud Tencent dependencies end --> <!-- Spring Cloud Tencent dependencies end -->
<!-- Polaris dependencies start --> <!-- Polaris dependencies start -->

@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils;
* *
*@author lepdou 2022-05-13 *@author lepdou 2022-05-13
*/ */
@Deprecated
public class RateLimitRuleLabelResolver { public class RateLimitRuleLabelResolver {
private final ServiceRuleManager serviceRuleManager; 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.ServiceRuleManager;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; 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.QuotaCheckReactiveFilter;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter; 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.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; 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.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.factory.LimitAPIFactory; 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.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 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.context.annotation.Configuration;
import org.springframework.lang.Nullable; 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 com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter.QUOTA_FILTER_BEAN_NAME;
import static jakarta.servlet.DispatcherType.ASYNC; import static jakarta.servlet.DispatcherType.ASYNC;
import static jakarta.servlet.DispatcherType.ERROR; import static jakarta.servlet.DispatcherType.ERROR;
@ -62,11 +64,6 @@ public class PolarisRateLimitAutoConfiguration {
return LimitAPIFactory.createLimitAPIByContext(polarisContext); return LimitAPIFactory.createLimitAPIByContext(polarisContext);
} }
@Bean
public RateLimitRuleLabelResolver rateLimitRuleLabelService(ServiceRuleManager serviceRuleManager) {
return new RateLimitRuleLabelResolver(serviceRuleManager);
}
/** /**
* Create when web application type is SERVLET. * Create when web application type is SERVLET.
*/ */
@ -74,15 +71,19 @@ public class PolarisRateLimitAutoConfiguration {
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
protected static class QuotaCheckFilterConfig { protected static class QuotaCheckFilterConfig {
@Bean
public RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver(ServiceRuleManager serviceRuleManager,
@Autowired(required = false) PolarisRateLimiterLabelServletResolver labelResolver) {
return new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolver);
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public QuotaCheckServletFilter quotaCheckFilter(LimitAPI limitAPI, public QuotaCheckServletFilter quotaCheckFilter(LimitAPI limitAPI,
@Nullable PolarisRateLimiterLabelServletResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties, PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver, RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { @Autowired(required = false) PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
return new QuotaCheckServletFilter(limitAPI, labelResolver, return new QuotaCheckServletFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentResolver, polarisRateLimiterLimitedFallback);
polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
} }
@Bean @Bean
@ -92,9 +93,11 @@ public class PolarisRateLimitAutoConfiguration {
quotaCheckServletFilter); quotaCheckServletFilter);
registrationBean.setDispatcherTypes(ASYNC, ERROR, FORWARD, INCLUDE, REQUEST); registrationBean.setDispatcherTypes(ASYNC, ERROR, FORWARD, INCLUDE, REQUEST);
registrationBean.setName(QUOTA_FILTER_BEAN_NAME); registrationBean.setName(QUOTA_FILTER_BEAN_NAME);
registrationBean.setOrder(RateLimitConstant.FILTER_ORDER); registrationBean.setOrder(FILTER_ORDER);
return registrationBean; return registrationBean;
} }
} }
/** /**
@ -104,14 +107,18 @@ public class PolarisRateLimitAutoConfiguration {
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
protected static class MetadataReactiveFilterConfig { protected static class MetadataReactiveFilterConfig {
@Bean
public RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver(ServiceRuleManager serviceRuleManager,
@Autowired(required = false) PolarisRateLimiterLabelReactiveResolver labelResolver) {
return new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolver);
}
@Bean @Bean
public QuotaCheckReactiveFilter quotaCheckReactiveFilter(LimitAPI limitAPI, public QuotaCheckReactiveFilter quotaCheckReactiveFilter(LimitAPI limitAPI,
@Nullable PolarisRateLimiterLabelReactiveResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties, PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver, RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
return new QuotaCheckReactiveFilter(limitAPI, labelResolver, return new QuotaCheckReactiveFilter(limitAPI, polarisRateLimitProperties, rateLimitRuleArgumentResolver, polarisRateLimiterLimitedFallback);
polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback);
} }
} }
} }

@ -20,26 +20,21 @@ package com.tencent.cloud.polaris.ratelimit.filter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.google.common.collect.Maps;
import com.tencent.cloud.common.metadata.MetadataContext; 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.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; 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.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils; import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils;
import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils; import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; 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.QuotaResponse;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode; import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -53,8 +48,6 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LABEL_METHOD;
/** /**
* Reactive filter to check quota. * Reactive filter to check quota.
* *
@ -66,11 +59,9 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
private final LimitAPI limitAPI; private final LimitAPI limitAPI;
private final PolarisRateLimiterLabelReactiveResolver labelResolver;
private final PolarisRateLimitProperties polarisRateLimitProperties; private final PolarisRateLimitProperties polarisRateLimitProperties;
private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver; private final RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver;
private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
@ -78,14 +69,12 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
private String rejectTips; private String rejectTips;
public QuotaCheckReactiveFilter(LimitAPI limitAPI, public QuotaCheckReactiveFilter(LimitAPI limitAPI,
PolarisRateLimiterLabelReactiveResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties, PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver, RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
this.limitAPI = limitAPI; this.limitAPI = limitAPI;
this.labelResolver = labelResolver;
this.polarisRateLimitProperties = polarisRateLimitProperties; this.polarisRateLimitProperties = polarisRateLimitProperties;
this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver; this.rateLimitRuleArgumentResolver = rateLimitRuleArgumentResolver;
this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback; this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback;
} }
@ -104,12 +93,12 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
String localNamespace = MetadataContext.LOCAL_NAMESPACE; String localNamespace = MetadataContext.LOCAL_NAMESPACE;
String localService = MetadataContext.LOCAL_SERVICE; String localService = MetadataContext.LOCAL_SERVICE;
Map<String, String> labels = getRequestLabels(exchange, localNamespace, localService); Set<Argument> arguments = rateLimitRuleArgumentResolver.getArguments(exchange, localNamespace, localService);
long waitMs = -1; long waitMs = -1;
try { try {
String path = exchange.getRequest().getURI().getPath(); String path = exchange.getRequest().getURI().getPath();
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota( QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(
limitAPI, localNamespace, localService, 1, labels, path); limitAPI, localNamespace, localService, 1, arguments, path);
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
@ -148,40 +137,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,22 +19,18 @@
package com.tencent.cloud.polaris.ratelimit.filter; package com.tencent.cloud.polaris.ratelimit.filter;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.tencent.cloud.common.metadata.MetadataContext; 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.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; 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.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils; import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils;
import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils; import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; 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.QuotaResponse;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode; import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@ -42,7 +38,6 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -52,8 +47,6 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LABEL_METHOD;
/** /**
* Servlet filter to check quota. * Servlet filter to check quota.
* *
@ -69,25 +62,21 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class); private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class);
private final LimitAPI limitAPI; private final LimitAPI limitAPI;
private final PolarisRateLimiterLabelServletResolver labelResolver;
private final PolarisRateLimitProperties polarisRateLimitProperties; private final PolarisRateLimitProperties polarisRateLimitProperties;
private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver; private final RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver;
private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback;
private String rejectTips; private String rejectTips;
public QuotaCheckServletFilter(LimitAPI limitAPI, public QuotaCheckServletFilter(LimitAPI limitAPI,
PolarisRateLimiterLabelServletResolver labelResolver,
PolarisRateLimitProperties polarisRateLimitProperties, PolarisRateLimitProperties polarisRateLimitProperties,
RateLimitRuleLabelResolver rateLimitRuleLabelResolver, RateLimitRuleArgumentServletResolver rateLimitRuleArgumentResolver,
@Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) {
this.limitAPI = limitAPI; this.limitAPI = limitAPI;
this.labelResolver = labelResolver;
this.polarisRateLimitProperties = polarisRateLimitProperties; this.polarisRateLimitProperties = polarisRateLimitProperties;
this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver; this.rateLimitRuleArgumentResolver = rateLimitRuleArgumentResolver;
this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback; this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback;
} }
@ -98,16 +87,15 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) @NonNull FilterChain filterChain) throws ServletException, IOException {
throws ServletException, IOException {
String localNamespace = MetadataContext.LOCAL_NAMESPACE; String localNamespace = MetadataContext.LOCAL_NAMESPACE;
String localService = MetadataContext.LOCAL_SERVICE; String localService = MetadataContext.LOCAL_SERVICE;
Map<String, String> labels = getRequestLabels(request, localNamespace, localService); Set<Argument> arguments = rateLimitRuleArgumentResolver.getArguments(request, localNamespace, localService);
try { try {
QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI, QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI,
localNamespace, localService, 1, labels, request.getRequestURI()); localNamespace, localService, 1, arguments, request.getRequestURI());
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
if (!Objects.isNull(polarisRateLimiterLimitedFallback)) { if (!Objects.isNull(polarisRateLimiterLimitedFallback)) {
@ -139,40 +127,4 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); 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,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.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,122 @@
/*
* 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 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 jakarta.servlet.http.HttpServletRequest;
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; package com.tencent.cloud.polaris.ratelimit.utils;
import java.util.Map; import java.util.Map;
import java.util.Set;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; 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.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -38,6 +40,7 @@ public final class QuotaCheckUtils {
private QuotaCheckUtils() { private QuotaCheckUtils() {
} }
@Deprecated
public static QuotaResponse getQuota(LimitAPI limitAPI, String namespace, String service, int count, public static QuotaResponse getQuota(LimitAPI limitAPI, String namespace, String service, int count,
Map<String, String> labels, String method) { Map<String, String> labels, String method) {
// build quota request // build quota request
@ -56,4 +59,23 @@ public final class QuotaCheckUtils {
return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "get quota failed")); 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; package com.tencent.cloud.polaris.ratelimit.config;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; 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.QuotaCheckReactiveFilter;
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter; 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 com.tencent.polaris.ratelimit.api.core.LimitAPI;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -54,7 +55,8 @@ public class PolarisRateLimitAutoConfigurationTest {
PolarisRateLimitAutoConfiguration.class)) PolarisRateLimitAutoConfiguration.class))
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class); 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(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class); assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class);
assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); assertThat(context).doesNotHaveBean(FilterRegistrationBean.class);
@ -72,12 +74,13 @@ public class PolarisRateLimitAutoConfigurationTest {
PolarisRateLimitAutoConfiguration.class)) PolarisRateLimitAutoConfiguration.class))
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class); assertThat(context).hasSingleBean(LimitAPI.class);
assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class); assertThat(context).hasSingleBean(RateLimitRuleArgumentServletResolver.class);
assertThat(context).hasSingleBean(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class); assertThat(context).hasSingleBean(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).hasSingleBean(QuotaCheckServletFilter.class); assertThat(context).hasSingleBean(QuotaCheckServletFilter.class);
assertThat(context).hasSingleBean(FilterRegistrationBean.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(QuotaCheckReactiveFilter.class);
assertThat(context).doesNotHaveBean(RateLimitRuleArgumentReactiveResolver.class);
}); });
} }
@ -90,7 +93,8 @@ public class PolarisRateLimitAutoConfigurationTest {
PolarisRateLimitAutoConfiguration.class)) PolarisRateLimitAutoConfiguration.class))
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(LimitAPI.class); 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(PolarisRateLimitAutoConfiguration.QuotaCheckFilterConfig.class);
assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class); assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class);
assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); assertThat(context).doesNotHaveBean(FilterRegistrationBean.class);

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

@ -17,32 +17,32 @@
package com.tencent.cloud.polaris.ratelimit.filter; 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.Field;
import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets;
import java.lang.reflect.Method;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CountDownLatch; 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.metadata.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.polaris.context.ServiceRuleManager;
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.config.PolarisRateLimitProperties;
import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; 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.PolarisRateLimiterLabelReactiveResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import org.junit.jupiter.api.AfterAll; import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; 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.assertThat;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@ -76,36 +74,15 @@ import static org.mockito.Mockito.when;
@SpringBootTest(classes = QuotaCheckReactiveFilterTest.TestApplication.class, @SpringBootTest(classes = QuotaCheckReactiveFilterTest.TestApplication.class,
properties = {"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"}) properties = {"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"})
public class QuotaCheckReactiveFilterTest { public class QuotaCheckReactiveFilterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<SpringWebExpressionLabelUtils> expressionLabelUtilsMockedStatic;
private final PolarisRateLimiterLabelReactiveResolver labelResolver = private final PolarisRateLimiterLabelReactiveResolver labelResolver =
exchange -> Collections.singletonMap("ReactiveResolver", "ReactiveResolver"); exchange -> Collections.singletonMap("xxx", "xxx");
private QuotaCheckReactiveFilter quotaCheckReactiveFilter; private QuotaCheckReactiveFilter quotaCheckReactiveFilter;
private QuotaCheckReactiveFilter quotaCheckWithRateLimiterLimitedFallbackReactiveFilter; private QuotaCheckReactiveFilter quotaCheckWithRateLimiterLimitedFallbackReactiveFilter;
private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; 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 @BeforeEach
void setUp() { void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST"; MetadataContext.LOCAL_NAMESPACE = "TEST";
polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback();
LimitAPI limitAPI = mock(LimitAPI.class); LimitAPI limitAPI = mock(LimitAPI.class);
when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> { when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> {
@ -128,14 +105,24 @@ public class QuotaCheckReactiveFilterTest {
polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips提示消息"); polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips提示消息");
polarisRateLimitProperties.setRejectHttpCode(419); polarisRateLimitProperties.setRejectHttpCode(419);
RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class); PolarisRateLimitProperties polarisRateLimitWithHtmlRejectTipsProperties = new PolarisRateLimitProperties();
when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())) polarisRateLimitWithHtmlRejectTipsProperties.setRejectRequestTips("<h1>RejectRequestTips提示消息</h1>");
.thenReturn(Collections.emptySet()); polarisRateLimitWithHtmlRejectTipsProperties.setRejectHttpCode(419);
this.quotaCheckReactiveFilter = new QuotaCheckReactiveFilter( ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null);
this.quotaCheckWithRateLimiterLimitedFallbackReactiveFilter = new QuotaCheckReactiveFilter( RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); 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 @Test
@ -156,42 +143,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 @Test
public void testFilter() { public void testFilter() {
// Create mock WebFilterChain // Create mock WebFilterChain
@ -199,19 +150,20 @@ public class QuotaCheckReactiveFilterTest {
// Mock request // Mock request
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build(); MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.init(); quotaCheckReactiveFilter.init();
// Pass // Pass
MetadataContext.LOCAL_SERVICE = "TestApp1"; MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckReactiveFilter.filter(exchange, webFilterChain); ServerWebExchange testApp1Exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.filter(testApp1Exchange, webFilterChain);
// Unirate waiting 1000ms // Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2"; MetadataContext.LOCAL_SERVICE = "TestApp2";
ServerWebExchange testApp2Exchange = MockServerWebExchange.from(request);
long startTimestamp = System.currentTimeMillis(); long startTimestamp = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch countDownLatch = new CountDownLatch(1);
quotaCheckReactiveFilter.filter(exchange, webFilterChain).subscribe(e -> { quotaCheckReactiveFilter.filter(testApp2Exchange, webFilterChain).subscribe(e -> {
}, t -> { }, t -> {
}, countDownLatch::countDown); }, countDownLatch::countDown);
try { try {
@ -224,14 +176,16 @@ public class QuotaCheckReactiveFilterTest {
// Rate limited // Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3"; MetadataContext.LOCAL_SERVICE = "TestApp3";
quotaCheckReactiveFilter.filter(exchange, webFilterChain); ServerWebExchange testApp3Exchange = MockServerWebExchange.from(request);
ServerHttpResponse response = exchange.getResponse(); quotaCheckReactiveFilter.filter(testApp3Exchange, webFilterChain);
ServerHttpResponse response = testApp3Exchange.getResponse();
assertThat(response.getRawStatusCode()).isEqualTo(419); assertThat(response.getRawStatusCode()).isEqualTo(419);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE);
// Exception // Exception
MetadataContext.LOCAL_SERVICE = "TestApp4"; MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckReactiveFilter.filter(exchange, webFilterChain); ServerWebExchange testApp4Exchange = MockServerWebExchange.from(request);
quotaCheckReactiveFilter.filter(testApp4Exchange, webFilterChain);
} }
@Test @Test

@ -17,52 +17,46 @@
package com.tencent.cloud.polaris.ratelimit.filter; package com.tencent.cloud.polaris.ratelimit.filter;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets;
import java.lang.reflect.Method;
import java.util.Collections; import java.util.Collections;
import java.util.Map; 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.MetadataContext;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.polaris.context.ServiceRuleManager;
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.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.PolarisRateLimiterLabelServletResolver;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback;
import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse;
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import 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.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; 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.assertThat;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@ -70,41 +64,23 @@ import static org.mockito.Mockito.when;
* *
* @author Haotian Zhang, cheese8 * @author Haotian Zhang, cheese8
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(SpringExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
@SpringBootTest(classes = QuotaCheckServletFilterTest.TestApplication.class, properties = { classes = QuotaCheckServletFilterTest.TestApplication.class,
properties = {
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp" "spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
}) })
public class QuotaCheckServletFilterTest { public class QuotaCheckServletFilterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<SpringWebExpressionLabelUtils> expressionLabelUtilsMockedStatic;
private final PolarisRateLimiterLabelServletResolver labelResolver = private final PolarisRateLimiterLabelServletResolver labelResolver =
exchange -> Collections.singletonMap("ServletResolver", "ServletResolver"); exchange -> Collections.singletonMap("xxx", "xxx");
private QuotaCheckServletFilter quotaCheckServletFilter; private QuotaCheckServletFilter quotaCheckServletFilter;
private QuotaCheckServletFilter quotaCheckWithHtmlRejectTipsServletFilter; private QuotaCheckServletFilter quotaCheckWithHtmlRejectTipsServletFilter;
private QuotaCheckServletFilter quotaCheckWithRateLimiterLimitedFallbackFilter; private QuotaCheckServletFilter quotaCheckWithRateLimiterLimitedFallbackFilter;
private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; 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 @BeforeEach
void setUp() { void setUp() throws InvalidProtocolBufferException {
MetadataContext.LOCAL_NAMESPACE = "TEST"; MetadataContext.LOCAL_NAMESPACE = "TEST";
LimitAPI limitAPI = mock(LimitAPI.class); LimitAPI limitAPI = mock(LimitAPI.class);
@ -132,15 +108,21 @@ public class QuotaCheckServletFilterTest {
polarisRateLimitWithHtmlRejectTipsProperties.setRejectRequestTips("<h1>RejectRequestTips提示消息</h1>"); polarisRateLimitWithHtmlRejectTipsProperties.setRejectRequestTips("<h1>RejectRequestTips提示消息</h1>");
polarisRateLimitWithHtmlRejectTipsProperties.setRejectHttpCode(419); polarisRateLimitWithHtmlRejectTipsProperties.setRejectHttpCode(419);
RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class); ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())).thenReturn(Collections.emptySet());
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null); InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader().getResourceAsStream("ratelimit.json");
this.quotaCheckWithHtmlRejectTipsServletFilter = new QuotaCheckServletFilter( String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, null); JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback(); RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
this.quotaCheckWithRateLimiterLimitedFallbackFilter = new QuotaCheckServletFilter( RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
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 @Test
@ -166,40 +148,6 @@ public class QuotaCheckServletFilterTest {
quotaCheckWithRateLimiterLimitedFallbackFilter.init(); 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 @Test
public void testDoFilterInternal() { public void testDoFilterInternal() {
// Create mock FilterChain // Create mock FilterChain
@ -209,33 +157,38 @@ public class QuotaCheckServletFilterTest {
// Mock request // Mock request
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
quotaCheckServletFilter.init(); quotaCheckServletFilter.init();
quotaCheckWithHtmlRejectTipsServletFilter.init();
try { try {
// Pass // Pass
MetadataContext.LOCAL_SERVICE = "TestApp1"; MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain); MockHttpServletResponse testApp1Response = new MockHttpServletResponse();
quotaCheckServletFilter.doFilterInternal(request, testApp1Response, filterChain);
// Unirate waiting 1000ms // Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2"; MetadataContext.LOCAL_SERVICE = "TestApp2";
MockHttpServletResponse testApp2Response = new MockHttpServletResponse();
long startTimestamp = System.currentTimeMillis(); long startTimestamp = System.currentTimeMillis();
quotaCheckServletFilter.doFilterInternal(request, response, filterChain); quotaCheckServletFilter.doFilterInternal(request, testApp2Response, filterChain);
assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L);
// Rate limited // Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3"; MetadataContext.LOCAL_SERVICE = "TestApp3";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain); MockHttpServletResponse testApp3Response = new MockHttpServletResponse();
assertThat(response.getStatus()).isEqualTo(419); quotaCheckServletFilter.doFilterInternal(request, testApp3Response, filterChain);
assertThat(response.getContentAsString()).isEqualTo("RejectRequestTips提示消息"); assertThat(testApp3Response.getStatus()).isEqualTo(419);
assertThat(testApp3Response.getContentAsString()).isEqualTo("RejectRequestTips提示消息");
quotaCheckWithHtmlRejectTipsServletFilter.doFilterInternal(request, response, filterChain); MockHttpServletResponse testApp3Response2 = new MockHttpServletResponse();
assertThat(response.getStatus()).isEqualTo(419); quotaCheckWithHtmlRejectTipsServletFilter.doFilterInternal(request, testApp3Response2, filterChain);
assertThat(response.getContentAsString()).isEqualTo("RejectRequestTips提示消息"); assertThat(testApp3Response2.getStatus()).isEqualTo(419);
assertThat(testApp3Response2.getContentAsString()).isEqualTo("<h1>RejectRequestTips提示消息</h1>");
// Exception // Exception
MockHttpServletResponse testApp4Response = new MockHttpServletResponse();
MetadataContext.LOCAL_SERVICE = "TestApp4"; MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckServletFilter.doFilterInternal(request, response, filterChain); quotaCheckServletFilter.doFilterInternal(request, testApp4Response, filterChain);
} }
catch (ServletException | IOException e) { catch (ServletException | IOException e) {
fail("Exception encountered.", e); fail("Exception encountered.", e);
@ -250,31 +203,34 @@ public class QuotaCheckServletFilterTest {
// Mock request // Mock request
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.init(); quotaCheckWithRateLimiterLimitedFallbackFilter.init();
try { try {
// Pass // Pass
MetadataContext.LOCAL_SERVICE = "TestApp1"; MetadataContext.LOCAL_SERVICE = "TestApp1";
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); MockHttpServletResponse testApp1response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp1response, filterChain);
// Unirate waiting 1000ms // Unirate waiting 1000ms
MetadataContext.LOCAL_SERVICE = "TestApp2"; MetadataContext.LOCAL_SERVICE = "TestApp2";
MockHttpServletResponse testApp2response = new MockHttpServletResponse();
long startTimestamp = System.currentTimeMillis(); long startTimestamp = System.currentTimeMillis();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp2response, filterChain);
assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L);
// Rate limited // Rate limited
MetadataContext.LOCAL_SERVICE = "TestApp3"; MetadataContext.LOCAL_SERVICE = "TestApp3";
MockHttpServletResponse testApp3response = new MockHttpServletResponse();
String contentType = new MediaType(polarisRateLimiterLimitedFallback.mediaType(), polarisRateLimiterLimitedFallback.charset()).toString(); String contentType = new MediaType(polarisRateLimiterLimitedFallback.mediaType(), polarisRateLimiterLimitedFallback.charset()).toString();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp3response, filterChain);
assertThat(response.getStatus()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode()); assertThat(testApp3response.getStatus()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode());
assertThat(response.getContentAsString()).isEqualTo(polarisRateLimiterLimitedFallback.rejectTips()); assertThat(testApp3response.getContentAsString()).isEqualTo(polarisRateLimiterLimitedFallback.rejectTips());
assertThat(response.getContentType()).isEqualTo(contentType); assertThat(testApp3response.getContentType()).isEqualTo(contentType);
// Exception // Exception
MetadataContext.LOCAL_SERVICE = "TestApp4"; MetadataContext.LOCAL_SERVICE = "TestApp4";
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); MockHttpServletResponse testApp4response = new MockHttpServletResponse();
quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, testApp4response, filterChain);
} }
catch (ServletException | IOException e) { catch (ServletException | IOException e) {
fail("Exception encountered.", 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; 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.api.plugin.ratelimiter.QuotaResult;
import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.api.core.LimitAPI;
import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest;
@ -66,28 +69,59 @@ public class QuotaCheckUtilsTest {
public void testGetQuota() { public void testGetQuota() {
// Pass // Pass
String serviceName = "TestApp1"; 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.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0); assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk"); assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Unirate waiting 1000ms // Unirate waiting 1000ms
serviceName = "TestApp2"; 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.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(1000); assertThat(quotaResponse.getWaitMs()).isEqualTo(1000);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk"); assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk");
// Rate limited // Rate limited
serviceName = "TestApp3"; 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.getCode()).isEqualTo(QuotaResultCode.QuotaResultLimited);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0); assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultLimited"); assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultLimited");
// Exception // Exception
serviceName = "TestApp4"; 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.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk);
assertThat(quotaResponse.getWaitMs()).isEqualTo(0); assertThat(quotaResponse.getWaitMs()).isEqualTo(0);
assertThat(quotaResponse.getInfo()).isEqualTo("get quota failed"); 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
}

@ -19,6 +19,7 @@
package com.tencent.cloud.polaris.router.config; package com.tencent.cloud.polaris.router.config;
import com.tencent.cloud.common.metadata.config.MetadataAutoConfiguration; import com.tencent.cloud.common.metadata.config.MetadataAutoConfiguration;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.polaris.router.feign.RouterLabelFeignInterceptor; import com.tencent.cloud.polaris.router.feign.RouterLabelFeignInterceptor;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -39,7 +40,9 @@ public class FeignAutoConfigurationTest {
MetadataAutoConfiguration.class, MetadataAutoConfiguration.class,
RouterAutoConfiguration.class, RouterAutoConfiguration.class,
PolarisContextAutoConfiguration.class, PolarisContextAutoConfiguration.class,
FeignAutoConfiguration.class)); FeignAutoConfiguration.class,
ApplicationContextAwareUtils.class
)).withPropertyValues("spring.application.name=test");
@Test @Test
public void routerLabelInterceptor() { public void routerLabelInterceptor() {

@ -19,6 +19,7 @@
package com.tencent.cloud.polaris.router.config; package com.tencent.cloud.polaris.router.config;
import com.tencent.cloud.common.metadata.config.MetadataAutoConfiguration; import com.tencent.cloud.common.metadata.config.MetadataAutoConfiguration;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -42,7 +43,9 @@ public class RouterAutoConfigurationTests {
MetadataAutoConfiguration.class, MetadataAutoConfiguration.class,
RouterAutoConfiguration.class, RouterAutoConfiguration.class,
PolarisContextAutoConfiguration.class, PolarisContextAutoConfiguration.class,
RouterAutoConfiguration.RouterLabelRestTemplateConfig.class)); RouterAutoConfiguration.RouterLabelRestTemplateConfig.class,
ApplicationContextAwareUtils.class
)).withPropertyValues("spring.application.name=test");
@Test @Test
public void testRouterLabelRestTemplateConfig() { public void testRouterLabelRestTemplateConfig() {

@ -55,7 +55,7 @@ public final class MetadataConstant {
/** /**
* Order of filter. * 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. * Order of MetadataFirstFeignInterceptor.
@ -93,4 +93,19 @@ public final class MetadataConstant {
*/ */
public static final String METADATA_CONTEXT = "SCT-METADATA-CONTEXT"; 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";
}
} }

@ -135,13 +135,13 @@ public final class MetadataContextHolder {
mergedTransitiveMetadata.putAll(staticTransitiveMetadata); mergedTransitiveMetadata.putAll(staticTransitiveMetadata);
mergedTransitiveMetadata.putAll(dynamicTransitiveMetadata); mergedTransitiveMetadata.putAll(dynamicTransitiveMetadata);
metadataContext.setTransitiveMetadata(Collections.unmodifiableMap(mergedTransitiveMetadata)); metadataContext.setTransitiveMetadata(Collections.unmodifiableMap(mergedTransitiveMetadata));
Map<String, String> mergedDisposableMetadata = new HashMap<>(dynamicDisposableMetadata);
metadataContext.setUpstreamDisposableMetadata(Collections.unmodifiableMap(mergedDisposableMetadata));
Map<String, String> staticDisposableMetadata = metadataContext.getDisposableMetadata();
metadataContext.setDisposableMetadata(Collections.unmodifiableMap(staticDisposableMetadata));
} }
if (!CollectionUtils.isEmpty(dynamicDisposableMetadata)) {
Map<String, String> mergedUpstreamDisposableMetadata = new HashMap<>(dynamicDisposableMetadata);
metadataContext.setUpstreamDisposableMetadata(Collections.unmodifiableMap(mergedUpstreamDisposableMetadata));
}
Map<String, String> staticDisposableMetadata = metadataContext.getDisposableMetadata();
metadataContext.setDisposableMetadata(Collections.unmodifiableMap(staticDisposableMetadata));
MetadataContextHolder.set(metadataContext); MetadataContextHolder.set(metadataContext);
} }

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
import com.tencent.cloud.common.spi.InstanceMetadataProvider; import com.tencent.cloud.common.spi.InstanceMetadataProvider;
@ -81,14 +82,14 @@ public class StaticMetadataManager {
private String campus; private String campus;
public StaticMetadataManager(MetadataLocalProperties metadataLocalProperties, public StaticMetadataManager(MetadataLocalProperties metadataLocalProperties,
InstanceMetadataProvider instanceMetadataProvider) { List<InstanceMetadataProvider> instanceMetadataProviders) {
parseConfigMetadata(metadataLocalProperties); parseConfigMetadata(metadataLocalProperties);
parseEnvMetadata(); parseEnvMetadata();
parseCustomMetadata(instanceMetadataProvider); parseCustomMetadata(instanceMetadataProviders);
parseLocationMetadata(metadataLocalProperties, instanceMetadataProvider); parseLocationMetadata(metadataLocalProperties, instanceMetadataProviders);
merge(); merge();
@ -182,14 +183,20 @@ public class StaticMetadataManager {
} }
@SuppressWarnings("DuplicatedCode") @SuppressWarnings("DuplicatedCode")
private void parseCustomMetadata(InstanceMetadataProvider instanceMetadataProvider) { private void parseCustomMetadata(List<InstanceMetadataProvider> instanceMetadataProviders) {
if (instanceMetadataProvider == null) { if (CollectionUtils.isEmpty(instanceMetadataProviders)) {
customSPIMetadata = Collections.emptyMap(); customSPIMetadata = Collections.emptyMap();
customSPITransitiveMetadata = Collections.emptyMap(); customSPITransitiveMetadata = Collections.emptyMap();
customSPIDisposableMetadata = Collections.emptyMap(); customSPIDisposableMetadata = Collections.emptyMap();
return; return;
} }
instanceMetadataProviders.forEach(this::parseCustomMetadata);
}
@SuppressWarnings("DuplicatedCode")
private void parseCustomMetadata(InstanceMetadataProvider instanceMetadataProvider) {
// resolve all metadata // resolve all metadata
Map<String, String> allMetadata = instanceMetadataProvider.getMetadata(); Map<String, String> allMetadata = instanceMetadataProvider.getMetadata();
if (allMetadata == null) { if (allMetadata == null) {
@ -247,10 +254,16 @@ public class StaticMetadataManager {
} }
private void parseLocationMetadata(MetadataLocalProperties metadataLocalProperties, private void parseLocationMetadata(MetadataLocalProperties metadataLocalProperties,
InstanceMetadataProvider instanceMetadataProvider) { List<InstanceMetadataProvider> instanceMetadataProviders) {
// resolve region info // resolve region info
if (instanceMetadataProvider != null) { if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
region = instanceMetadataProvider.getRegion(); 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)) { if (StringUtils.isBlank(region)) {
region = System.getenv(ENV_METADATA_REGION); region = System.getenv(ENV_METADATA_REGION);
@ -260,8 +273,14 @@ public class StaticMetadataManager {
} }
// resolve zone info // resolve zone info
if (instanceMetadataProvider != null) { if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
zone = instanceMetadataProvider.getZone(); 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)) { if (StringUtils.isBlank(zone)) {
zone = System.getenv(ENV_METADATA_ZONE); zone = System.getenv(ENV_METADATA_ZONE);
@ -271,8 +290,14 @@ public class StaticMetadataManager {
} }
// resolve campus info // resolve campus info
if (instanceMetadataProvider != null) { if (!CollectionUtils.isEmpty(instanceMetadataProviders)) {
campus = instanceMetadataProvider.getCampus(); 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)) { if (StringUtils.isBlank(campus)) {
campus = System.getenv(ENV_METADATA_CAMPUS); campus = System.getenv(ENV_METADATA_CAMPUS);

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

@ -93,7 +93,7 @@ public class StaticMetadataManagerTest {
when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1")); when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1"));
StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties, StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties,
new MockedMetadataProvider()); Collections.singletonList(new MockedMetadataProvider()));
Map<String, String> metadata = metadataManager.getAllCustomMetadata(); Map<String, String> metadata = metadataManager.getAllCustomMetadata();
assertThat(metadata.size()).isEqualTo(3); assertThat(metadata.size()).isEqualTo(3);
@ -126,7 +126,7 @@ public class StaticMetadataManagerTest {
when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1")); when(metadataLocalProperties.getTransitive()).thenReturn(Collections.singletonList("k1"));
StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties, StaticMetadataManager metadataManager = new StaticMetadataManager(metadataLocalProperties,
new MockedMetadataProvider()); Collections.singletonList(new MockedMetadataProvider()));
Map<String, String> metadata = metadataManager.getMergedStaticMetadata(); Map<String, String> metadata = metadataManager.getMergedStaticMetadata();
assertThat(metadata.size()).isEqualTo(6); assertThat(metadata.size()).isEqualTo(6);

@ -18,6 +18,8 @@
package com.tencent.cloud.common.metadata.config; package com.tencent.cloud.common.metadata.config;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -33,11 +35,17 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
*/ */
public class MetadataAutoConfigurationTest { 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. * No any web application.
@ -45,7 +53,7 @@ public class MetadataAutoConfigurationTest {
@Test @Test
public void test1() { public void test1() {
this.applicationContextRunner this.applicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> { .run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class); Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
}); });
@ -57,7 +65,7 @@ public class MetadataAutoConfigurationTest {
@Test @Test
public void test2() { public void test2() {
this.webApplicationContextRunner this.webApplicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> { .run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class); Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
}); });
@ -69,7 +77,7 @@ public class MetadataAutoConfigurationTest {
@Test @Test
public void test3() { public void test3() {
this.reactiveWebApplicationContextRunner this.reactiveWebApplicationContextRunner
.withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(MetadataAutoConfiguration.class, ApplicationContextAwareUtils.class))
.run(context -> { .run(context -> {
Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class); Assertions.assertThat(context).hasSingleBean(MetadataLocalProperties.class);
}); });

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

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

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

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

@ -0,0 +1,39 @@
/*
* Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.feign.example;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* ProviderBWithFallback.
*
* @author sean yu
*/
@FeignClient(name = "polaris-circuitbreaker-callee-service", contextId = "fallback-from-code", fallback = ProviderBFallback.class)
public interface ProviderBWithFallback {
/**
* Get info of service B.
*
* @return info of service B
*/
@GetMapping("/example/service/b/info")
String info();
}

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

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

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

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

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

@ -18,6 +18,11 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.tencent.cloud</groupId> <groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId> <artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>

@ -17,15 +17,22 @@
package com.tencent.cloud.ratelimit.example.service.callee; package com.tencent.cloud.ratelimit.example.service.callee;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -33,6 +40,8 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException.TooManyRequests; import org.springframework.web.client.HttpClientErrorException.TooManyRequests;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/** /**
* Rate limit controller. * Rate limit controller.
@ -49,6 +58,8 @@ public class BusinessController {
private final AtomicLong lastTimestamp = new AtomicLong(0); private final AtomicLong lastTimestamp = new AtomicLong(0);
@Autowired @Autowired
private RestTemplate restTemplate; private RestTemplate restTemplate;
@Autowired
private WebClient.Builder webClientBuilder;
@Value("${spring.application.name}") @Value("${spring.application.name}")
private String appName; private String appName;
@ -56,11 +67,55 @@ public class BusinessController {
* Get information. * Get information.
* @return information * @return information
*/ */
@RequestMapping("/info") @GetMapping("/info")
public String info() { public String info() {
return "hello world for ratelimit service " + index.incrementAndGet(); return "hello world for ratelimit service " + index.incrementAndGet();
} }
@GetMapping("/info/webclient")
public Mono<String> infoWebClient() {
return Mono.just("hello world for ratelimit service " + index.incrementAndGet());
}
@GetMapping("/invoke/webclient")
public String invokeInfoWebClient() throws InterruptedException, ExecutionException {
StringBuffer builder = new StringBuffer();
WebClient webClient = webClientBuilder.baseUrl("http://" + appName).build();
List<Mono<String>> monoList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
Mono<String> response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/business/info/webclient")
.queryParam("yyy", "yyy")
.build()
)
.header("xxx", "xxx")
.retrieve()
.bodyToMono(String.class)
.doOnSuccess(s -> builder.append(s + "\n"))
.doOnError(e -> {
if (e instanceof WebClientResponseException) {
if (((WebClientResponseException) e).getRawStatusCode() == 429) {
builder.append("TooManyRequests ").append(index.incrementAndGet() + "\n");
}
}
})
.onErrorReturn("");
monoList.add(response);
}
for (Mono<String> mono : monoList) {
mono.toFuture().get();
}
index.set(0);
return builder.toString();
}
/**
* Get information 30 times per 1 second.
*
* @return result of 30 calls.
* @throws InterruptedException exception
*/
@GetMapping("/invoke") @GetMapping("/invoke")
public String invokeInfo() throws InterruptedException { public String invokeInfo() throws InterruptedException {
StringBuffer builder = new StringBuffer(); StringBuffer builder = new StringBuffer();
@ -68,19 +123,31 @@ public class BusinessController {
for (int i = 0; i < 30; i++) { for (int i = 0; i < 30; i++) {
new Thread(() -> { new Thread(() -> {
try { try {
ResponseEntity<String> entity = restTemplate.getForEntity("http://" + appName + "/business/info", HttpHeaders httpHeaders = new HttpHeaders();
String.class); httpHeaders.add("xxx", "xxx");
ResponseEntity<String> entity = restTemplate.exchange(
"http://" + appName + "/business/info?yyy={yyy}",
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
String.class,
"yyy"
);
builder.append(entity.getBody() + "\n"); builder.append(entity.getBody() + "\n");
} }
catch (RestClientException e) { catch (RestClientException e) {
if (e instanceof TooManyRequests) { if (e instanceof TooManyRequests) {
builder.append("TooManyRequests " + index.incrementAndGet() + "\n"); builder.append("TooManyRequests ").append(index.incrementAndGet() + "\n");
} }
else { else {
throw e; throw e;
} }
} }
count.countDown(); catch (Exception e) {
e.printStackTrace();
}
finally {
count.countDown();
}
}).start(); }).start();
} }
count.await(); count.await();
@ -90,6 +157,7 @@ public class BusinessController {
/** /**
* Get information with unirate. * Get information with unirate.
*
* @return information * @return information
*/ */
@GetMapping("/unirate") @GetMapping("/unirate")

@ -0,0 +1,44 @@
/*
* 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.ratelimit.example.service.callee;
import java.util.HashMap;
import java.util.Map;
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* resolver custom label from request.
*
* @author sean yu
*/
@Component
public class CustomLabelResolverReactive implements PolarisRateLimiterLabelReactiveResolver {
@Override
public Map<String, String> resolve(ServerWebExchange exchange) {
// rate limit by some request params. such as query params, headers ..
Map<String, String> labels = new HashMap<>();
labels.put("user", "zhangsan");
return labels;
}
}

@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
/** /**
* Rate limit application. * Rate limit application.
@ -40,4 +41,11 @@ public class RateLimitCalleeService {
public RestTemplate restTemplate() { public RestTemplate restTemplate() {
return new RestTemplate(); return new RestTemplate();
} }
@LoadBalanced
@Bean
WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
} }

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

@ -50,7 +50,7 @@ public class RpcEnhancementAutoConfigurationTest {
RpcEnhancementAutoConfiguration.class, RpcEnhancementAutoConfiguration.class,
PolarisRestTemplateAutoConfigurationTester.class, PolarisRestTemplateAutoConfigurationTester.class,
FeignLoadBalancerAutoConfiguration.class)) FeignLoadBalancerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true"); .withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true", "spring.application.name=test");
@Test @Test
public void testDefaultInitialization() { public void testDefaultInitialization() {

@ -38,7 +38,7 @@ import static org.springframework.http.HttpStatus.Series.SERVER_ERROR;
* @author Haotian Zhang * @author Haotian Zhang
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@SpringBootTest(classes = RpcEnhancementReporterPropertiesTest.TestApplication.class) @SpringBootTest(classes = RpcEnhancementReporterPropertiesTest.TestApplication.class, properties = "spring.application.name=test")
@ActiveProfiles("test") @ActiveProfiles("test")
public class RpcEnhancementReporterPropertiesTest { public class RpcEnhancementReporterPropertiesTest {

@ -41,7 +41,8 @@ public class StatConfigModifierTest {
.withPropertyValues("spring.cloud.polaris.stat.enabled=true") .withPropertyValues("spring.cloud.polaris.stat.enabled=true")
.withPropertyValues("spring.cloud.polaris.stat.host=127.0.0.1") .withPropertyValues("spring.cloud.polaris.stat.host=127.0.0.1")
.withPropertyValues("spring.cloud.polaris.stat.port=20000") .withPropertyValues("spring.cloud.polaris.stat.port=20000")
.withPropertyValues("spring.cloud.polaris.stat.path=/xxx"); .withPropertyValues("spring.cloud.polaris.stat.path=/xxx")
.withPropertyValues("spring.application.name=test");
private final ApplicationContextRunner pushContextRunner = new ApplicationContextRunner() private final ApplicationContextRunner pushContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TestApplication.class)) .withConfiguration(AutoConfigurations.of(TestApplication.class))
@ -49,12 +50,14 @@ public class StatConfigModifierTest {
.withPropertyValues("spring.cloud.polaris.stat.enabled=true") .withPropertyValues("spring.cloud.polaris.stat.enabled=true")
.withPropertyValues("spring.cloud.polaris.stat.pushgateway.enabled=true") .withPropertyValues("spring.cloud.polaris.stat.pushgateway.enabled=true")
.withPropertyValues("spring.cloud.polaris.stat.pushgateway.address=127.0.0.1:9091") .withPropertyValues("spring.cloud.polaris.stat.pushgateway.address=127.0.0.1:9091")
.withPropertyValues("spring.cloud.polaris.stat.pushgateway.push-interval=1000"); .withPropertyValues("spring.cloud.polaris.stat.pushgateway.push-interval=1000")
.withPropertyValues("spring.application.name=test");
private final ApplicationContextRunner disabledContextRunner = new ApplicationContextRunner() private final ApplicationContextRunner disabledContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TestApplication.class)) .withConfiguration(AutoConfigurations.of(TestApplication.class))
.withPropertyValues("spring.cloud.polaris.enabled=true") .withPropertyValues("spring.cloud.polaris.enabled=true")
.withPropertyValues("spring.cloud.polaris.stat.enabled=false"); .withPropertyValues("spring.cloud.polaris.stat.enabled=false")
.withPropertyValues("spring.application.name=test");
@Test @Test
void testPull() { void testPull() {

Loading…
Cancel
Save