From 8bd2dc364988ead27f1380b84a2fa5f7db71e9e8 Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sun, 18 Aug 2024 23:35:16 +0800 Subject: [PATCH 1/8] fix issue 2485 which occur oom when using async servlet request. --- .../webmvc/AbstractSentinelInterceptor.java | 31 ++++++++++++++-- .../SentinelSpringMvcIntegrationTest.java | 16 +++++++- .../webmvc/controller/TestController.java | 14 +++++++ .../webmvc/config/InterceptorConfig.java | 37 +++++++++++++++++++ .../controller/WebMvcTestController.java | 17 +++++++-- 5 files changed, 106 insertions(+), 9 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java index e793aac73c..4ffa68db16 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java @@ -30,7 +30,8 @@ import com.alibaba.csp.sentinel.util.AssertUtil; import com.alibaba.csp.sentinel.util.StringUtil; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** @@ -52,7 +53,7 @@ * @author kaizi2009 * @since 1.7.1 */ -public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { +public abstract class AbstractSentinelInterceptor implements AsyncHandlerInterceptor { public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context"; private static final String EMPTY_ORIGIN = ""; @@ -133,13 +134,35 @@ protected String getContextName(HttpServletRequest request) { return SENTINEL_SPRING_WEB_CONTEXT_NAME; } + + /** + * When a handler starts an asynchronous request, the DispatcherServlet exits without invoking postHandle and afterCompletion + * Called instead of postHandle and afterCompletion to exit the context and clean thread-local variables when the handler is being executed concurrently. + * @param request the current request + * @param response the current response + * @param handler the handler (or {@link HandlerMethod}) that started async + * execution, for type and/or instance examination + */ + @Override + public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + exitContext(request); + } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + exitContext(request, ex); + } + + private void exitContext(HttpServletRequest request) { + exitContext(request, null); + } + + private void exitContext(HttpServletRequest request, Exception ex) { if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) { return; } - + Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName()); if (entry == null) { // should not happen @@ -147,7 +170,7 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp getClass().getSimpleName(), baseWebMvcConfig.getRequestAttributeName()); return; } - + traceExceptionAndExit(entry, ex); removeEntryInRequest(request); ContextUtil.exit(); diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java index f1ddb937ce..7c80e1f1b4 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java @@ -15,12 +15,12 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; @@ -64,6 +64,18 @@ public void testBase() throws Exception { assertEquals(1, cn.passQps(), 0.01); } + @Test + public void testAsync() throws Exception { + String url = "/async"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + assertNull(ContextUtil.getContext()); + } + @Test public void testOriginParser() throws Exception { String springMvcPathVariableUrl = "/foo/{id}"; diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java index d8a42ab8fb..a5d91c86b4 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java @@ -18,7 +18,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; /** * @author kaizi2009 @@ -52,4 +54,16 @@ public String apiExclude(@PathVariable("id") Long id) { return "Exclude " + id; } + @GetMapping("/async") + @ResponseBody + public DeferredResult distribute() throws Exception{ + DeferredResult result = new DeferredResult<>(); + + Thread thread = new Thread(() -> result.setResult("async result.")); + thread.start(); + + Thread.yield(); + return result; + } + } diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java index c90f51658e..8753be944d 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java @@ -21,10 +21,19 @@ import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** * Config sentinel interceptor * @@ -33,6 +42,8 @@ @Configuration public class InterceptorConfig implements WebMvcConfigurer { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + @Override public void addInterceptors(InterceptorRegistry registry) { // Add Sentinel interceptor @@ -63,6 +74,31 @@ private void addSpringMvcInterceptor(InterceptorRegistry registry) { // Add sentinel interceptor registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + + //Add sentinel interceptor call info + registry.addInterceptor(new AsyncHandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ + logger.info("preHandle: dispatcher type {}, {}, {}, {}", request.getDispatcherType(), ContextUtil.contextSize(), request.getRequestURI(), request.getAttribute(config.getRequestRefName())); + return true; + } + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + + logger.info("postHandle: dispatcher type {}, {}, {},{}", request.getDispatcherType(), ContextUtil.contextSize(), request.getRequestURI(), request.getAttribute(config.getRequestRefName())); + } + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable Exception ex) throws Exception { + logger.info("afterCompletion: dispatcher type {}, {},{},{}", request.getDispatcherType(), ContextUtil.contextSize(), request.getRequestURI(), request.getAttribute(config.getRequestRefName())); + } + + public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + logger.info("afterConcurrentHandlingStarted: dispatcher type {}, {},{},{}", request.getDispatcherType(), ContextUtil.contextSize(), request.getRequestURI(), request.getAttribute(config.getRequestRefName())); + + } + + }).addPathPatterns("/**"); } private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { @@ -75,5 +111,6 @@ private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { //Add sentinel interceptor registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); + } } diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java index ac2aa97635..8fafc1c14d 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -17,10 +17,10 @@ import java.util.Random; import java.util.concurrent.TimeUnit; + import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.servlet.ModelAndView; /** @@ -65,6 +65,17 @@ public ModelAndView apiForward() { return mav; } + @GetMapping("/async") + @ResponseBody + public DeferredResult distribute() throws Exception{ +// return new DeferredResult(5000L, (Supplier) () -> {doBusiness(); return "err";}); + DeferredResult result = new DeferredResult<>(4000L); + + Thread thread = new Thread(() -> result.setResult("async result")); + thread.start(); + + return result; + } private void doBusiness() { Random random = new Random(1); try { From ec11cb71858a1525a2a8467e4473bcc23f932f79 Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Tue, 20 Aug 2024 20:52:32 +0800 Subject: [PATCH 2/8] optimize imports --- .../spring/webmvc/SentinelSpringMvcIntegrationTest.java | 4 +++- .../demo/spring/webmvc/controller/WebMvcTestController.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java index 73700784c4..7f8bbc051e 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java @@ -15,7 +15,9 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java index 8fafc1c14d..f6c5c2d34b 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -19,7 +19,9 @@ import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.servlet.ModelAndView; From 22b6688044c476dded98bc1d0471a2e5312878c9 Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Thu, 22 Aug 2024 22:04:02 +0800 Subject: [PATCH 3/8] 1. fix the same issue in the webmvc-v6x 2. improve based on review comments --- .../webmvc/AbstractSentinelInterceptor.java | 36 ++++++++++--------- .../AbstractSentinelInterceptor.java | 35 ++++++++++++++++-- .../SentinelSpringMvcIntegrationTest.java | 16 ++++++++- .../webmvc_v6x/controller/TestController.java | 14 ++++++++ .../controller/WebMvcTestController.java | 7 ++-- 5 files changed, 84 insertions(+), 24 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java index f4d5a459ec..f26e07947f 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java @@ -52,7 +52,7 @@ * return mav; * } * - * + * * @author kaizi2009 * @since 1.7.1 */ @@ -68,12 +68,12 @@ public AbstractSentinelInterceptor(BaseWebMvcConfig config) { AssertUtil.assertNotBlank(config.getRequestAttributeName(), "requestAttributeName should not be blank"); this.baseWebMvcConfig = config; } - + /** * @param request * @param rcKey * @param step - * @return reference count after increasing (initial value as zero to be increased) + * @return reference count after increasing (initial value as zero to be increased) */ private Integer increaseReference(HttpServletRequest request, String rcKey, int step) { Object obj = request.getAttribute(rcKey); @@ -87,10 +87,10 @@ private Integer increaseReference(HttpServletRequest request, String rcKey, int request.setAttribute(rcKey, newRc); return newRc; } - + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { + throws Exception { try { String resourceName = getResourceName(request); @@ -101,7 +101,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) { return true; } - + // Parse the request origin using registered origin parser. String origin = parseOrigin(request); String contextName = getContextName(request); @@ -141,27 +141,29 @@ protected String getContextName(HttpServletRequest request) { /** * When a handler starts an asynchronous request, the DispatcherServlet exits without invoking postHandle and afterCompletion * Called instead of postHandle and afterCompletion to exit the context and clean thread-local variables when the handler is being executed concurrently. - * @param request the current request + * + * @param request the current request * @param response the current response - * @param handler the handler (or {@link HandlerMethod}) that started async - * execution, for type and/or instance examination + * @param handler the handler (or {@link HandlerMethod}) that started async + * execution, for type and/or instance examination */ @Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { - exitContext(request); + Object handler) throws Exception { + exit(request); } + @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - exitContext(request, ex); + exit(request, ex); } - private void exitContext(HttpServletRequest request) { - exitContext(request, null); + private void exit(HttpServletRequest request) { + exit(request, null); } - private void exitContext(HttpServletRequest request, Exception ex) { + private void exit(HttpServletRequest request, Exception ex) { if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) { return; } @@ -186,7 +188,7 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response, protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) { Object entryObject = request.getAttribute(attrKey); - return entryObject == null ? null : (Entry)entryObject; + return entryObject == null ? null : (Entry) entryObject; } protected void removeEntryInRequest(HttpServletRequest request) { @@ -212,7 +214,7 @@ && increaseReference(request, this.baseWebMvcConfig.getRequestRefName() + ":" + } protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e) - throws Exception { + throws Exception { if (baseWebMvcConfig.getBlockExceptionHandler() != null) { baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e); } else { diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java index 271d1442da..dc2e273add 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java @@ -15,7 +15,11 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; -import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.BaseWebMvcConfig; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.log.RecordLog; @@ -24,7 +28,8 @@ import com.alibaba.csp.sentinel.util.StringUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** @@ -45,7 +50,7 @@ * * @since 1.8.8 */ -public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { +public abstract class AbstractSentinelInterceptor implements AsyncHandlerInterceptor { public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context"; private static final String EMPTY_ORIGIN = ""; @@ -124,9 +129,33 @@ protected String getContextName(HttpServletRequest request) { return SENTINEL_SPRING_WEB_CONTEXT_NAME; } + + /** + * When a handler starts an asynchronous request, the DispatcherServlet exits without invoking postHandle and afterCompletion + * Called instead of postHandle and afterCompletion to exit the context and clean thread-local variables when the handler is being executed concurrently. + * + * @param request the current request + * @param response the current response + * @param handler the handler (or {@link HandlerMethod}) that started async + * execution, for type and/or instance examination + */ + @Override + public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + exit(request); + } + @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + exit(request, ex); + } + + private void exit(HttpServletRequest request) { + exit(request, null); + } + + private void exit(HttpServletRequest request, Exception ex) { if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) { return; } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java index 69d21a325a..f7e7ac7796 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java @@ -17,10 +17,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; @@ -64,6 +66,18 @@ public void testBase() throws Exception { assertEquals(1, cn.passQps(), 0.01); } + @Test + public void testAsync() throws Exception { + String url = "/async"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + assertNull(ContextUtil.getContext()); + } + @Test public void testOriginParser() throws Exception { String springMvcPathVariableUrl = "/foo/{id}"; @@ -78,7 +92,7 @@ public void testOriginParser() throws Exception { // This will be blocked since the caller is same: userA this.mvc.perform( - get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) .andExpect(status().isOk()) .andExpect(content().json(ResultWrapper.blocked().toJsonString())); diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java index 9b9ecfe25b..cf16bff4db 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java @@ -18,7 +18,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; /** * @author kaizi2009 @@ -52,4 +54,16 @@ public String apiExclude(@PathVariable("id") Long id) { return "Exclude " + id; } + @GetMapping("/async") + @ResponseBody + public DeferredResult distribute() throws Exception { + DeferredResult result = new DeferredResult<>(); + + Thread thread = new Thread(() -> result.setResult("async result.")); + thread.start(); + + Thread.yield(); + return result; + } + } diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java index f6c5c2d34b..178762ec12 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -27,6 +27,7 @@ /** * Test controller + * * @author kaizi2009 */ @Controller @@ -59,7 +60,7 @@ public String apiExclude(@PathVariable("id") Long id) { doBusiness(); return "Exclude " + id; } - + @GetMapping("/forward") public ModelAndView apiForward() { ModelAndView mav = new ModelAndView(); @@ -69,8 +70,7 @@ public ModelAndView apiForward() { @GetMapping("/async") @ResponseBody - public DeferredResult distribute() throws Exception{ -// return new DeferredResult(5000L, (Supplier) () -> {doBusiness(); return "err";}); + public DeferredResult distribute() throws Exception { DeferredResult result = new DeferredResult<>(4000L); Thread thread = new Thread(() -> result.setResult("async result")); @@ -78,6 +78,7 @@ public DeferredResult distribute() throws Exception{ return result; } + private void doBusiness() { Random random = new Random(1); try { From 39b22a3b901831cc5b95dc9b2089acf0eb3b2ada Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sat, 24 Aug 2024 21:45:08 +0800 Subject: [PATCH 4/8] 1. fix the issue#3443, set appropriate status and message for all type of BlockException. --- .../sentinel-spring-webmvc-adapter/pom.xml | 4 + .../DefaultBlockExceptionHandler.java | 9 +- ...tinelDefaultBlockExceptionHandlerTest.java | 159 ++++++++++++++++++ .../config/DefaultInterceptorConfig.java | 60 +++++++ .../sentinel-web-adapter-common/pom.xml | 8 + .../common/DefaultBlockExceptionResponse.java | 67 ++++++++ 6 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/DefaultInterceptorConfig.java create mode 100644 sentinel-adapter/sentinel-web-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/web/common/DefaultBlockExceptionResponse.java diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml index 001445ec3e..673da94d80 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml @@ -23,6 +23,10 @@ com.alibaba.csp sentinel-core + + com.alibaba.csp + sentinel-web-adapter-common + javax.servlet javax.servlet-api diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java index 0ad80095af..ba4ed67d1c 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java @@ -15,8 +15,8 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc.callback; +import com.alibaba.csp.sentinel.adapter.web.common.DefaultBlockExceptionResponse; import com.alibaba.csp.sentinel.slots.block.BlockException; -import com.alibaba.csp.sentinel.util.StringUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -31,11 +31,10 @@ public class DefaultBlockExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { - // Return 429 (Too Many Requests) by default. - response.setStatus(429); - + DefaultBlockExceptionResponse expRes = DefaultBlockExceptionResponse.resolve(e.getClass()); + response.setStatus(expRes.getStatus()); PrintWriter out = response.getWriter(); - out.print("Blocked by Sentinel (flow limiting)"); + out.print(expRes.getMsg()); out.flush(); out.close(); } diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java new file mode 100644 index 0000000000..3f4df87e1d --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java @@ -0,0 +1,159 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.DefaultInterceptorConfig; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.InterceptorConfig; +import com.alibaba.csp.sentinel.adapter.web.common.DefaultBlockExceptionResponse; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Lingzhi + */ +@RunWith(SpringRunner.class) +@Import(DefaultInterceptorConfig.class) +@WebMvcTest(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = InterceptorConfig.class)) +public class SentinelDefaultBlockExceptionHandlerTest { + @Autowired + private MockMvc mvc; + + @Test + public void testOriginParser() throws Exception { + String springMvcPathVariableUrl = "/foo/{id}"; + String limitOrigin = "userA"; + final String headerName = "S-User"; + configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin); + + // This will be passed since the caller is different: userB + this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) + .andExpect(status().isOk()) + .andExpect(content().string("foo 1")); + + // This will be blocked since the caller is same: userA + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.FLOW_EXCEPTION; + this.mvc.perform( + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + + // This will be passed since the caller is different: "" + this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("foo 3")); + + FlowRuleManager.loadRules(null); + } + + @Test + public void testRuntimeException() throws Exception { + String url = "/runtimeException"; + configureExceptionRulesFor(url, 3, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.error().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and response json. + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.resolve(FlowException.class); + this.mvc.perform(get(url)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + } + + + @Test + public void testExceptionPerception() throws Exception { + String url = "/bizException"; + configureExceptionDegradeRulesFor(url, 2.6, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(new ResultWrapper(-1, "Biz error").toJsonString())); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and response. + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.resolve(DegradeException.class); + this.mvc.perform(get(url)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule().setCount(count).setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule().setCount(count).setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionDegradeRulesFor(String resource, double count, String limitApp) { + DegradeRule rule = new DegradeRule().setCount(count) + .setStatIntervalMs(1000).setMinRequestAmount(1) + .setTimeWindow(5).setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + DegradeRuleManager.loadRules(Collections.singletonList(rule)); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/DefaultInterceptorConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/DefaultInterceptorConfig.java new file mode 100644 index 0000000000..5e81c52d9c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/DefaultInterceptorConfig.java @@ -0,0 +1,60 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelExceptionAware; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Interceptor Config using DefaultBlockExceptionHandler + * + * @author Lingzhi + */ +@TestConfiguration +public class DefaultInterceptorConfig implements WebMvcConfigurer { + + @Bean + public SentinelExceptionAware sentinelExceptionAware() { + return new SentinelExceptionAware(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //Add sentinel interceptor + addSpringMvcInterceptor(registry); + + //If you want to sentinel the total flow, you can add total interceptor + addSpringMvcTotalInterceptor(registry); + } + + private void addSpringMvcInterceptor(InterceptorRegistry registry) { + //Config + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + + config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + + //Custom configuration if necessary + config.setHttpMethodSpecify(false); + config.setWebContextUnify(true); + config.setOriginParser(request -> request.getHeader("S-user")); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + } + + private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { + //Configure + SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); + + //Custom configuration if necessary + config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container"); + config.setTotalResourceName("my_spring_mvc_total_url_request"); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); + } +} diff --git a/sentinel-adapter/sentinel-web-adapter-common/pom.xml b/sentinel-adapter/sentinel-web-adapter-common/pom.xml index cf7fc0f953..870d72ab51 100644 --- a/sentinel-adapter/sentinel-web-adapter-common/pom.xml +++ b/sentinel-adapter/sentinel-web-adapter-common/pom.xml @@ -15,6 +15,14 @@ 8 + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-parameter-flow-control + junit junit diff --git a/sentinel-adapter/sentinel-web-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/web/common/DefaultBlockExceptionResponse.java b/sentinel-adapter/sentinel-web-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/web/common/DefaultBlockExceptionResponse.java new file mode 100644 index 0000000000..14d04e15f5 --- /dev/null +++ b/sentinel-adapter/sentinel-web-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/web/common/DefaultBlockExceptionResponse.java @@ -0,0 +1,67 @@ +package com.alibaba.csp.sentinel.adapter.web.common; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowException; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException; +import com.alibaba.csp.sentinel.slots.system.SystemBlockException; + +/** + * Default BlockException Response + * + * @author Lingzhi + */ +public enum DefaultBlockExceptionResponse { + //Too Many Request + FLOW_EXCEPTION(FlowException.class, 429, "Blocked by Sentinel (flow limiting)"), + //Too Many Request + PARAM_FLOW_EXCEPTION(ParamFlowException.class, 429, "Blocked by Sentinel (frequent parameter flow limiting)"), + //Service Unavailable + DEGRADE_EXCEPTION(DegradeException.class, 503, "Blocked by Sentinel (circuit breaker)"), + //Forbidden + AUTHORITY_EXCEPTION(AuthorityException.class, 403, "Blocked by Sentinel (origin request limiting)"), + //Too Many Request + SYSTEM_BLOCK_EXCEPTION(SystemBlockException.class, 429, "Blocked by Sentinel (system limiting)"), + // Bad Request + DEFAULT_BLOCK_EXCEPTION(BlockException.class, 400, "Blocked by Sentinel"); + + private final Class exp; + + private final int status; + private final String msg; + + + private static final DefaultBlockExceptionResponse[] VALUES; + + static { + VALUES = values(); + } + + DefaultBlockExceptionResponse(Class exp, int status, String msg) { + this.exp = exp; + this.status = status; + this.msg = msg; + } + + + public int getStatus() { + return status; + } + + public String getMsg() { + return msg; + } + + public static DefaultBlockExceptionResponse resolve(Class exp) { + // Use cached VALUES instead of values() to prevent array allocation. + for (DefaultBlockExceptionResponse res : VALUES) { + if (res.exp == exp) { + return res; + } + } + return DEFAULT_BLOCK_EXCEPTION; + } +} + + From 056a09660f0546fb8328076e0796178c74bad834 Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sat, 31 Aug 2024 00:19:24 +0800 Subject: [PATCH 5/8] 1. fix the issue#3443 for webmvc-v6x-adapter 2. add the Sentinel Exception Aware for webmvc-v6x-adapter. --- ...tinelDefaultBlockExceptionHandlerTest.java | 1 + .../SentinelSpringMvcIntegrationTest.java | 20 +-- .../AbstractSentinelInterceptor.java | 37 ++-- .../webmvc_v6x/SentinelExceptionAware.java | 26 +++ .../DefaultBlockExceptionHandler.java | 11 +- .../webmvc_v6x/config/BaseWebMvcConfig.java | 5 +- ...tinelDefaultBlockExceptionHandlerTest.java | 162 ++++++++++++++++++ .../SentinelSpringMvcIntegrationTest.java | 56 ++++-- .../DefaultBlockExceptionHandlerTest.java | 6 + .../config/DefaultInterceptorConfig.java | 61 +++++++ .../webmvc_v6x/config/InterceptorConfig.java | 12 +- .../SentinelSpringMvcBlockHandlerConfig.java | 10 +- .../webmvc_v6x/controller/TestController.java | 6 + .../webmvc_v6x/exception/BizException.java | 7 + 14 files changed, 377 insertions(+), 43 deletions(-) create mode 100644 sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelExceptionAware.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/DefaultInterceptorConfig.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/exception/BizException.java diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java index 3f4df87e1d..96ae246548 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java @@ -154,6 +154,7 @@ private void configureExceptionDegradeRulesFor(String resource, double count, St @After public void cleanUp() { FlowRuleManager.loadRules(null); + DegradeRuleManager.loadRules(null); ClusterBuilderSlot.resetClusterNodes(); } } diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java index 7f8bbc051e..5767c7eec0 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java @@ -15,13 +15,6 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; @@ -31,9 +24,6 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; import com.alibaba.csp.sentinel.util.StringUtil; - -import java.util.Collections; - import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,6 +34,13 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * @author kaizi2009 */ @@ -94,7 +91,7 @@ public void testOriginParser() throws Exception { // This will be blocked since the caller is same: userA this.mvc.perform( - get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) .andExpect(status().isOk()) .andExpect(content().json(ResultWrapper.blocked().toJsonString())); @@ -209,6 +206,7 @@ private void configureExceptionDegradeRulesFor(String resource, double count, St @After public void cleanUp() { FlowRuleManager.loadRules(null); + DegradeRuleManager.loadRules(null); ClusterBuilderSlot.resetClusterNodes(); } } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java index dc2e273add..543a2ae871 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java @@ -15,11 +15,7 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; -import com.alibaba.csp.sentinel.Entry; -import com.alibaba.csp.sentinel.EntryType; -import com.alibaba.csp.sentinel.ResourceTypeConstants; -import com.alibaba.csp.sentinel.SphU; -import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.*; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.BaseWebMvcConfig; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.log.RecordLog; @@ -28,10 +24,14 @@ import com.alibaba.csp.sentinel.util.StringUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; +import java.util.Objects; + /** * Since request may be reprocessed in flow if any forwarding or including or other action * happened (see {@link jakarta.servlet.ServletRequest#getDispatcherType()}) we will only @@ -74,7 +74,7 @@ private Integer increaseReference(HttpServletRequest request, String rcKey, int if (obj == null) { // initial - obj = Integer.valueOf(0); + obj = 0; } Integer newRc = (Integer) obj + step; @@ -193,12 +193,21 @@ protected void removeEntryInRequest(HttpServletRequest request) { } protected void traceExceptionAndExit(Entry entry, Exception ex) { - if (entry != null) { - if (ex != null) { - Tracer.traceEntry(ex, entry); - } - entry.exit(); + if (entry == null) { + return; + } + HttpServletRequest request = getHttpServletRequest(); + if (request != null + && ex == null + && increaseReference(request, this.baseWebMvcConfig.getRequestRefName() + ":" + BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, 1) == 1) { + //Each interceptor can only catch exception once + ex = (Exception) request.getAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME); } + + if (ex != null) { + Tracer.traceEntry(ex, entry); + } + entry.exit(); } protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, String resourceName, @@ -228,4 +237,10 @@ protected String parseOrigin(HttpServletRequest request) { return origin; } + private HttpServletRequest getHttpServletRequest() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + return Objects.isNull(servletRequestAttributes) ? null : servletRequestAttributes.getRequest(); + } + } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelExceptionAware.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelExceptionAware.java new file mode 100644 index 0000000000..276f23698d --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelExceptionAware.java @@ -0,0 +1,26 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.BaseWebMvcConfig; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.annotation.Order; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +@Order(-1) +public class SentinelExceptionAware implements HandlerExceptionResolver { + @Override + public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + addExceptionToRequest(request, ex); + return null; + } + + private void addExceptionToRequest(HttpServletRequest httpServletRequest, Exception exception) { + if (BlockException.isBlockException(exception)) { + return; + } + httpServletRequest.setAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, exception); + } +} + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandler.java index 5181da74e3..dcd96094b2 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandler.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandler.java @@ -15,6 +15,7 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback; +import com.alibaba.csp.sentinel.adapter.web.common.DefaultBlockExceptionResponse; import com.alibaba.csp.sentinel.slots.block.BlockException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -29,14 +30,14 @@ public class DefaultBlockExceptionHandler implements BlockExceptionHandler { @Override - public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException ex) + public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception { - // Return 429 (Too Many Requests) by default. - response.setStatus(429); - + DefaultBlockExceptionResponse expRes = DefaultBlockExceptionResponse.resolve(e.getClass()); + response.setStatus(expRes.getStatus()); PrintWriter out = response.getWriter(); - out.print("Blocked by Sentinel (flow limiting)"); + out.print(expRes.getMsg()); out.flush(); out.close(); } + } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/BaseWebMvcConfig.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/BaseWebMvcConfig.java index 84d64b6a3d..0cb3e60814 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/BaseWebMvcConfig.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/BaseWebMvcConfig.java @@ -25,6 +25,7 @@ * @since 1.8.8 */ public abstract class BaseWebMvcConfig { + public final static String REQUEST_REF_EXCEPTION_NAME = "$$sentinel_spring_web_entry_attr-exception"; protected String requestAttributeName; protected String requestRefName; @@ -39,10 +40,10 @@ public void setRequestAttributeName(String requestAttributeName) { this.requestAttributeName = requestAttributeName; this.requestRefName = this.requestAttributeName + "-rc"; } - + /** * Paired with attr name used to track reference count. - * + * * @return */ public String getRequestRefName() { diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java new file mode 100644 index 0000000000..beb299c07c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java @@ -0,0 +1,162 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.DefaultInterceptorConfig; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.InterceptorConfig; +import com.alibaba.csp.sentinel.adapter.web.common.DefaultBlockExceptionResponse; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Lingzhi + */ +@RunWith(SpringRunner.class) +@Import(DefaultInterceptorConfig.class) +@WebMvcTest(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = InterceptorConfig.class)) +public class SentinelDefaultBlockExceptionHandlerTest { + @Autowired + private MockMvc mvc; + + @Test + public void testOriginParser() throws Exception { + String springMvcPathVariableUrl = "/foo/{id}"; + String limitOrigin = "userA"; + final String headerName = "S-User"; + configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin); + + // This will be passed since the caller is different: userB + this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) + .andExpect(status().isOk()) + .andExpect(content().string("foo 1")); + + // This will be blocked since the caller is same: userA + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.FLOW_EXCEPTION; + this.mvc.perform( + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + + // This will be passed since the caller is different: "" + this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("foo 3")); + + FlowRuleManager.loadRules(null); + } + + @Test + public void testRuntimeException() throws Exception { + String url = "/runtimeException"; + configureExceptionRulesFor(url, 3, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.error().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and response json. + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.resolve(FlowException.class); + this.mvc.perform(get(url)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + + FlowRuleManager.loadRules(null); + } + + + @Test + public void testExceptionPerception() throws Exception { + String url = "/bizException"; + configureExceptionDegradeRulesFor(url, 2.6, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(new ResultWrapper(-1, "Biz error").toJsonString())); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and response. + DefaultBlockExceptionResponse res = DefaultBlockExceptionResponse.resolve(DegradeException.class); + this.mvc.perform(get(url)) + .andExpect(status().is(res.getStatus())) + .andExpect(content().string(res.getMsg())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule().setCount(count).setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule().setCount(count).setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionDegradeRulesFor(String resource, double count, String limitApp) { + DegradeRule rule = new DegradeRule().setCount(count) + .setStatIntervalMs(1000).setMinRequestAmount(1) + .setTimeWindow(5).setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + DegradeRuleManager.loadRules(Collections.singletonList(rule)); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + DegradeRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java index f7e7ac7796..b500ed4418 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java @@ -15,23 +15,15 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; import com.alibaba.csp.sentinel.util.StringUtil; - -import java.util.Collections; - import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +34,13 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * @author kaizi2009 */ @@ -142,6 +141,31 @@ public void testRuntimeException() throws Exception { assertEquals(1, cn.blockRequest(), 1); } + @Test + public void testExceptionPerception() throws Exception { + String url = "/bizException"; + configureExceptionDegradeRulesFor(url, 2.6, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(new ResultWrapper(-1, "Biz error").toJsonString())); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and response json. + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.blocked().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + } + private void configureRulesFor(String resource, int count, String limitApp) { FlowRule rule = new FlowRule() .setCount(count) @@ -164,9 +188,21 @@ private void configureExceptionRulesFor(String resource, int count, String limit FlowRuleManager.loadRules(Collections.singletonList(rule)); } + private void configureExceptionDegradeRulesFor(String resource, double count, String limitApp) { + DegradeRule rule = new DegradeRule().setCount(count) + .setStatIntervalMs(1000).setMinRequestAmount(1) + .setTimeWindow(5).setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + DegradeRuleManager.loadRules(Collections.singletonList(rule)); + } + @After public void cleanUp() { FlowRuleManager.loadRules(null); + DegradeRuleManager.loadRules(null); ClusterBuilderSlot.resetClusterNodes(); } } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandlerTest.java index fca59ffe62..205da171b5 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandlerTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/callback/DefaultBlockExceptionHandlerTest.java @@ -1,6 +1,8 @@ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback; +import com.alibaba.csp.sentinel.adapter.web.common.DefaultBlockExceptionResponse; import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -20,6 +22,10 @@ public void handle_writeBlockPage() throws Exception { BlockException ex = new FlowException("msg"); h.handle(req, resp, resourceName, ex); assertEquals(429, resp.getStatus()); + BlockException dex = new DegradeException("msg"); + MockHttpServletResponse dresp = new MockHttpServletResponse(); + h.handle(req, dresp, resourceName, dex); + assertEquals(DefaultBlockExceptionResponse.DEGRADE_EXCEPTION.getStatus(), dresp.getStatus()); } } \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/DefaultInterceptorConfig.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/DefaultInterceptorConfig.java new file mode 100644 index 0000000000..87d52b2117 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/DefaultInterceptorConfig.java @@ -0,0 +1,61 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelExceptionAware; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebTotalInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.DefaultBlockExceptionHandler; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Interceptor Config using DefaultBlockExceptionHandler + * + * @author Lingzhi + */ +@TestConfiguration +public class DefaultInterceptorConfig implements WebMvcConfigurer { + + @Bean + public SentinelExceptionAware sentinelExceptionAware() { + return new SentinelExceptionAware(); + } + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //Add sentinel interceptor + addSpringMvcInterceptor(registry); + + //If you want to sentinel the total flow, you can add total interceptor + addSpringMvcTotalInterceptor(registry); + } + + private void addSpringMvcInterceptor(InterceptorRegistry registry) { + //Config + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + + config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + + //Custom configuration if necessary + config.setHttpMethodSpecify(false); + config.setWebContextUnify(true); + config.setOriginParser(request -> request.getHeader("S-user")); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + } + + private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { + //Configure + SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); + + //Custom configuration if necessary + config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container"); + config.setTotalResourceName("my_spring_mvc_total_url_request"); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/InterceptorConfig.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/InterceptorConfig.java index 363fea2e9b..b2ca3b680b 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/InterceptorConfig.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/InterceptorConfig.java @@ -15,18 +15,19 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelExceptionAware; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebInterceptor; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebTotalInterceptor; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.RequestOriginParser; import com.alibaba.csp.sentinel.slots.block.BlockException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - /** * Config sentinel interceptor * @@ -35,6 +36,11 @@ @Configuration public class InterceptorConfig implements WebMvcConfigurer { + @Bean + public SentinelExceptionAware sentinelExceptionAware() { + return new SentinelExceptionAware(); + } + @Override public void addInterceptors(InterceptorRegistry registry) { //Add sentinel interceptor diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/SentinelSpringMvcBlockHandlerConfig.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/SentinelSpringMvcBlockHandlerConfig.java index 6fab6530a0..40805d273c 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/SentinelSpringMvcBlockHandlerConfig.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/config/SentinelSpringMvcBlockHandlerConfig.java @@ -16,6 +16,7 @@ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.ResultWrapper; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.exception.BizException; import com.alibaba.csp.sentinel.slots.block.AbstractRule; import com.alibaba.csp.sentinel.slots.block.BlockException; import org.slf4j.Logger; @@ -33,7 +34,7 @@ @ControllerAdvice @Order(0) public class SentinelSpringMvcBlockHandlerConfig { - private Logger logger = LoggerFactory.getLogger(this.getClass()); + private final Logger logger = LoggerFactory.getLogger(this.getClass()); @ExceptionHandler(BlockException.class) @ResponseBody @@ -51,4 +52,11 @@ public ResultWrapper exceptionHandler(Exception e) { logger.error("System error", e.getMessage()); return new ResultWrapper(-1, "System error"); } + + @ExceptionHandler(BizException.class) + @ResponseBody + public ResultWrapper bizExceptionHandler(BizException e) { + logger.error("Biz error", e.getMessage()); + return new ResultWrapper(-1, "Biz error"); + } } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java index cf16bff4db..c7cc556139 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java @@ -16,6 +16,7 @@ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.controller; +import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.exception.BizException; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; @@ -49,6 +50,11 @@ public String runtimeException() { return "runtimeException"; } + @GetMapping("/bizException") + public String bizException() { + throw new BizException(); + } + @GetMapping("/exclude/{id}") public String apiExclude(@PathVariable("id") Long id) { return "Exclude " + id; diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/exception/BizException.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/exception/BizException.java new file mode 100644 index 0000000000..82c0452338 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/exception/BizException.java @@ -0,0 +1,7 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.exception; + +/** + * @author lemonj + */ +public class BizException extends RuntimeException { +} From 0d6aa2badb8813a608e8bb195cefd4553ef6819b Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sat, 31 Aug 2024 10:39:37 +0800 Subject: [PATCH 6/8] remove the clean since cleanUp runs after each test --- .../webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java index beb299c07c..e9030a59ca 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelDefaultBlockExceptionHandlerTest.java @@ -66,8 +66,6 @@ public void testOriginParser() throws Exception { this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string("foo 3")); - - FlowRuleManager.loadRules(null); } @Test @@ -93,8 +91,6 @@ public void testRuntimeException() throws Exception { assertNotNull(cn); assertEquals(repeat, cn.passQps(), 0.01); assertEquals(1, cn.blockRequest(), 1); - - FlowRuleManager.loadRules(null); } From 9bd7945ad6b7e81f2ddc194b37c925a2ec5b1d9a Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sat, 31 Aug 2024 22:56:24 +0800 Subject: [PATCH 7/8] remove the clean since cleanUp runs after each test --- .../spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java | 2 -- .../adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java | 2 -- .../spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java | 1 - 3 files changed, 5 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java index 96ae246548..85972d9d8c 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelDefaultBlockExceptionHandlerTest.java @@ -66,8 +66,6 @@ public void testOriginParser() throws Exception { this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string("foo 3")); - - FlowRuleManager.loadRules(null); } @Test diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java index 5767c7eec0..4579f4ba9b 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java @@ -99,8 +99,6 @@ public void testOriginParser() throws Exception { this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string("foo 3")); - - FlowRuleManager.loadRules(null); } @Test diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java index b500ed4418..28820780ad 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java @@ -174,7 +174,6 @@ private void configureRulesFor(String resource, int count, String limitApp) { if (StringUtil.isNotBlank(limitApp)) { rule.setLimitApp(limitApp); } - FlowRuleManager.loadRules(Collections.singletonList(rule)); } private void configureExceptionRulesFor(String resource, int count, String limitApp) { From 06fa5090bfbb26c95aba85ae7241b3afc7fb396d Mon Sep 17 00:00:00 2001 From: linzyoo <17732834@qq.com> Date: Sat, 31 Aug 2024 23:17:02 +0800 Subject: [PATCH 8/8] remove the clean since cleanUp runs after each test --- .../spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java index 28820780ad..c69a7c5db1 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java @@ -99,8 +99,6 @@ public void testOriginParser() throws Exception { this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string("foo 3")); - - FlowRuleManager.loadRules(null); } @Test @@ -174,6 +172,7 @@ private void configureRulesFor(String resource, int count, String limitApp) { if (StringUtil.isNotBlank(limitApp)) { rule.setLimitApp(limitApp); } + FlowRuleManager.loadRules(Collections.singletonList(rule)); } private void configureExceptionRulesFor(String resource, int count, String limitApp) {