Skip to content

Commit

Permalink
1. fix the issue#3443 for webmvc-v6x-adapter
Browse files Browse the repository at this point in the history
2. add the Sentinel Exception Aware for webmvc-v6x-adapter.
  • Loading branch information
Linzyoo committed Aug 30, 2024
1 parent c89c6af commit 056a096
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ private void configureExceptionDegradeRulesFor(String resource, double count, St
@After
public void cleanUp() {
FlowRuleManager.loadRules(null);
DegradeRuleManager.loadRules(null);
ClusterBuilderSlot.resetClusterNodes();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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()));

Expand Down Expand Up @@ -209,6 +206,7 @@ private void configureExceptionDegradeRulesFor(String resource, double count, St
@After
public void cleanUp() {
FlowRuleManager.loadRules(null);
DegradeRuleManager.loadRules(null);
ClusterBuilderSlot.resetClusterNodes();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 056a096

Please sign in to comment.