Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -1,3 +1,18 @@ | |||
/* | |||
* Copyright 1999-2018 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.grpc; | |||
import io.grpc.Server; | |||
@@ -6,18 +21,18 @@ import io.grpc.ServerBuilder; | |||
import java.io.IOException; | |||
class GrpcTestServer { | |||
private Server server; | |||
GrpcTestServer() { | |||
} | |||
GrpcTestServer() {} | |||
void start(int port, boolean shouldintercept) throws IOException { | |||
void start(int port, boolean shouldIntercept) throws IOException { | |||
if (server != null) { | |||
throw new IllegalStateException("Server already running!"); | |||
} | |||
ServerBuilder<?> serverBuild = ServerBuilder.forPort(port) | |||
.addService(new FooServiceImpl()); | |||
if (shouldintercept) { | |||
.addService(new FooServiceImpl()); | |||
if (shouldIntercept) { | |||
serverBuild.intercept(new SentinelGrpcServerInterceptor()); | |||
} | |||
server = serverBuild.build(); | |||
@@ -27,6 +27,8 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; | |||
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; | |||
import io.grpc.StatusRuntimeException; | |||
import org.junit.After; | |||
import org.junit.Test; | |||
import static org.junit.Assert.*; | |||
@@ -41,9 +43,9 @@ public class SentinelGrpcClientInterceptorTest { | |||
private final int threshold = 2; | |||
private final GrpcTestServer server = new GrpcTestServer(); | |||
private void configureFlowRule() { | |||
private void configureFlowRule(int count) { | |||
FlowRule rule = new FlowRule() | |||
.setCount(threshold) | |||
.setCount(count) | |||
.setGrade(RuleConstant.FLOW_GRADE_QPS) | |||
.setResource(resourceName) | |||
.setLimitApp("default") | |||
@@ -51,42 +53,44 @@ public class SentinelGrpcClientInterceptorTest { | |||
FlowRuleManager.loadRules(Collections.singletonList(rule)); | |||
} | |||
//@Test | |||
@Test | |||
public void testGrpcClientInterceptor() throws Exception { | |||
final int port = 19328; | |||
configureFlowRule(); | |||
configureFlowRule(threshold); | |||
server.start(port, false); | |||
FooServiceClient client = new FooServiceClient("localhost", port, new SentinelGrpcClientInterceptor()); | |||
final int total = 8; | |||
for (int i = 0; i < total; i++) { | |||
sendRequest(client); | |||
} | |||
assertTrue(sendRequest(client)); | |||
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT); | |||
assertNotNull(clusterNode); | |||
assertEquals(1, clusterNode.passQps()); | |||
assertEquals((total - threshold) / 2, clusterNode.blockRequest()); | |||
assertEquals(total / 2, clusterNode.totalRequest()); | |||
// Not allowed to pass. | |||
configureFlowRule(0); | |||
long totalQps = clusterNode.totalQps(); | |||
long passQps = clusterNode.passQps(); | |||
long blockQps = clusterNode.blockQps(); | |||
assertEquals(total, totalQps); | |||
assertEquals(total - threshold, blockQps); | |||
assertEquals(threshold, passQps); | |||
// The second request will be blocked. | |||
assertFalse(sendRequest(client)); | |||
assertEquals(1, clusterNode.blockQps()); | |||
server.stop(); | |||
} | |||
private void sendRequest(FooServiceClient client) { | |||
private boolean sendRequest(FooServiceClient client) { | |||
try { | |||
FooResponse response = client.sayHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); | |||
System.out.println(ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT).avgRt()); | |||
System.out.println("Response: " + response); | |||
return true; | |||
} catch (StatusRuntimeException ex) { | |||
System.out.println("Blocked, cause: " + ex.getMessage()); | |||
return false; | |||
} | |||
} | |||
@After | |||
public void cleanUp() { | |||
FlowRuleManager.loadRules(null); | |||
ClusterBuilderSlot.getClusterNodeMap().clear(); | |||
} | |||
} |
@@ -27,6 +27,8 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; | |||
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; | |||
import io.grpc.StatusRuntimeException; | |||
import org.junit.After; | |||
import org.junit.Test; | |||
import static org.junit.Assert.*; | |||
@@ -43,9 +45,9 @@ public class SentinelGrpcServerInterceptorTest { | |||
private FooServiceClient client; | |||
private void configureFlowRule() { | |||
private void configureFlowRule(int count) { | |||
FlowRule rule = new FlowRule() | |||
.setCount(threshold) | |||
.setCount(count) | |||
.setGrade(RuleConstant.FLOW_GRADE_QPS) | |||
.setResource(resourceName) | |||
.setLimitApp("default") | |||
@@ -53,41 +55,44 @@ public class SentinelGrpcServerInterceptorTest { | |||
FlowRuleManager.loadRules(Collections.singletonList(rule)); | |||
} | |||
//@Test | |||
@Test | |||
public void testGrpcServerInterceptor() throws Exception { | |||
final int port = 19329; | |||
client = new FooServiceClient("localhost", port); | |||
configureFlowRule(); | |||
configureFlowRule(threshold); | |||
server.start(port, true); | |||
final int total = 8; | |||
for (int i = 0; i < total; i++) { | |||
sendRequest(); | |||
} | |||
assertTrue(sendRequest()); | |||
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.IN); | |||
assertNotNull(clusterNode); | |||
assertEquals(1, clusterNode.passQps()); | |||
assertEquals((total - threshold) / 2, clusterNode.blockRequest()); | |||
assertEquals(total / 2, clusterNode.totalRequest()); | |||
// Not allowed to pass. | |||
configureFlowRule(0); | |||
long totalQps = clusterNode.totalQps(); | |||
long passQps = clusterNode.passQps(); | |||
long blockQps = clusterNode.blockQps(); | |||
assertEquals(total, totalQps); | |||
assertEquals(total - threshold, blockQps); | |||
assertEquals(threshold, passQps); | |||
// The second request will be blocked. | |||
assertFalse(sendRequest()); | |||
assertEquals(1, clusterNode.blockQps()); | |||
server.stop(); | |||
} | |||
private void sendRequest() { | |||
private boolean sendRequest() { | |||
try { | |||
FooResponse response = client.anotherHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); | |||
System.out.println("Response: " + response); | |||
return true; | |||
} catch (StatusRuntimeException ex) { | |||
System.out.println("Blocked, cause: " + ex.getMessage()); | |||
return false; | |||
} | |||
} | |||
@After | |||
public void cleanUp() { | |||
FlowRuleManager.loadRules(null); | |||
ClusterBuilderSlot.getClusterNodeMap().clear(); | |||
} | |||
} |
@@ -28,5 +28,23 @@ | |||
<version>${servlet.api.version}</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-web</artifactId> | |||
<version>1.5.17.RELEASE</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-test</artifactId> | |||
<version>1.5.17.RELEASE</version> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
</project> |
@@ -57,8 +57,7 @@ public class WebCallbackManager { | |||
return requestOriginParser; | |||
} | |||
public static void setRequestOriginParser( | |||
RequestOriginParser requestOriginParser) { | |||
public static void setRequestOriginParser(RequestOriginParser requestOriginParser) { | |||
WebCallbackManager.requestOriginParser = requestOriginParser; | |||
} | |||
} |
@@ -64,7 +64,7 @@ public final class FilterUtil { | |||
url.append("?").append(request.getQueryString()); | |||
} | |||
if (StringUtil.isEmpty(WebServletConfig.getBlockPage())) { | |||
if (StringUtil.isBlank(WebServletConfig.getBlockPage())) { | |||
writeDefaultBlockedPage(response); | |||
} else { | |||
String redirectUrl = WebServletConfig.getBlockPage() + "?http_referer=" + url.toString(); | |||
@@ -75,7 +75,7 @@ public final class FilterUtil { | |||
private static void writeDefaultBlockedPage(HttpServletResponse response) throws IOException { | |||
PrintWriter out = response.getWriter(); | |||
out.println("Blocked by Sentinel (flow limiting)"); | |||
out.print(DEFAULT_BLOCK_MSG); | |||
out.flush(); | |||
out.close(); | |||
} | |||
@@ -183,5 +183,7 @@ public final class FilterUtil { | |||
return i; | |||
} | |||
public static final String DEFAULT_BLOCK_MSG = "Blocked by Sentinel (flow limiting)"; | |||
private FilterUtil() {} | |||
} |
@@ -0,0 +1,176 @@ | |||
/* | |||
* Copyright 1999-2018 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.servlet; | |||
import java.util.Collections; | |||
import javax.servlet.http.HttpServletRequest; | |||
import com.alibaba.csp.sentinel.adapter.servlet.callback.DefaultUrlCleaner; | |||
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; | |||
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; | |||
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; | |||
import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; | |||
import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; | |||
import com.alibaba.csp.sentinel.node.ClusterNode; | |||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | |||
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.AutoConfigureMockMvc; | |||
import org.springframework.boot.test.context.SpringBootTest; | |||
import org.springframework.http.MediaType; | |||
import org.springframework.test.context.junit4.SpringRunner; | |||
import org.springframework.test.web.servlet.MockMvc; | |||
import static org.junit.Assert.*; | |||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; | |||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; | |||
/** | |||
* @author Eric Zhao | |||
*/ | |||
@RunWith(SpringRunner.class) | |||
@SpringBootTest(classes = TestApplication.class) | |||
@AutoConfigureMockMvc | |||
public class CommonFilterTest { | |||
private static final String HELLO_STR = "Hello!"; | |||
@Autowired | |||
private MockMvc mvc; | |||
private void configureRulesFor(String resource, int count) { | |||
configureRulesFor(resource, count, "default"); | |||
} | |||
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)); | |||
} | |||
@Test | |||
public void testCommonFilterMiscellaneous() throws Exception { | |||
String url = "/hello"; | |||
this.mvc.perform(get(url)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string(HELLO_STR)); | |||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); | |||
assertNotNull(cn); | |||
assertEquals(1, cn.passQps()); | |||
testCommonBlockAndRedirectBlockPage(url, cn); | |||
// Test for url cleaner. | |||
testUrlCleaner(); | |||
testCustomOriginParser(); | |||
} | |||
private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cn) throws Exception { | |||
configureRulesFor(url, 0); | |||
// The request will be blocked and response is default block message. | |||
this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); | |||
assertEquals(1, cn.blockQps()); | |||
// Test for redirect. | |||
String redirectUrl = "http://some-location.com"; | |||
WebServletConfig.setBlockPage(redirectUrl); | |||
this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) | |||
.andExpect(status().is3xxRedirection()) | |||
.andExpect(header().string("Location", redirectUrl + "?http_referer=http://localhost/hello")); | |||
FlowRuleManager.loadRules(null); | |||
WebServletConfig.setBlockPage(""); | |||
} | |||
private void testUrlCleaner() throws Exception { | |||
final String fooPrefix = "/foo/"; | |||
String url1 = fooPrefix + 1; | |||
String url2 = fooPrefix + 2; | |||
WebCallbackManager.setUrlCleaner(new UrlCleaner() { | |||
@Override | |||
public String clean(String originUrl) { | |||
if (originUrl.startsWith(fooPrefix)) { | |||
return "/foo/*"; | |||
} | |||
return originUrl; | |||
} | |||
}); | |||
this.mvc.perform(get(url1).accept(MediaType.TEXT_PLAIN)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string("Hello 1")); | |||
this.mvc.perform(get(url2).accept(MediaType.TEXT_PLAIN)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string("Hello 2")); | |||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(fooPrefix + "*"); | |||
assertEquals(2, cn.passQps()); | |||
assertNull(ClusterBuilderSlot.getClusterNode(url1)); | |||
assertNull(ClusterBuilderSlot.getClusterNode(url2)); | |||
WebCallbackManager.setUrlCleaner(new DefaultUrlCleaner()); | |||
} | |||
private void testCustomOriginParser() throws Exception { | |||
String url = "/hello"; | |||
String limitOrigin = "userA"; | |||
final String headerName = "S-User"; | |||
configureRulesFor(url, 0, limitOrigin); | |||
WebCallbackManager.setRequestOriginParser(new RequestOriginParser() { | |||
@Override | |||
public String parseOrigin(HttpServletRequest request) { | |||
String origin = request.getHeader(headerName); | |||
return origin != null ? origin : ""; | |||
} | |||
}); | |||
this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string(HELLO_STR)); | |||
// This will be blocked. | |||
this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN).header(headerName, limitOrigin)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); | |||
this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) | |||
.andExpect(status().isOk()) | |||
.andExpect(content().string(HELLO_STR)); | |||
WebCallbackManager.setRequestOriginParser(null); | |||
FlowRuleManager.loadRules(null); | |||
} | |||
@After | |||
public void cleanUp() { | |||
FlowRuleManager.loadRules(null); | |||
ClusterBuilderSlot.getClusterNodeMap().clear(); | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* Copyright 1999-2018 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.servlet; | |||
import org.springframework.boot.web.servlet.FilterRegistrationBean; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
/** | |||
* @author Eric Zhao | |||
*/ | |||
@Configuration | |||
public class FilterConfig { | |||
@Bean | |||
public FilterRegistrationBean sentinelFilterRegistration() { | |||
FilterRegistrationBean registration = new FilterRegistrationBean(); | |||
registration.setFilter(new CommonFilter()); | |||
registration.addUrlPatterns("/*"); | |||
registration.setName("sentinelFilter"); | |||
registration.setOrder(1); | |||
return registration; | |||
} | |||
} |
@@ -0,0 +1,30 @@ | |||
/* | |||
* Copyright 1999-2018 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.servlet; | |||
import org.springframework.boot.SpringApplication; | |||
import org.springframework.boot.autoconfigure.SpringBootApplication; | |||
/** | |||
* @author Eric Zhao | |||
*/ | |||
@SpringBootApplication | |||
public class TestApplication { | |||
public static void main(String[] args) { | |||
SpringApplication.run(TestApplication.class, args); | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* Copyright 1999-2018 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.servlet; | |||
import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.PathVariable; | |||
import org.springframework.web.bind.annotation.RestController; | |||
/** | |||
* @author Eric Zhao | |||
*/ | |||
@RestController | |||
public class TestController { | |||
@GetMapping("/hello") | |||
public String apiHello() { | |||
return "Hello!"; | |||
} | |||
@GetMapping("/err") | |||
public String apiError() { | |||
return "Oops..."; | |||
} | |||
@GetMapping("/foo/{id}") | |||
public String apiFoo(@PathVariable("id") Long id) { | |||
return "Hello " + id; | |||
} | |||
} |