diff --git a/CHANGELOG.md b/CHANGELOG.md index e53681b5d..d9e0c14db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Change Log --- - - [Feature: Support parse ratelimit rule expression labels.](https://github.com/Tencent/spring-cloud-tencent/pull/183) - [Feature: Router support request label.](https://github.com/Tencent/spring-cloud-tencent/pull/165) - [Feature: Support router expression label](https://github.com/Tencent/spring-cloud-tencent/pull/190) - [Add metadata transfer example.](https://github.com/Tencent/spring-cloud-tencent/pull/184) - [Feature: Support metadata router.](https://github.com/Tencent/spring-cloud-tencent/pull/191) - [Feature: Misc optimize metadata router.](https://github.com/Tencent/spring-cloud-tencent/pull/192) +- [feat:add rate limit of unirate.](https://github.com/Tencent/spring-cloud-tencent/pull/197) diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfiguration.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfiguration.java new file mode 100644 index 000000000..28389a46f --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfiguration.java @@ -0,0 +1,75 @@ +/* + * 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.config; + +import com.tencent.cloud.common.constant.ContextConstant; +import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; +import com.tencent.cloud.polaris.context.PolarisConfigModifier; +import com.tencent.polaris.factory.config.ConfigurationImpl; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Autoconfiguration of rate limit at bootstrap phase. + * + * @author Haotian Zhang + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnPolarisEnabled +@ConditionalOnProperty(name = "spring.cloud.polaris.ratelimit.enabled", matchIfMissing = true) +public class PolarisRateLimitBootstrapConfiguration { + + @Bean + public PolarisRateLimitProperties polarisRateLimitProperties() { + return new PolarisRateLimitProperties(); + } + + @Bean + public RateLimitConfigModifier rateLimitConfigModifier(PolarisRateLimitProperties polarisRateLimitProperties) { + return new RateLimitConfigModifier(polarisRateLimitProperties); + } + + /** + * Config modifier for rate limit. + * + * @author Haotian Zhang + */ + public static class RateLimitConfigModifier implements PolarisConfigModifier { + + private PolarisRateLimitProperties polarisRateLimitProperties; + + public RateLimitConfigModifier(PolarisRateLimitProperties polarisRateLimitProperties) { + this.polarisRateLimitProperties = polarisRateLimitProperties; + } + + @Override + public void modify(ConfigurationImpl configuration) { + // Update MaxQueuingTime. + configuration.getProvider().getRateLimit() + .setMaxQueuingTime(polarisRateLimitProperties.getMaxQueuingTime()); + } + + @Override + public int getOrder() { + return ContextConstant.ModifierOrder.CIRCUIT_BREAKER_ORDER; + } + + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/RateLimitConfiguration.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfiguration.java similarity index 93% rename from spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/RateLimitConfiguration.java rename to spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfiguration.java index bf912674d..e0d9933b8 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/RateLimitConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfiguration.java @@ -19,6 +19,7 @@ package com.tencent.cloud.polaris.ratelimit.config; import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; +import com.tencent.cloud.polaris.context.PolarisContextAutoConfiguration; import com.tencent.cloud.polaris.context.ServiceRuleManager; import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; @@ -30,6 +31,7 @@ import com.tencent.polaris.client.api.SDKContext; import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.factory.LimitAPIFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -45,18 +47,15 @@ import static javax.servlet.DispatcherType.INCLUDE; import static javax.servlet.DispatcherType.REQUEST; /** + * Configuration of rate limit. + * * @author Haotian Zhang */ @Configuration @ConditionalOnPolarisEnabled -@ConditionalOnProperty(name = "spring.cloud.polaris.ratelimit.enabled", - matchIfMissing = true) -public class RateLimitConfiguration { - - @Bean - public PolarisRateLimitProperties polarisRateLimitProperties() { - return new PolarisRateLimitProperties(); - } +@AutoConfigureAfter(PolarisContextAutoConfiguration.class) +@ConditionalOnProperty(name = "spring.cloud.polaris.ratelimit.enabled", matchIfMissing = true) +public class PolarisRateLimitConfiguration { @Bean @ConditionalOnMissingBean diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitProperties.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitProperties.java index d51d90b9c..a7a0f4995 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitProperties.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitProperties.java @@ -44,6 +44,11 @@ public class PolarisRateLimitProperties { */ private int rejectHttpCode = HttpStatus.TOO_MANY_REQUESTS.value(); + /** + * Max queuing time when using unirate. + */ + private long maxQueuingTime = 1000L; + public String getRejectRequestTips() { return rejectRequestTips; } @@ -68,4 +73,11 @@ public class PolarisRateLimitProperties { this.rejectHttpCode = rejectHttpCode; } + public long getMaxQueuingTime() { + return maxQueuingTime; + } + + public void setMaxQueuingTime(long maxQueuingTime) { + this.maxQueuingTime = maxQueuingTime; + } } diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java index d9b8d3389..b3f300e15 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java @@ -59,8 +59,7 @@ import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LAB */ public class QuotaCheckReactiveFilter implements WebFilter, Ordered { - private static final Logger LOG = LoggerFactory - .getLogger(QuotaCheckReactiveFilter.class); + private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckReactiveFilter.class); private final LimitAPI limitAPI; @@ -111,6 +110,10 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered { .write(rejectTips.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(dataBuffer)); } + // Unirate + if (quotaResponse.getCode() == QuotaResultCode.QuotaResultOk && quotaResponse.getWaitMs() > 0) { + Thread.sleep(quotaResponse.getWaitMs()); + } } catch (Throwable t) { // An exception occurs in the rate limiting API call, @@ -147,8 +150,7 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered { return labelResolver.resolve(exchange); } catch (Throwable e) { - LOG.error("resolve custom label failed. resolver = {}", - labelResolver.getClass().getName(), e); + LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e); } } return Maps.newHashMap(); diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java index 54a9e545f..bbb183ac9 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java @@ -102,6 +102,10 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter { response.getWriter().write(rejectTips); return; } + // Unirate + if (quotaResponse.getCode() == QuotaResultCode.QuotaResultOk && quotaResponse.getWaitMs() > 0) { + Thread.sleep(quotaResponse.getWaitMs()); + } filterChain.doFilter(request, response); } diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtils.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtils.java index e5f70039c..4a67aea01 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtils.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtils.java @@ -33,15 +33,13 @@ import org.springframework.util.StringUtils; */ public final class RateLimitUtils { - private static final Logger LOG = LoggerFactory - .getLogger(RateLimitUtils.class); + private static final Logger LOG = LoggerFactory.getLogger(RateLimitUtils.class); private RateLimitUtils() { } - public static String getRejectTips( - PolarisRateLimitProperties polarisRateLimitProperties) { + public static String getRejectTips(PolarisRateLimitProperties polarisRateLimitProperties) { String tips = polarisRateLimitProperties.getRejectRequestTips(); if (!StringUtils.isEmpty(tips)) { diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 19b37aeb5..34d80d038 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -23,6 +23,12 @@ "type": "java.lang.Integer", "defaultValue": "429", "description": "Custom http code when reject request." + }, + { + "name": "spring.cloud.polaris.ratelimit.maxQueuingTime", + "type": "java.lang.Long", + "defaultValue": "1000", + "description": "Max queuing time when using unirate." } ] } diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/spring.factories index 89a6c50a1..4a140ac9a 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/resources/META-INF/spring.factories @@ -1,2 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - com.tencent.cloud.polaris.ratelimit.config.RateLimitConfiguration + com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitConfiguration +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitBootstrapConfiguration diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/RateLimitRuleLabelResolverTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/RateLimitRuleLabelResolverTest.java new file mode 100644 index 000000000..73d39dade --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/RateLimitRuleLabelResolverTest.java @@ -0,0 +1,101 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.ratelimit; + +import java.util.Set; + +import com.google.protobuf.StringValue; +import com.tencent.cloud.polaris.context.ServiceRuleManager; +import com.tencent.polaris.client.pb.ModelProto; +import com.tencent.polaris.client.pb.RateLimitProto; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link RateLimitRuleLabelResolver}. + * + * @author Haotian Zhang + */ +@RunWith(MockitoJUnitRunner.class) +public class RateLimitRuleLabelResolverTest { + + private ServiceRuleManager serviceRuleManager; + + private RateLimitRuleLabelResolver rateLimitRuleLabelResolver; + + @Before + public void setUp() { + serviceRuleManager = mock(ServiceRuleManager.class); + when(serviceRuleManager.getServiceRateLimitRule(any(), anyString())).thenAnswer(invocationOnMock -> { + String serviceName = invocationOnMock.getArgument(1).toString(); + if (serviceName.equals("TestApp1")) { + return null; + } + else if (serviceName.equals("TestApp2")) { + return RateLimitProto.RateLimit.newBuilder().build(); + } + else if (serviceName.equals("TestApp3")) { + RateLimitProto.Rule rule = RateLimitProto.Rule.newBuilder().build(); + return RateLimitProto.RateLimit.newBuilder().addRules(rule).build(); + } + else { + ModelProto.MatchString matchString = ModelProto.MatchString.newBuilder() + .setType(ModelProto.MatchString.MatchStringType.EXACT) + .setValue(StringValue.of("value")) + .setValueType(ModelProto.MatchString.ValueType.TEXT).build(); + RateLimitProto.Rule rule = RateLimitProto.Rule.newBuilder() + .putLabels("${http.method}", matchString).build(); + return RateLimitProto.RateLimit.newBuilder().addRules(rule).build(); + } + }); + + rateLimitRuleLabelResolver = new RateLimitRuleLabelResolver(serviceRuleManager); + } + + @Test + public void testGetExpressionLabelKeys() { + // rateLimitRule == null + String serviceName = "TestApp1"; + Set labelKeys = rateLimitRuleLabelResolver.getExpressionLabelKeys(null, serviceName); + assertThat(labelKeys).isEmpty(); + + // CollectionUtils.isEmpty(rules) + serviceName = "TestApp2"; + labelKeys = rateLimitRuleLabelResolver.getExpressionLabelKeys(null, serviceName); + assertThat(labelKeys).isEmpty(); + + // CollectionUtils.isEmpty(labels) + serviceName = "TestApp3"; + labelKeys = rateLimitRuleLabelResolver.getExpressionLabelKeys(null, serviceName); + assertThat(labelKeys).isEmpty(); + + // Has labels + serviceName = "TestApp4"; + labelKeys = rateLimitRuleLabelResolver.getExpressionLabelKeys(null, serviceName); + assertThat(labelKeys).isNotEmpty(); + assertThat(labelKeys).contains("${http.method}"); + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfigurationTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfigurationTest.java new file mode 100644 index 000000000..9b085a637 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitBootstrapConfigurationTest.java @@ -0,0 +1,46 @@ +/* + * 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.config; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link PolarisRateLimitBootstrapConfiguration}. + * + * @author Haotian Zhang + */ +public class PolarisRateLimitBootstrapConfigurationTest { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(PolarisRateLimitBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.polaris.ratelimit.enabled=true"); + + @Test + public void testDefaultInitialization() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(PolarisRateLimitProperties.class); + assertThat(context).hasSingleBean(PolarisRateLimitBootstrapConfiguration.RateLimitConfigModifier.class); + }); + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfigurationTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfigurationTest.java new file mode 100644 index 000000000..5a03159f1 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitConfigurationTest.java @@ -0,0 +1,99 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.ratelimit.config; + +import com.tencent.cloud.polaris.context.PolarisContextAutoConfiguration; +import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; +import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckReactiveFilter; +import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter; +import com.tencent.polaris.ratelimit.api.core.LimitAPI; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.FilterRegistrationBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link PolarisRateLimitConfiguration}. + * + * @author Haotian Zhang + */ +public class PolarisRateLimitConfigurationTest { + + private ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner(); + + private WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner(); + + private ReactiveWebApplicationContextRunner reactiveWebApplicationContextRunner = new ReactiveWebApplicationContextRunner(); + + @Test + public void testNoWebApplication() { + this.applicationContextRunner + .withConfiguration(AutoConfigurations.of( + PolarisContextAutoConfiguration.class, + PolarisRateLimitProperties.class, + PolarisRateLimitConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(LimitAPI.class); + assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class); + assertThat(context).doesNotHaveBean(PolarisRateLimitConfiguration.QuotaCheckFilterConfig.class); + assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class); + assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); + assertThat(context).doesNotHaveBean(PolarisRateLimitConfiguration.MetadataReactiveFilterConfig.class); + assertThat(context).doesNotHaveBean(QuotaCheckReactiveFilter.class); + }); + } + + @Test + public void testServletWebApplication() { + this.webApplicationContextRunner + .withConfiguration(AutoConfigurations.of(PolarisContextAutoConfiguration.class, + PolarisRateLimitProperties.class, + PolarisRateLimitConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(LimitAPI.class); + assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class); + assertThat(context).hasSingleBean(PolarisRateLimitConfiguration.QuotaCheckFilterConfig.class); + assertThat(context).hasSingleBean(QuotaCheckServletFilter.class); + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + assertThat(context).doesNotHaveBean(PolarisRateLimitConfiguration.MetadataReactiveFilterConfig.class); + assertThat(context).doesNotHaveBean(QuotaCheckReactiveFilter.class); + }); + } + + @Test + public void testReactiveWebApplication() { + this.reactiveWebApplicationContextRunner + .withConfiguration(AutoConfigurations.of(PolarisContextAutoConfiguration.class, + PolarisRateLimitProperties.class, + PolarisRateLimitConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(LimitAPI.class); + assertThat(context).hasSingleBean(RateLimitRuleLabelResolver.class); + assertThat(context).doesNotHaveBean(PolarisRateLimitConfiguration.QuotaCheckFilterConfig.class); + assertThat(context).doesNotHaveBean(QuotaCheckServletFilter.class); + assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); + assertThat(context).hasSingleBean(PolarisRateLimitConfiguration.MetadataReactiveFilterConfig.class); + assertThat(context).hasSingleBean(QuotaCheckReactiveFilter.class); + }); + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitPropertiesTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitPropertiesTest.java new file mode 100644 index 000000000..70a3333e5 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitPropertiesTest.java @@ -0,0 +1,59 @@ +/* + * 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.config; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link PolarisRateLimitProperties}. + * + * @author Haotian Zhang + */ +public class PolarisRateLimitPropertiesTest { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PolarisRateLimitPropertiesAutoConfiguration.class, PolarisRateLimitProperties.class)) + .withPropertyValues("spring.cloud.polaris.ratelimit.rejectRequestTips=xxx") + .withPropertyValues("spring.cloud.polaris.ratelimit.rejectRequestTipsFilePath=/index.html") + .withPropertyValues("spring.cloud.polaris.ratelimit.rejectHttpCode=419") + .withPropertyValues("spring.cloud.polaris.ratelimit.maxQueuingTime=500"); + + @Test + public void testDefaultInitialization() { + this.contextRunner.run(context -> { + PolarisRateLimitProperties polarisRateLimitProperties = context.getBean(PolarisRateLimitProperties.class); + assertThat(polarisRateLimitProperties.getRejectRequestTips()).isEqualTo("xxx"); + assertThat(polarisRateLimitProperties.getRejectRequestTipsFilePath()).isEqualTo("/index.html"); + assertThat(polarisRateLimitProperties.getRejectHttpCode()).isEqualTo(419); + assertThat(polarisRateLimitProperties.getMaxQueuingTime()).isEqualTo(500L); + }); + } + + @Configuration + @EnableAutoConfiguration + static class PolarisRateLimitPropertiesAutoConfiguration { + + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilterTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilterTest.java new file mode 100644 index 000000000..179cc1186 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilterTest.java @@ -0,0 +1,212 @@ +/* + * 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.filter; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.util.ExpressionLabelUtils; +import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; +import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; +import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; +import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; +import com.tencent.polaris.ratelimit.api.core.LimitAPI; +import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; +import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Test for {@link QuotaCheckReactiveFilter}. + * + * @author Haotian Zhang + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.script.*"}) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest(ExpressionLabelUtils.class) +@SpringBootTest(classes = QuotaCheckReactiveFilterTest.TestApplication.class) +public class QuotaCheckReactiveFilterTest { + + private PolarisRateLimiterLabelReactiveResolver labelResolver = exchange -> Collections.singletonMap("ReactiveResolver", "ReactiveResolver"); + + private QuotaCheckReactiveFilter quotaCheckReactiveFilter; + + @BeforeClass + public static void beforeClass() { + // mock ExpressionLabelUtils.resolve() + mockStatic(ExpressionLabelUtils.class); + when(ExpressionLabelUtils.resolve(any(ServerWebExchange.class), anySet())).thenReturn(Collections.singletonMap("RuleLabelResolver", "RuleLabelResolver")); + } + + @Before + public void setUp() { + MetadataContext.LOCAL_NAMESPACE = "TEST"; + + LimitAPI limitAPI = mock(LimitAPI.class); + when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> { + String serviceName = ((QuotaRequest) invocationOnMock.getArgument(0)).getService(); + if (serviceName.equals("TestApp1")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp2")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 1000, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp3")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited")); + } + else { + return new QuotaResponse(new QuotaResult(null, 0, null)); + } + }); + + PolarisRateLimitProperties polarisRateLimitProperties = new PolarisRateLimitProperties(); + polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips"); + polarisRateLimitProperties.setRejectHttpCode(419); + + RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class); + when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())).thenReturn(Collections.EMPTY_SET); + + this.quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver); + } + + @Test + public void testGetOrder() { + assertThat(this.quotaCheckReactiveFilter.getOrder()).isEqualTo(RateLimitConstant.FILTER_ORDER); + } + + @Test + public void testInit() { + quotaCheckReactiveFilter.init(); + try { + Field rejectTips = QuotaCheckReactiveFilter.class.getDeclaredField("rejectTips"); + rejectTips.setAccessible(true); + assertThat(rejectTips.get(quotaCheckReactiveFilter)).isEqualTo("RejectRequestTips"); + } + catch (NoSuchFieldException | IllegalAccessException e) { + fail("Exception encountered.", e); + } + } + + @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 result = (Map) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get("ReactiveResolver")).isEqualTo("ReactiveResolver"); + + // throw exception + PolarisRateLimiterLabelReactiveResolver exceptionLabelResolver = new PolarisRateLimiterLabelReactiveResolver() { + @Override + public Map resolve(ServerWebExchange exchange) { + throw new RuntimeException("Mock exception."); + } + }; + quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, exceptionLabelResolver, null, null); + result = (Map) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange); + assertThat(result.size()).isEqualTo(0); + + // labelResolver == null + quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, null, null, null); + result = (Map) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange); + assertThat(result.size()).isEqualTo(0); + + getCustomResolvedLabels.setAccessible(false); + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + fail("Exception encountered.", e); + } + } + + @Test + public void testFilter() { + // Create mock WebFilterChain + WebFilterChain webFilterChain = serverWebExchange -> Mono.empty(); + + // Mock request + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + quotaCheckReactiveFilter.init(); + + // Pass + MetadataContext.LOCAL_SERVICE = "TestApp1"; + quotaCheckReactiveFilter.filter(exchange, webFilterChain); + + // Unirate waiting 1000ms + MetadataContext.LOCAL_SERVICE = "TestApp2"; + long startTimestamp = System.currentTimeMillis(); + quotaCheckReactiveFilter.filter(exchange, webFilterChain); + assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); + + // Rate limited + MetadataContext.LOCAL_SERVICE = "TestApp3"; + quotaCheckReactiveFilter.filter(exchange, webFilterChain); + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getRawStatusCode()).isEqualTo(419); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE); + + // Exception + MetadataContext.LOCAL_SERVICE = "TestApp4"; + quotaCheckReactiveFilter.filter(exchange, webFilterChain); + } + + @SpringBootApplication + protected static class TestApplication { + + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilterTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilterTest.java new file mode 100644 index 000000000..1fb343f61 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilterTest.java @@ -0,0 +1,209 @@ +/* + * 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.filter; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.util.ExpressionLabelUtils; +import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; +import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver; +import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; +import com.tencent.polaris.ratelimit.api.core.LimitAPI; +import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; +import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Test for {@link QuotaCheckServletFilter}. + * + * @author Haotian Zhang + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.script.*"}) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest(ExpressionLabelUtils.class) +@SpringBootTest(classes = QuotaCheckServletFilterTest.TestApplication.class) +public class QuotaCheckServletFilterTest { + + private PolarisRateLimiterLabelServletResolver labelResolver = exchange -> Collections.singletonMap("ServletResolver", "ServletResolver"); + + private QuotaCheckServletFilter quotaCheckServletFilter; + + @BeforeClass + public static void beforeClass() { + // mock ExpressionLabelUtils.resolve() + mockStatic(ExpressionLabelUtils.class); + when(ExpressionLabelUtils.resolve(any(ServerWebExchange.class), anySet())).thenReturn(Collections.singletonMap("RuleLabelResolver", "RuleLabelResolver")); + } + + @Before + public void setUp() { + MetadataContext.LOCAL_NAMESPACE = "TEST"; + + LimitAPI limitAPI = mock(LimitAPI.class); + when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> { + String serviceName = ((QuotaRequest) invocationOnMock.getArgument(0)).getService(); + if (serviceName.equals("TestApp1")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp2")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 1000, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp3")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited")); + } + else { + return new QuotaResponse(new QuotaResult(null, 0, null)); + } + }); + + PolarisRateLimitProperties polarisRateLimitProperties = new PolarisRateLimitProperties(); + polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips"); + polarisRateLimitProperties.setRejectHttpCode(419); + + RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class); + when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())).thenReturn(Collections.EMPTY_SET); + + this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver); + } + + @Test + public void testInit() { + quotaCheckServletFilter.init(); + try { + Field rejectTips = QuotaCheckServletFilter.class.getDeclaredField("rejectTips"); + rejectTips.setAccessible(true); + assertThat(rejectTips.get(quotaCheckServletFilter)).isEqualTo("RejectRequestTips"); + } + catch (NoSuchFieldException | IllegalAccessException e) { + fail("Exception encountered.", e); + } + } + + @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 result = (Map) 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); + result = (Map) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request); + assertThat(result.size()).isEqualTo(0); + + // labelResolver == null + quotaCheckServletFilter = new QuotaCheckServletFilter(null, null, null, null); + result = (Map) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request); + assertThat(result.size()).isEqualTo(0); + + getCustomResolvedLabels.setAccessible(false); + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + fail("Exception encountered.", e); + } + } + + @Test + public void testDoFilterInternal() { + // Create mock FilterChain + FilterChain filterChain = (servletRequest, servletResponse) -> { + + }; + + // Mock request + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + quotaCheckServletFilter.init(); + try { + // Pass + MetadataContext.LOCAL_SERVICE = "TestApp1"; + quotaCheckServletFilter.doFilterInternal(request, response, filterChain); + + // Unirate waiting 1000ms + MetadataContext.LOCAL_SERVICE = "TestApp2"; + long startTimestamp = System.currentTimeMillis(); + quotaCheckServletFilter.doFilterInternal(request, response, filterChain); + assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); + + // Rate limited + MetadataContext.LOCAL_SERVICE = "TestApp3"; + quotaCheckServletFilter.doFilterInternal(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(419); + assertThat(response.getContentAsString()).isEqualTo("RejectRequestTips"); + + + // Exception + MetadataContext.LOCAL_SERVICE = "TestApp4"; + quotaCheckServletFilter.doFilterInternal(request, response, filterChain); + } + catch (ServletException | IOException e) { + fail("Exception encountered.", e); + } + } + + @SpringBootApplication + protected static class TestApplication { + + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/QuotaCheckUtilsTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/QuotaCheckUtilsTest.java new file mode 100644 index 000000000..8589a2b6b --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/QuotaCheckUtilsTest.java @@ -0,0 +1,97 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.ratelimit.utils; + +import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; +import com.tencent.polaris.ratelimit.api.core.LimitAPI; +import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; +import com.tencent.polaris.ratelimit.api.rpc.QuotaResponse; +import com.tencent.polaris.ratelimit.api.rpc.QuotaResultCode; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Test for {@link QuotaCheckUtils}. + * + * @author Haotian Zhang + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.script.*"}) +public class QuotaCheckUtilsTest { + + private LimitAPI limitAPI; + + @Before + public void setUp() { + limitAPI = mock(LimitAPI.class); + when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> { + String serviceName = ((QuotaRequest) invocationOnMock.getArgument(0)).getService(); + if (serviceName.equals("TestApp1")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 0, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp2")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultOk, 1000, "QuotaResultOk")); + } + else if (serviceName.equals("TestApp3")) { + return new QuotaResponse(new QuotaResult(QuotaResult.Code.QuotaResultLimited, 0, "QuotaResultLimited")); + } + else { + throw new RuntimeException("Mock exception."); + } + }); + } + + @Test + public void testGetQuota() { + // Pass + String serviceName = "TestApp1"; + QuotaResponse quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null); + assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk); + assertThat(quotaResponse.getWaitMs()).isEqualTo(0); + assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk"); + + // Unirate waiting 1000ms + serviceName = "TestApp2"; + quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null); + assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk); + assertThat(quotaResponse.getWaitMs()).isEqualTo(1000); + assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultOk"); + + // Rate limited + serviceName = "TestApp3"; + quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null); + assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultLimited); + assertThat(quotaResponse.getWaitMs()).isEqualTo(0); + assertThat(quotaResponse.getInfo()).isEqualTo("QuotaResultLimited"); + + // Exception + serviceName = "TestApp4"; + quotaResponse = QuotaCheckUtils.getQuota(limitAPI, null, serviceName, 1, null, null); + assertThat(quotaResponse.getCode()).isEqualTo(QuotaResultCode.QuotaResultOk); + assertThat(quotaResponse.getWaitMs()).isEqualTo(0); + assertThat(quotaResponse.getInfo()).isEqualTo("get quota failed"); + } +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtilsTest.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtilsTest.java new file mode 100644 index 000000000..5eb7901df --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/utils/RateLimitUtilsTest.java @@ -0,0 +1,79 @@ +/* + * 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.utils; + +import java.io.IOException; + +import com.tencent.cloud.common.util.ResourceFileUtils; +import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.QUOTA_LIMITED_INFO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Test for {@link RateLimitUtils}. + * + * @author Haotian Zhang + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.script.*"}) +@PrepareForTest(ResourceFileUtils.class) +public class RateLimitUtilsTest { + + @BeforeClass + public static void beforeClass() throws IOException { + mockStatic(ResourceFileUtils.class); + when(ResourceFileUtils.readFile(anyString())).thenAnswer(invocation -> { + String rejectFilePath = invocation.getArgument(0).toString(); + if (rejectFilePath.equals("exception.html")) { + throw new IOException("Mock exceptions"); + } + else { + return "RejectRequestTips"; + } + }); + } + + @Test + public void testGetRejectTips() { + PolarisRateLimitProperties polarisRateLimitProperties = new PolarisRateLimitProperties(); + + // RejectRequestTips + polarisRateLimitProperties.setRejectRequestTips("RejectRequestTips"); + assertThat(RateLimitUtils.getRejectTips(polarisRateLimitProperties)).isEqualTo("RejectRequestTips"); + + // RejectRequestTipsFilePath + polarisRateLimitProperties.setRejectRequestTips(null); + polarisRateLimitProperties.setRejectRequestTipsFilePath("reject-tips.html"); + assertThat(RateLimitUtils.getRejectTips(polarisRateLimitProperties)).isEqualTo("RejectRequestTips"); + + // RejectRequestTipsFilePath with Exception + polarisRateLimitProperties.setRejectRequestTips(null); + polarisRateLimitProperties.setRejectRequestTipsFilePath("exception.html"); + assertThat(RateLimitUtils.getRejectTips(polarisRateLimitProperties)).isEqualTo(QUOTA_LIMITED_INFO); + } +} diff --git a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/ReflectionUtils.java b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/ReflectionUtils.java index 67e4c9210..6c4a3353e 100644 --- a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/ReflectionUtils.java +++ b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/ReflectionUtils.java @@ -31,8 +31,10 @@ public final class ReflectionUtils { } public static Object getFieldValue(Object instance, String fieldName) { - Field field = org.springframework.util.ReflectionUtils - .findField(instance.getClass(), fieldName); + Field field = org.springframework.util.ReflectionUtils.findField(instance.getClass(), fieldName); + if (field == null) { + return null; + } field.setAccessible(true); try { diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java index e001201f4..05e18a191 100644 --- a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java @@ -18,6 +18,10 @@ package com.tencent.cloud.ratelimit.example.service.callee; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -38,6 +42,8 @@ import org.springframework.web.client.RestTemplate; @RequestMapping("/business") public class BusinessController { + private static final Logger LOG = LoggerFactory.getLogger(BusinessController.class); + private final AtomicInteger index = new AtomicInteger(0); @Autowired @@ -46,6 +52,8 @@ public class BusinessController { @Value("${spring.application.name}") private String appName; + private AtomicLong lastTimestamp = new AtomicLong(0); + /** * Get information. * @return information @@ -77,4 +85,21 @@ public class BusinessController { return builder.toString(); } + /** + * Get information with unirate. + * @return information + */ + @GetMapping("/unirate") + public String unirate() { + long currentTimestamp = System.currentTimeMillis(); + long lastTime = lastTimestamp.get(); + if (lastTime != 0) { + LOG.info("Current timestamp:" + currentTimestamp + ", diff from last timestamp:" + (currentTimestamp - lastTime)); + } + else { + LOG.info("Current timestamp:" + currentTimestamp); + } + lastTimestamp.set(currentTimestamp); + return "hello world for ratelimit service with diff from last request:" + (currentTimestamp - lastTime) + "ms."; + } } diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/resources/bootstrap.yml b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/resources/bootstrap.yml index a33fc48f1..66c72070e 100644 --- a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/resources/bootstrap.yml +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/resources/bootstrap.yml @@ -11,3 +11,4 @@ spring: ratelimit: enabled: true rejectRequestTipsFilePath: reject-tips.html + maxQueuingTime: 500