From 35adebfca63803d463c6d7815790c24bc0830b5e Mon Sep 17 00:00:00 2001 From: "lingxiao.wu" <51630311+lingxiao-wu@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:45:53 +0800 Subject: [PATCH] feature:add PolarisRateLimiterLimitedFallback spi. (#857) --- CHANGELOG.md | 1 + .../PolarisRateLimitAutoConfiguration.java | 11 ++-- .../filter/QuotaCheckReactiveFilter.java | 31 +++++++-- .../filter/QuotaCheckServletFilter.java | 24 +++++-- .../PolarisRateLimiterLimitedFallback.java | 66 +++++++++++++++++++ ...JsonPolarisRateLimiterLimitedFallback.java | 39 +++++++++++ .../filter/QuotaCheckReactiveFilterTest.java | 55 ++++++++++++++-- .../filter/QuotaCheckServletFilterTest.java | 55 ++++++++++++++-- ...JsonPolarisRateLimiterLimitedFallback.java | 41 ++++++++++++ 9 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/spi/PolarisRateLimiterLimitedFallback.java create mode 100644 spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/JsonPolarisRateLimiterLimitedFallback.java create mode 100644 spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/JsonPolarisRateLimiterLimitedFallback.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abbeba9..9392941a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,4 @@ - [docs:Add license checker GitHub Action.](https://github.com/Tencent/spring-cloud-tencent/pull/840) - [refactor:move loadbalancer to discovery module.](https://github.com/Tencent/spring-cloud-tencent/pull/846) - [feat:update spring framework version of 2022 branch.](https://github.com/Tencent/spring-cloud-tencent/pull/851) +- [feature:add PolarisRateLimiterLimitedFallback spi.](https://github.com/Tencent/spring-cloud-tencent/pull/857) diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitAutoConfiguration.java index 8f2520d0..7da7599f 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/config/PolarisRateLimitAutoConfiguration.java @@ -26,6 +26,7 @@ import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckReactiveFilter; import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.polaris.client.api.SDKContext; import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.factory.LimitAPIFactory; @@ -78,9 +79,10 @@ public class PolarisRateLimitAutoConfiguration { public QuotaCheckServletFilter quotaCheckFilter(LimitAPI limitAPI, @Nullable PolarisRateLimiterLabelServletResolver labelResolver, PolarisRateLimitProperties polarisRateLimitProperties, - RateLimitRuleLabelResolver rateLimitRuleLabelResolver) { + RateLimitRuleLabelResolver rateLimitRuleLabelResolver, + @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { return new QuotaCheckServletFilter(limitAPI, labelResolver, - polarisRateLimitProperties, rateLimitRuleLabelResolver); + polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); } @Bean @@ -106,9 +108,10 @@ public class PolarisRateLimitAutoConfiguration { public QuotaCheckReactiveFilter quotaCheckReactiveFilter(LimitAPI limitAPI, @Nullable PolarisRateLimiterLabelReactiveResolver labelResolver, PolarisRateLimitProperties polarisRateLimitProperties, - RateLimitRuleLabelResolver rateLimitRuleLabelResolver) { + RateLimitRuleLabelResolver rateLimitRuleLabelResolver, + @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { return new QuotaCheckReactiveFilter(limitAPI, labelResolver, - polarisRateLimitProperties, rateLimitRuleLabelResolver); + polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); } } } 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 7cdd6f60..dc2e2140 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 @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import com.google.common.collect.Maps; @@ -31,6 +32,7 @@ import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils; import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils; import com.tencent.polaris.ratelimit.api.core.LimitAPI; @@ -46,6 +48,7 @@ import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -69,16 +72,21 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered { private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver; + private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; + + private String rejectTips; public QuotaCheckReactiveFilter(LimitAPI limitAPI, - PolarisRateLimiterLabelReactiveResolver labelResolver, - PolarisRateLimitProperties polarisRateLimitProperties, - RateLimitRuleLabelResolver rateLimitRuleLabelResolver) { + PolarisRateLimiterLabelReactiveResolver labelResolver, + PolarisRateLimitProperties polarisRateLimitProperties, + RateLimitRuleLabelResolver rateLimitRuleLabelResolver, + @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { this.limitAPI = limitAPI; this.labelResolver = labelResolver; this.polarisRateLimitProperties = polarisRateLimitProperties; this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver; + this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback; } @PostConstruct @@ -105,10 +113,19 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered { if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { ServerHttpResponse response = exchange.getResponse(); - response.setRawStatusCode(polarisRateLimitProperties.getRejectHttpCode()); - response.getHeaders().setContentType(MediaType.TEXT_HTML); - DataBuffer dataBuffer = response.bufferFactory().allocateBuffer() - .write(rejectTips.getBytes(StandardCharsets.UTF_8)); + DataBuffer dataBuffer; + if (!Objects.isNull(polarisRateLimiterLimitedFallback)) { + response.setRawStatusCode(polarisRateLimiterLimitedFallback.rejectHttpCode()); + response.getHeaders().setContentType(polarisRateLimiterLimitedFallback.mediaType()); + dataBuffer = response.bufferFactory().allocateBuffer() + .write(polarisRateLimiterLimitedFallback.rejectTips().getBytes(polarisRateLimiterLimitedFallback.charset())); + } + else { + response.setRawStatusCode(polarisRateLimitProperties.getRejectHttpCode()); + response.getHeaders().setContentType(MediaType.TEXT_HTML); + dataBuffer = response.bufferFactory().allocateBuffer() + .write(rejectTips.getBytes(StandardCharsets.UTF_8)); + } return response.writeWith(Mono.just(dataBuffer)); } // Unirate 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 734656a9..a9df1f8c 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 @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import com.tencent.cloud.common.metadata.MetadataContext; @@ -30,6 +31,7 @@ import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.cloud.polaris.ratelimit.utils.QuotaCheckUtils; import com.tencent.cloud.polaris.ratelimit.utils.RateLimitUtils; import com.tencent.polaris.ratelimit.api.core.LimitAPI; @@ -45,7 +47,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.web.filter.OncePerRequestFilter; import static com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant.LABEL_METHOD; @@ -71,16 +75,20 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter { private final RateLimitRuleLabelResolver rateLimitRuleLabelResolver; + private final PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; + private String rejectTips; public QuotaCheckServletFilter(LimitAPI limitAPI, PolarisRateLimiterLabelServletResolver labelResolver, PolarisRateLimitProperties polarisRateLimitProperties, - RateLimitRuleLabelResolver rateLimitRuleLabelResolver) { + RateLimitRuleLabelResolver rateLimitRuleLabelResolver, + @Nullable PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback) { this.limitAPI = limitAPI; this.labelResolver = labelResolver; this.polarisRateLimitProperties = polarisRateLimitProperties; this.rateLimitRuleLabelResolver = rateLimitRuleLabelResolver; + this.polarisRateLimiterLimitedFallback = polarisRateLimiterLimitedFallback; } @PostConstruct @@ -102,9 +110,17 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter { localNamespace, localService, 1, labels, request.getRequestURI()); if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { - response.setStatus(polarisRateLimitProperties.getRejectHttpCode()); - response.setContentType("text/html;charset=UTF-8"); - response.getWriter().write(rejectTips); + if (!Objects.isNull(polarisRateLimiterLimitedFallback)) { + response.setStatus(polarisRateLimiterLimitedFallback.rejectHttpCode()); + String contentType = new MediaType(polarisRateLimiterLimitedFallback.mediaType(), polarisRateLimiterLimitedFallback.charset()).toString(); + response.setContentType(contentType); + response.getWriter().write(polarisRateLimiterLimitedFallback.rejectTips()); + } + else { + response.setStatus(polarisRateLimitProperties.getRejectHttpCode()); + response.setContentType("text/html;charset=UTF-8"); + response.getWriter().write(rejectTips); + } return; } // Unirate diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/spi/PolarisRateLimiterLimitedFallback.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/spi/PolarisRateLimiterLimitedFallback.java new file mode 100644 index 00000000..d1a30142 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/spi/PolarisRateLimiterLimitedFallback.java @@ -0,0 +1,66 @@ +/* + * 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.spi; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +/** + * PolarisRateLimiterLimitedFallback. + * + * @author Lingxiao.Wu + */ +public interface PolarisRateLimiterLimitedFallback { + + /** + * Customized mediaType when polaris rateLimiter limited. + * + * @return Customized mediaType + */ + default MediaType mediaType() { + return MediaType.TEXT_HTML; + } + + /** + * Customized charset when polaris rateLimiter limited. + * + * @return Customized charset + */ + default Charset charset() { + return StandardCharsets.UTF_8; + } + + /** + * Customized rejectHttpCode when polaris rateLimiter limited. + * + * @return Customized rejectHttpCode + */ + default Integer rejectHttpCode() { + return HttpStatus.TOO_MANY_REQUESTS.value(); + } + + /** + * Customized rejectTips when polaris rateLimiter limited. + * + * @return Customized rejectTips + */ + String rejectTips(); +} diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/JsonPolarisRateLimiterLimitedFallback.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/JsonPolarisRateLimiterLimitedFallback.java new file mode 100644 index 00000000..43f95dda --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/test/java/com/tencent/cloud/polaris/ratelimit/filter/JsonPolarisRateLimiterLimitedFallback.java @@ -0,0 +1,39 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.ratelimit.filter; + +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; + +import org.springframework.http.MediaType; + +/** + * JsonPolarisRateLimiterLimitedFallback. + * + * @author Lingxiao.Wu + */ +public class JsonPolarisRateLimiterLimitedFallback implements PolarisRateLimiterLimitedFallback { + @Override + public MediaType mediaType() { + return MediaType.APPLICATION_JSON; + } + + @Override + public String rejectTips() { + return "{errMsg:RejectRequestTips提示消息}"; + } +} 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 index 61bffb21..0e5b9d3d 100644 --- 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 @@ -31,6 +31,7 @@ import com.tencent.cloud.polaris.ratelimit.RateLimitRuleLabelResolver; import com.tencent.cloud.polaris.ratelimit.config.PolarisRateLimitProperties; import com.tencent.cloud.polaris.ratelimit.constant.RateLimitConstant; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; @@ -78,6 +79,8 @@ public class QuotaCheckReactiveFilterTest { private final PolarisRateLimiterLabelReactiveResolver labelResolver = exchange -> Collections.singletonMap("ReactiveResolver", "ReactiveResolver"); private QuotaCheckReactiveFilter quotaCheckReactiveFilter; + private QuotaCheckReactiveFilter quotaCheckWithRateLimiterLimitedFallbackReactiveFilter; + private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; @BeforeClass public static void beforeClass() { @@ -99,6 +102,7 @@ public class QuotaCheckReactiveFilterTest { @Before public void setUp() { MetadataContext.LOCAL_NAMESPACE = "TEST"; + polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback(); LimitAPI limitAPI = mock(LimitAPI.class); when(limitAPI.getQuota(any(QuotaRequest.class))).thenAnswer(invocationOnMock -> { @@ -126,7 +130,9 @@ public class QuotaCheckReactiveFilterTest { .thenReturn(Collections.emptySet()); this.quotaCheckReactiveFilter = new QuotaCheckReactiveFilter( - limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver); + limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null); + this.quotaCheckWithRateLimiterLimitedFallbackReactiveFilter = new QuotaCheckReactiveFilter( + limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); } @Test @@ -167,12 +173,12 @@ public class QuotaCheckReactiveFilterTest { PolarisRateLimiterLabelReactiveResolver exceptionLabelResolver = exchange1 -> { throw new RuntimeException("Mock exception."); }; - quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, exceptionLabelResolver, null, null); + quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, exceptionLabelResolver, null, null, null); result = (Map) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange); assertThat(result.size()).isEqualTo(0); // labelResolver == null - quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, null, null, null); + quotaCheckReactiveFilter = new QuotaCheckReactiveFilter(null, null, null, null, null); result = (Map) getCustomResolvedLabels.invoke(quotaCheckReactiveFilter, exchange); assertThat(result.size()).isEqualTo(0); @@ -225,8 +231,49 @@ public class QuotaCheckReactiveFilterTest { quotaCheckReactiveFilter.filter(exchange, webFilterChain); } + @Test + public void polarisRateLimiterLimitedFallbackTest() { + // Create mock WebFilterChain + WebFilterChain webFilterChain = serverWebExchange -> Mono.empty(); + + // Mock request + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/test").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + quotaCheckWithRateLimiterLimitedFallbackReactiveFilter.init(); + + // Pass + MetadataContext.LOCAL_SERVICE = "TestApp1"; + quotaCheckWithRateLimiterLimitedFallbackReactiveFilter.filter(exchange, webFilterChain); + + // Unirate waiting 1000ms + MetadataContext.LOCAL_SERVICE = "TestApp2"; + long startTimestamp = System.currentTimeMillis(); + CountDownLatch countDownLatch = new CountDownLatch(1); + quotaCheckWithRateLimiterLimitedFallbackReactiveFilter.filter(exchange, webFilterChain).subscribe(e -> { + }, t -> { + }, countDownLatch::countDown); + try { + countDownLatch.await(); + } + catch (InterruptedException e) { + fail("Exception encountered.", e); + } + assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); + + // Rate limited + MetadataContext.LOCAL_SERVICE = "TestApp3"; + quotaCheckWithRateLimiterLimitedFallbackReactiveFilter.filter(exchange, webFilterChain); + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getRawStatusCode()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode()); + assertThat(response.getHeaders().getContentType()).isEqualTo(polarisRateLimiterLimitedFallback.mediaType()); + + // Exception + MetadataContext.LOCAL_SERVICE = "TestApp4"; + quotaCheckWithRateLimiterLimitedFallbackReactiveFilter.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 index 031b4278..44f09146 100644 --- 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 @@ -30,6 +30,7 @@ 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.spi.PolarisRateLimiterLabelServletResolver; +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; import com.tencent.polaris.api.plugin.ratelimiter.QuotaResult; import com.tencent.polaris.ratelimit.api.core.LimitAPI; import com.tencent.polaris.ratelimit.api.rpc.QuotaRequest; @@ -48,6 +49,7 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.server.ServerWebExchange; @@ -78,6 +80,8 @@ public class QuotaCheckServletFilterTest { exchange -> Collections.singletonMap("ServletResolver", "ServletResolver"); private QuotaCheckServletFilter quotaCheckServletFilter; private QuotaCheckServletFilter quotaCheckWithHtmlRejectTipsServletFilter; + private QuotaCheckServletFilter quotaCheckWithRateLimiterLimitedFallbackFilter; + private PolarisRateLimiterLimitedFallback polarisRateLimiterLimitedFallback; @BeforeClass public static void beforeClass() { @@ -128,9 +132,12 @@ public class QuotaCheckServletFilterTest { RateLimitRuleLabelResolver rateLimitRuleLabelResolver = mock(RateLimitRuleLabelResolver.class); when(rateLimitRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString())).thenReturn(Collections.emptySet()); - this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver); + this.quotaCheckServletFilter = new QuotaCheckServletFilter(limitAPI, labelResolver, polarisRateLimitProperties, rateLimitRuleLabelResolver, null); this.quotaCheckWithHtmlRejectTipsServletFilter = new QuotaCheckServletFilter( - limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver); + limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, null); + polarisRateLimiterLimitedFallback = new JsonPolarisRateLimiterLimitedFallback(); + this.quotaCheckWithRateLimiterLimitedFallbackFilter = new QuotaCheckServletFilter( + limitAPI, labelResolver, polarisRateLimitWithHtmlRejectTipsProperties, rateLimitRuleLabelResolver, polarisRateLimiterLimitedFallback); } @Test @@ -153,6 +160,7 @@ public class QuotaCheckServletFilterTest { catch (NoSuchFieldException | IllegalAccessException e) { fail("Exception encountered.", e); } + quotaCheckWithRateLimiterLimitedFallbackFilter.init(); } @Test @@ -173,12 +181,12 @@ public class QuotaCheckServletFilterTest { PolarisRateLimiterLabelServletResolver exceptionLabelResolver = request1 -> { throw new RuntimeException("Mock exception."); }; - quotaCheckServletFilter = new QuotaCheckServletFilter(null, exceptionLabelResolver, null, null); + quotaCheckServletFilter = new QuotaCheckServletFilter(null, exceptionLabelResolver, null, null, null); result = (Map) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request); assertThat(result.size()).isEqualTo(0); // labelResolver == null - quotaCheckServletFilter = new QuotaCheckServletFilter(null, null, null, null); + quotaCheckServletFilter = new QuotaCheckServletFilter(null, null, null, null, null); result = (Map) getCustomResolvedLabels.invoke(quotaCheckServletFilter, request); assertThat(result.size()).isEqualTo(0); @@ -230,9 +238,46 @@ public class QuotaCheckServletFilterTest { fail("Exception encountered.", e); } } + @Test + public void polarisRateLimiterLimitedFallbackTest() { + // Create mock FilterChain + FilterChain filterChain = (servletRequest, servletResponse) -> { + }; + + // Mock request + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + quotaCheckWithRateLimiterLimitedFallbackFilter.init(); + try { + // Pass + MetadataContext.LOCAL_SERVICE = "TestApp1"; + quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); + + // Unirate waiting 1000ms + MetadataContext.LOCAL_SERVICE = "TestApp2"; + long startTimestamp = System.currentTimeMillis(); + quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); + assertThat(System.currentTimeMillis() - startTimestamp).isGreaterThanOrEqualTo(1000L); + + // Rate limited + MetadataContext.LOCAL_SERVICE = "TestApp3"; + String contentType = new MediaType(polarisRateLimiterLimitedFallback.mediaType(), polarisRateLimiterLimitedFallback.charset()).toString(); + quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(polarisRateLimiterLimitedFallback.rejectHttpCode()); + assertThat(response.getContentAsString()).isEqualTo(polarisRateLimiterLimitedFallback.rejectTips()); + assertThat(response.getContentType()).isEqualTo(contentType); + + // Exception + MetadataContext.LOCAL_SERVICE = "TestApp4"; + quotaCheckWithRateLimiterLimitedFallbackFilter.doFilterInternal(request, response, filterChain); + } + catch (ServletException | IOException e) { + fail("Exception encountered.", e); + } + } @SpringBootApplication protected static class TestApplication { - } } diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/JsonPolarisRateLimiterLimitedFallback.java b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/JsonPolarisRateLimiterLimitedFallback.java new file mode 100644 index 00000000..c6979728 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/JsonPolarisRateLimiterLimitedFallback.java @@ -0,0 +1,41 @@ +/* + * 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 com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLimitedFallback; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +/** + * JsonPolarisRateLimiterLimitedFallback. + * + * @author Lingxiao.Wu + */ +@Component +public class JsonPolarisRateLimiterLimitedFallback implements PolarisRateLimiterLimitedFallback { + @Override + public MediaType mediaType() { + return MediaType.APPLICATION_JSON; + } + + @Override + public String rejectTips() { + return "{\"success\":false,\"code\":429,\"msg\":\"RejectRequestTips提示消息\",\"timestamp\":1673415310479}"; + } +}