init
This commit is contained in:
6
sentinel/sentinel-extension/README.md
Normal file
6
sentinel/sentinel-extension/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Sentinel Extension
|
||||
|
||||
Sentinel extension modules provide additional extension points
|
||||
and functions.
|
||||
|
||||
|
30
sentinel/sentinel-extension/pom.xml
Normal file
30
sentinel/sentinel-extension/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-parent</artifactId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>sentinel-datasource-extension</module>
|
||||
<module>sentinel-datasource-nacos</module>
|
||||
<module>sentinel-datasource-zookeeper</module>
|
||||
<module>sentinel-datasource-apollo</module>
|
||||
<module>sentinel-datasource-redis</module>
|
||||
<module>sentinel-annotation-aspectj</module>
|
||||
<module>sentinel-parameter-flow-control</module>
|
||||
<module>sentinel-datasource-spring-cloud-config</module>
|
||||
<module>sentinel-datasource-consul</module>
|
||||
<module>sentinel-datasource-etcd</module>
|
||||
<module>sentinel-datasource-eureka</module>
|
||||
<module>sentinel-annotation-cdi-interceptor</module>
|
||||
<module>sentinel-metric-exporter</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
@@ -0,0 +1,69 @@
|
||||
# Sentinel Annotation AspectJ
|
||||
|
||||
This extension is an AOP implementation using AspectJ for Sentinel annotations.
|
||||
Currently only runtime weaving is supported.
|
||||
|
||||
## Annotation
|
||||
|
||||
The `@SentinelResource` annotation indicates a resource definition, including:
|
||||
|
||||
- `value`: Resource name, required (cannot be empty)
|
||||
- `entryType`: Resource entry type (inbound or outbound), `EntryType.OUT` by default
|
||||
- `fallback` (refactored since 1.6.0): Fallback method when exceptions caught (including `BlockException`, but except the exceptions defined in `exceptionsToIgnore`). The fallback method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `fallbackClass` with corresponding `Class` (Note the method in other classes must be *static*). The method signature requirement:
|
||||
- The return type should match the origin method;
|
||||
- The parameter list should match the origin method, and an additional `Throwable` parameter can be provided to get the actual exception.
|
||||
- `defaultFallback` (since 1.6.0): The default fallback method when exceptions caught (including `BlockException`, but except the exceptions defined in `exceptionsToIgnore`). Its intended to be a universal common fallback method. The method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `fallbackClass` with corresponding `Class` (Note the method in other classes must be *static*). The default fallback method signature requirement:
|
||||
- The return type should match the origin method;
|
||||
- parameter list should be empty, and an additional `Throwable` parameter can be provided to get the actual exception.
|
||||
- `blockHandler`: Handler method that handles `BlockException` when blocked. The parameter list of the method should match original method, with the last additional parameter type `BlockException`. The return type should be same as the original method. The `blockHandler` method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `blockHandlerClass` with corresponding `Class` (Note the method in other classes must be *static*).
|
||||
- `exceptionsToIgnore` (since 1.6.0): List of business exception classes that should not be traced and caught in fallback.
|
||||
- `exceptionsToTrace` (since 1.5.1): List of business exception classes to trace and record. In most cases, using `exceptionsToIgnore` is better. If both `exceptionsToTrace` and `exceptionsToIgnore` are present, only `exceptionsToIgnore` will be activated.
|
||||
|
||||
For example:
|
||||
|
||||
```java
|
||||
@SentinelResource(value = "abc", fallback = "doFallback")
|
||||
public String doSomething(long i) {
|
||||
return "Hello " + i;
|
||||
}
|
||||
|
||||
public String doFallback(long i, Throwable t) {
|
||||
// Return fallback value.
|
||||
return "fallback";
|
||||
}
|
||||
|
||||
public String defaultFallback(Throwable t) {
|
||||
return "default_fallback";
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### AspectJ
|
||||
|
||||
If you are using AspectJ directly, you can add the Sentinel annotation aspect to
|
||||
your `aop.xml`:
|
||||
|
||||
```xml
|
||||
<aspects>
|
||||
<aspect name="com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect"/>
|
||||
</aspects>
|
||||
```
|
||||
|
||||
### Spring AOP
|
||||
|
||||
If you are using Spring AOP, you should add a configuration to register the aspect
|
||||
as a Spring bean:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class SentinelAspectConfiguration {
|
||||
|
||||
@Bean
|
||||
public SentinelResourceAspect sentinelResourceAspect() {
|
||||
return new SentinelResourceAspect();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An example for using Sentinel Annotation AspectJ with Spring Boot can be found [here](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-annotation-spring-aop).
|
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-annotation-aspectj</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<aspectj.version>1.9.2</aspectj.version>
|
||||
|
||||
<spring.test.version>5.1.5.RELEASE</spring.test.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
<version>${aspectj.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>${spring.test.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aop</artifactId>
|
||||
<version>${spring.test.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>${spring.test.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.aspectj;
|
||||
|
||||
import com.alibaba.csp.sentinel.Tracer;
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import com.alibaba.csp.sentinel.util.MethodUtil;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Some common functions for Sentinel annotation aspect.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
* @author zhaoyuguang
|
||||
*/
|
||||
public abstract class AbstractSentinelAspectSupport {
|
||||
|
||||
protected void traceException(Throwable ex) {
|
||||
Tracer.trace(ex);
|
||||
}
|
||||
|
||||
protected void traceException(Throwable ex, SentinelResource annotation) {
|
||||
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
|
||||
// The ignore list will be checked first.
|
||||
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
|
||||
return;
|
||||
}
|
||||
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
|
||||
traceException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the exception is in provided list of exception classes.
|
||||
*
|
||||
* @param ex provided throwable
|
||||
* @param exceptions list of exceptions
|
||||
* @return true if it is in the list, otherwise false
|
||||
*/
|
||||
protected boolean exceptionBelongsTo(Throwable ex, Class<? extends Throwable>[] exceptions) {
|
||||
if (exceptions == null) {
|
||||
return false;
|
||||
}
|
||||
for (Class<? extends Throwable> exceptionClass : exceptions) {
|
||||
if (exceptionClass.isAssignableFrom(ex.getClass())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String getResourceName(String resourceName, /*@NonNull*/ Method method) {
|
||||
// If resource name is present in annotation, use this value.
|
||||
if (StringUtil.isNotBlank(resourceName)) {
|
||||
return resourceName;
|
||||
}
|
||||
// Parse name of target method.
|
||||
return MethodUtil.resolveMethodName(method);
|
||||
}
|
||||
|
||||
protected Object handleFallback(ProceedingJoinPoint pjp, SentinelResource annotation, Throwable ex)
|
||||
throws Throwable {
|
||||
return handleFallback(pjp, annotation.fallback(), annotation.defaultFallback(), annotation.fallbackClass(), ex);
|
||||
}
|
||||
|
||||
protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String defaultFallback,
|
||||
Class<?>[] fallbackClass, Throwable ex) throws Throwable {
|
||||
Object[] originArgs = pjp.getArgs();
|
||||
|
||||
// Execute fallback function if configured.
|
||||
Method fallbackMethod = extractFallbackMethod(pjp, fallback, fallbackClass);
|
||||
if (fallbackMethod != null) {
|
||||
// Construct args.
|
||||
int paramCount = fallbackMethod.getParameterTypes().length;
|
||||
Object[] args;
|
||||
if (paramCount == originArgs.length) {
|
||||
args = originArgs;
|
||||
} else {
|
||||
args = Arrays.copyOf(originArgs, originArgs.length + 1);
|
||||
args[args.length - 1] = ex;
|
||||
}
|
||||
|
||||
return invoke(pjp, fallbackMethod, args);
|
||||
}
|
||||
// If fallback is absent, we'll try the defaultFallback if provided.
|
||||
return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
|
||||
}
|
||||
|
||||
protected Object handleDefaultFallback(ProceedingJoinPoint pjp, String defaultFallback,
|
||||
Class<?>[] fallbackClass, Throwable ex) throws Throwable {
|
||||
// Execute the default fallback function if configured.
|
||||
Method fallbackMethod = extractDefaultFallbackMethod(pjp, defaultFallback, fallbackClass);
|
||||
if (fallbackMethod != null) {
|
||||
// Construct args.
|
||||
Object[] args = fallbackMethod.getParameterTypes().length == 0 ? new Object[0] : new Object[] {ex};
|
||||
return invoke(pjp, fallbackMethod, args);
|
||||
}
|
||||
|
||||
// If no any fallback is present, then directly throw the exception.
|
||||
throw ex;
|
||||
}
|
||||
|
||||
protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
|
||||
throws Throwable {
|
||||
|
||||
// Execute block handler if configured.
|
||||
Method blockHandlerMethod = extractBlockHandlerMethod(pjp, annotation.blockHandler(),
|
||||
annotation.blockHandlerClass());
|
||||
if (blockHandlerMethod != null) {
|
||||
Object[] originArgs = pjp.getArgs();
|
||||
// Construct args.
|
||||
Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
|
||||
args[args.length - 1] = ex;
|
||||
return invoke(pjp, blockHandlerMethod, args);
|
||||
}
|
||||
|
||||
// If no block handler is present, then go to fallback.
|
||||
return handleFallback(pjp, annotation, ex);
|
||||
}
|
||||
|
||||
private Object invoke(ProceedingJoinPoint pjp, Method method, Object[] args) throws Throwable {
|
||||
try {
|
||||
if (!method.isAccessible()) {
|
||||
makeAccessible(method);
|
||||
}
|
||||
if (isStatic(method)) {
|
||||
return method.invoke(null, args);
|
||||
}
|
||||
return method.invoke(pjp.getTarget(), args);
|
||||
} catch (InvocationTargetException e) {
|
||||
// throw the actual exception
|
||||
throw e.getTargetException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the given method accessible, explicitly setting it accessible if
|
||||
* necessary. The {@code setAccessible(true)} method is only called
|
||||
* when actually necessary, to avoid unnecessary conflicts with a JVM
|
||||
* SecurityManager (if active).
|
||||
* @param method the method to make accessible
|
||||
* @see java.lang.reflect.Method#setAccessible
|
||||
*/
|
||||
private static void makeAccessible(Method method) {
|
||||
boolean isNotPublic = !Modifier.isPublic(method.getModifiers()) ||
|
||||
!Modifier.isPublic(method.getDeclaringClass().getModifiers());
|
||||
if (isNotPublic && !method.isAccessible()) {
|
||||
method.setAccessible(true);
|
||||
}
|
||||
}
|
||||
|
||||
private Method extractFallbackMethod(ProceedingJoinPoint pjp, String fallbackName, Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(fallbackName)) {
|
||||
return null;
|
||||
}
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz = mustStatic ? locationClass[0] : pjp.getTarget().getClass();
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupFallback(clazz, fallbackName);
|
||||
if (m == null) {
|
||||
// First time, resolve the fallback.
|
||||
Method method = resolveFallbackInternal(pjp, fallbackName, clazz, mustStatic);
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, fallbackName, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method extractDefaultFallbackMethod(ProceedingJoinPoint pjp, String defaultFallback,
|
||||
Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(defaultFallback)) {
|
||||
SentinelResource annotationClass = pjp.getTarget().getClass().getAnnotation(SentinelResource.class);
|
||||
if (annotationClass != null && StringUtil.isNotBlank(annotationClass.defaultFallback())) {
|
||||
defaultFallback = annotationClass.defaultFallback();
|
||||
if (locationClass == null || locationClass.length < 1) {
|
||||
locationClass = annotationClass.fallbackClass();
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz = mustStatic ? locationClass[0] : pjp.getTarget().getClass();
|
||||
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupDefaultFallback(clazz, defaultFallback);
|
||||
if (m == null) {
|
||||
// First time, resolve the default fallback.
|
||||
Class<?> originReturnType = resolveMethod(pjp).getReturnType();
|
||||
// Default fallback allows two kinds of parameter list.
|
||||
// One is empty parameter list.
|
||||
Class<?>[] defaultParamTypes = new Class<?>[0];
|
||||
// The other is a single parameter {@link Throwable} to get relevant exception info.
|
||||
Class<?>[] paramTypeWithException = new Class<?>[] {Throwable.class};
|
||||
// We first find the default fallback with empty parameter list.
|
||||
Method method = findMethod(mustStatic, clazz, defaultFallback, originReturnType, defaultParamTypes);
|
||||
// If default fallback with empty params is absent, we then try to find the other one.
|
||||
if (method == null) {
|
||||
method = findMethod(mustStatic, clazz, defaultFallback, originReturnType, paramTypeWithException);
|
||||
}
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateDefaultFallbackFor(clazz, defaultFallback, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method resolveFallbackInternal(ProceedingJoinPoint pjp, /*@NonNull*/ String name, Class<?> clazz,
|
||||
boolean mustStatic) {
|
||||
Method originMethod = resolveMethod(pjp);
|
||||
// Fallback function allows two kinds of parameter list.
|
||||
Class<?>[] defaultParamTypes = originMethod.getParameterTypes();
|
||||
Class<?>[] paramTypesWithException = Arrays.copyOf(defaultParamTypes, defaultParamTypes.length + 1);
|
||||
paramTypesWithException[paramTypesWithException.length - 1] = Throwable.class;
|
||||
// We first find the fallback matching the signature of origin method.
|
||||
Method method = findMethod(mustStatic, clazz, name, originMethod.getReturnType(), defaultParamTypes);
|
||||
// If fallback matching the origin method is absent, we then try to find the other one.
|
||||
if (method == null) {
|
||||
method = findMethod(mustStatic, clazz, name, originMethod.getReturnType(), paramTypesWithException);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private Method extractBlockHandlerMethod(ProceedingJoinPoint pjp, String name, Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz;
|
||||
if (mustStatic) {
|
||||
clazz = locationClass[0];
|
||||
} else {
|
||||
// By default current class.
|
||||
clazz = pjp.getTarget().getClass();
|
||||
}
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupBlockHandler(clazz, name);
|
||||
if (m == null) {
|
||||
// First time, resolve the block handler.
|
||||
Method method = resolveBlockHandlerInternal(pjp, name, clazz, mustStatic);
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, name, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method resolveBlockHandlerInternal(ProceedingJoinPoint pjp, /*@NonNull*/ String name, Class<?> clazz,
|
||||
boolean mustStatic) {
|
||||
Method originMethod = resolveMethod(pjp);
|
||||
Class<?>[] originList = originMethod.getParameterTypes();
|
||||
Class<?>[] parameterTypes = Arrays.copyOf(originList, originList.length + 1);
|
||||
parameterTypes[parameterTypes.length - 1] = BlockException.class;
|
||||
return findMethod(mustStatic, clazz, name, originMethod.getReturnType(), parameterTypes);
|
||||
}
|
||||
|
||||
private boolean checkStatic(boolean mustStatic, Method method) {
|
||||
return !mustStatic || isStatic(method);
|
||||
}
|
||||
|
||||
private Method findMethod(boolean mustStatic, Class<?> clazz, String name, Class<?> returnType,
|
||||
Class<?>... parameterTypes) {
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
if (name.equals(method.getName()) && checkStatic(mustStatic, method)
|
||||
&& returnType.isAssignableFrom(method.getReturnType())
|
||||
&& Arrays.equals(parameterTypes, method.getParameterTypes())) {
|
||||
|
||||
RecordLog.info("Resolved method [{}] in class [{}]", name, clazz.getCanonicalName());
|
||||
return method;
|
||||
}
|
||||
}
|
||||
// Current class not found, find in the super classes recursively.
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null && !Object.class.equals(superClass)) {
|
||||
return findMethod(mustStatic, superClass, name, returnType, parameterTypes);
|
||||
} else {
|
||||
String methodType = mustStatic ? " static" : "";
|
||||
RecordLog.warn("Cannot find{} method [{}] in class [{}] with parameters {}",
|
||||
methodType, name, clazz.getCanonicalName(), Arrays.toString(parameterTypes));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStatic(Method method) {
|
||||
return Modifier.isStatic(method.getModifiers());
|
||||
}
|
||||
|
||||
protected Method resolveMethod(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
|
||||
Class<?> targetClass = joinPoint.getTarget().getClass();
|
||||
|
||||
Method method = getDeclaredMethodFor(targetClass, signature.getName(),
|
||||
signature.getMethod().getParameterTypes());
|
||||
if (method == null) {
|
||||
throw new IllegalStateException("Cannot resolve target method: " + signature.getMethod().getName());
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get declared method with provided name and parameterTypes in given class and its super classes.
|
||||
* All parameters should be valid.
|
||||
*
|
||||
* @param clazz class where the method is located
|
||||
* @param name method name
|
||||
* @param parameterTypes method parameter type list
|
||||
* @return resolved method, null if not found
|
||||
*/
|
||||
private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
|
||||
try {
|
||||
return clazz.getDeclaredMethod(name, parameterTypes);
|
||||
} catch (NoSuchMethodException e) {
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null) {
|
||||
return getDeclaredMethodFor(superClass, name, parameterTypes);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
class MethodWrapper {
|
||||
|
||||
private final Method method;
|
||||
private final boolean present;
|
||||
|
||||
private MethodWrapper(Method method, boolean present) {
|
||||
this.method = method;
|
||||
this.present = present;
|
||||
}
|
||||
|
||||
static MethodWrapper wrap(Method method) {
|
||||
if (method == null) {
|
||||
return none();
|
||||
}
|
||||
return new MethodWrapper(method, true);
|
||||
}
|
||||
|
||||
static MethodWrapper none() {
|
||||
return new MethodWrapper(null, false);
|
||||
}
|
||||
|
||||
Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
boolean isPresent() {
|
||||
return present;
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Registry for resource configuration metadata (e.g. fallback method)
|
||||
*
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
final class ResourceMetadataRegistry {
|
||||
|
||||
private static final Map<String, MethodWrapper> FALLBACK_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, MethodWrapper> DEFAULT_FALLBACK_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, MethodWrapper> BLOCK_HANDLER_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
static MethodWrapper lookupFallback(Class<?> clazz, String name) {
|
||||
return FALLBACK_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static MethodWrapper lookupDefaultFallback(Class<?> clazz, String name) {
|
||||
return DEFAULT_FALLBACK_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static MethodWrapper lookupBlockHandler(Class<?> clazz, String name) {
|
||||
return BLOCK_HANDLER_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static void updateFallbackFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
FALLBACK_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
static void updateDefaultFallbackFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
DEFAULT_FALLBACK_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
static void updateBlockHandlerFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
BLOCK_HANDLER_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
private static String getKey(Class<?> clazz, String name) {
|
||||
return String.format("%s:%s", clazz.getCanonicalName(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for internal test.
|
||||
*/
|
||||
static void clearFallbackMap() {
|
||||
FALLBACK_MAP.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for internal test.
|
||||
*/
|
||||
static void clearBlockHandlerMap() {
|
||||
BLOCK_HANDLER_MAP.clear();
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import com.alibaba.csp.sentinel.Entry;
|
||||
import com.alibaba.csp.sentinel.EntryType;
|
||||
import com.alibaba.csp.sentinel.SphU;
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Aspect for methods with {@link SentinelResource} annotation.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
@Aspect
|
||||
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
|
||||
|
||||
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
|
||||
public void sentinelResourceAnnotationPointcut() {
|
||||
}
|
||||
|
||||
@Around("sentinelResourceAnnotationPointcut()")
|
||||
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
|
||||
Method originMethod = resolveMethod(pjp);
|
||||
|
||||
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
|
||||
if (annotation == null) {
|
||||
// Should not go through here.
|
||||
throw new IllegalStateException("Wrong state for SentinelResource annotation");
|
||||
}
|
||||
String resourceName = getResourceName(annotation.value(), originMethod);
|
||||
EntryType entryType = annotation.entryType();
|
||||
int resourceType = annotation.resourceType();
|
||||
Entry entry = null;
|
||||
try {
|
||||
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
|
||||
return pjp.proceed();
|
||||
} catch (BlockException ex) {
|
||||
return handleBlockException(pjp, annotation, ex);
|
||||
} catch (Throwable ex) {
|
||||
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
|
||||
// The ignore list will be checked first.
|
||||
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
|
||||
throw ex;
|
||||
}
|
||||
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
|
||||
traceException(ex);
|
||||
return handleFallback(pjp, annotation, ex);
|
||||
}
|
||||
|
||||
// No fallback function can handle the exception, so throw it out.
|
||||
throw ex;
|
||||
} finally {
|
||||
if (entry != null) {
|
||||
entry.exit(1, pjp.getArgs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.service.FooService;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class AbstractSentinelAspectSupportTest extends AbstractSentinelAspectSupport {
|
||||
|
||||
@Test
|
||||
public void testGetResourceName() throws Exception {
|
||||
Method method = FooService.class.getMethod("random");
|
||||
String resourceName = "someRandom";
|
||||
String expectedResolvedName = FooService.class.getName() + ":random()";
|
||||
assertThat(getResourceName(resourceName, method)).isEqualTo(resourceName);
|
||||
assertThat(getResourceName(null, method)).isEqualTo(expectedResolvedName);
|
||||
assertThat(getResourceName("", method)).isEqualTo(expectedResolvedName);
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class MethodWrapperTest {
|
||||
|
||||
@Test
|
||||
public void testWrapMethod() {
|
||||
Method method = String.class.getMethods()[0];
|
||||
MethodWrapper m = MethodWrapper.wrap(method);
|
||||
assertThat(m.isPresent()).isTrue();
|
||||
assertThat(m.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNone() {
|
||||
MethodWrapper none = MethodWrapper.none();
|
||||
assertThat(none.isPresent()).isFalse();
|
||||
assertThat(none.getMethod()).isNull();
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.annotation.aspectj;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.service.FooService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class ResourceMetadataRegistryTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ResourceMetadataRegistry.clearBlockHandlerMap();
|
||||
ResourceMetadataRegistry.clearFallbackMap();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
ResourceMetadataRegistry.clearBlockHandlerMap();
|
||||
ResourceMetadataRegistry.clearFallbackMap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateThenLookupFallback() {
|
||||
Class<?> clazz = FooService.class;
|
||||
String methodName = "someMethodFallback";
|
||||
Method method = clazz.getMethods()[0];
|
||||
assertThat(ResourceMetadataRegistry.lookupFallback(clazz, methodName)).isNull();
|
||||
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, methodName, null);
|
||||
assertThat(ResourceMetadataRegistry.lookupFallback(clazz, methodName).isPresent()).isFalse();
|
||||
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, methodName, method);
|
||||
MethodWrapper wrapper = ResourceMetadataRegistry.lookupFallback(clazz, methodName);
|
||||
assertThat(wrapper.isPresent()).isTrue();
|
||||
assertThat(wrapper.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateThenLookupBlockHandler() {
|
||||
Class<?> clazz = FooService.class;
|
||||
String methodName = "someMethodBlockHand;er";
|
||||
Method method = clazz.getMethods()[1];
|
||||
assertThat(ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName)).isNull();
|
||||
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, methodName, null);
|
||||
assertThat(ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName).isPresent()).isFalse();
|
||||
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, methodName, method);
|
||||
MethodWrapper wrapper = ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName);
|
||||
assertThat(wrapper.isPresent()).isTrue();
|
||||
assertThat(wrapper.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testUpdateBlockHandlerBadArgument() {
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(null, "sxs", String.class.getMethods()[0]);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testUpdateFallbackBadArgument() {
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(String.class, "", String.class.getMethods()[0]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.aspectj.integration;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.config.AopTestConfig;
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.service.BarService;
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.service.FooService;
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.integration.service.FooUtil;
|
||||
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||
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.MethodUtil;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
|
||||
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration test for Sentinel annotation AspectJ extension.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
* @author zhaoyuguang
|
||||
*/
|
||||
@ContextConfiguration(classes = {SentinelAnnotationIntegrationTest.class, AopTestConfig.class})
|
||||
public class SentinelAnnotationIntegrationTest extends AbstractJUnit4SpringContextTests {
|
||||
|
||||
@Autowired
|
||||
private FooService fooService;
|
||||
@Autowired
|
||||
private BarService barService;
|
||||
|
||||
@Test
|
||||
public void testProxySuccessful() {
|
||||
assertThat(AopUtils.isAopProxy(fooService)).isTrue();
|
||||
assertThat(AopUtils.isCglibProxy(fooService)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForeignBlockHandlerClass() throws Exception {
|
||||
assertThat(fooService.random()).isNotEqualTo(FooUtil.BLOCK_FLAG);
|
||||
String resourceName = MethodUtil.resolveMethodName(FooService.class.getDeclaredMethod("random"));
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
assertThat(fooService.random()).isEqualTo(FooUtil.BLOCK_FLAG);
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test(expected = UndeclaredThrowableException.class)
|
||||
public void testBlockHandlerNotFound() {
|
||||
assertThat(fooService.baz("Sentinel")).isEqualTo("cheers, Sentinel");
|
||||
String resourceName = "apiBaz";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
fooService.baz("Sentinel");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotationExceptionsToIgnore() {
|
||||
assertThat(fooService.baz("Sentinel")).isEqualTo("cheers, Sentinel");
|
||||
String resourceName = "apiBaz";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
try {
|
||||
fooService.baz("fail");
|
||||
fail("should not reach here");
|
||||
} catch (IllegalMonitorStateException ex) {
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFallbackWithNoParams() throws Exception {
|
||||
assertThat(fooService.fooWithFallback(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiFooWithFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Fallback should be ignored for this.
|
||||
try {
|
||||
fooService.fooWithFallback(5758);
|
||||
fail("should not reach here");
|
||||
} catch (IllegalAccessException e) {
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
|
||||
// Fallback should take effect.
|
||||
assertThat(fooService.fooWithFallback(5763)).isEqualTo("eee...");
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
assertThat(cn.blockQps()).isZero();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
// Fallback should not take effect for BlockException, as blockHandler is configured.
|
||||
assertThat(fooService.fooWithFallback(2221)).isEqualTo("Oops, 2221");
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFallbackWithSingleParam() {
|
||||
assertThat(fooService.anotherFoo(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiAnotherFooWithDefaultFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Default fallback should take effect.
|
||||
assertThat(fooService.anotherFoo(5758)).isEqualTo(FooUtil.FALLBACK_DEFAULT_RESULT);
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
assertThat(cn.blockQps()).isZero();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
// Default fallback should also take effect for BlockException.
|
||||
assertThat(fooService.anotherFoo(5758)).isEqualTo(FooUtil.FALLBACK_DEFAULT_RESULT);
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalBlockHandlerAndFallback() throws Exception {
|
||||
assertThat(fooService.foo(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiFoo";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Test for biz exception.
|
||||
try {
|
||||
fooService.foo(5758);
|
||||
fail("should not reach here");
|
||||
} catch (Exception ex) {
|
||||
// Should not be traced.
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
|
||||
try {
|
||||
fooService.foo(5763);
|
||||
fail("should not reach here");
|
||||
} catch (Exception ex) {
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
}
|
||||
|
||||
// Test for blockHandler
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
assertThat(fooService.foo(1121)).isEqualTo("Oops, 1121");
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassLevelDefaultFallbackWithSingleParam() {
|
||||
assertThat(barService.anotherBar(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiAnotherBarWithDefaultFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
assertThat(barService.doSomething(1)).isEqualTo("do something");
|
||||
String resourceName1 = "com.alibaba.csp.sentinel.annotation.aspectj.integration.service.BarService:doSomething(int)";
|
||||
ClusterNode cn1 = ClusterBuilderSlot.getClusterNode(resourceName1);
|
||||
assertThat(cn1).isNotNull();
|
||||
assertThat(cn1.passQps()).isPositive();
|
||||
|
||||
assertThat(barService.anotherBar(5758)).isEqualTo("eee...");
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
assertThat(cn.blockQps()).isZero();
|
||||
|
||||
assertThat(barService.doSomething(5758)).isEqualTo("GlobalFallback:doFallback");
|
||||
assertThat(cn1.exceptionQps()).isPositive();
|
||||
assertThat(cn1.blockQps()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFallBackPrivateMethod() throws Exception {
|
||||
String resourceName = "apiFooWithFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
|
||||
try {
|
||||
fooService.fooWithPrivateFallback(5758);
|
||||
fail("should not reach here");
|
||||
} catch (Exception ex) {
|
||||
// Should not be traced.
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
|
||||
assertThat(fooService.fooWithPrivateFallback(5763)).isEqualTo("EEE...");
|
||||
|
||||
// Test for blockHandler
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
assertThat(fooService.fooWithPrivateFallback(2221)).isEqualTo("Oops, 2221");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
FlowRuleManager.loadRules(new ArrayList<FlowRule>());
|
||||
ClusterBuilderSlot.resetClusterNodes();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
FlowRuleManager.loadRules(new ArrayList<FlowRule>());
|
||||
ClusterBuilderSlot.resetClusterNodes();
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.annotation.aspectj.integration.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy(proxyTargetClass = true)
|
||||
@ComponentScan("com.alibaba.csp.sentinel.annotation.aspectj.integration")
|
||||
public class AopTestConfig {
|
||||
|
||||
@Bean
|
||||
public SentinelResourceAspect sentinelResourceAspect() {
|
||||
return new SentinelResourceAspect();
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.aspectj.integration.service;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author zhaoyuguang
|
||||
*/
|
||||
@Service
|
||||
@SentinelResource(defaultFallback = "doFallback", fallbackClass = GlobalFallback.class)
|
||||
public class BarService {
|
||||
|
||||
@SentinelResource(value = "apiAnotherBarWithDefaultFallback", defaultFallback = "fallbackFunc")
|
||||
public String anotherBar(int i) {
|
||||
if (i == 5758) {
|
||||
throw new IllegalArgumentException("oops");
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResource()
|
||||
public String doSomething(int i) {
|
||||
if (i == 5758) {
|
||||
throw new IllegalArgumentException("oops");
|
||||
}
|
||||
return "do something";
|
||||
}
|
||||
|
||||
public String fallbackFunc(Throwable t) {
|
||||
System.out.println(t.getMessage());
|
||||
return "eee...";
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.annotation.aspectj.integration.service;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
@Service
|
||||
public class FooService {
|
||||
|
||||
@SentinelResource(value = "apiFoo", blockHandler = "fooBlockHandler",
|
||||
exceptionsToTrace = {IllegalArgumentException.class})
|
||||
public String foo(int i) throws Exception {
|
||||
if (i == 5758) {
|
||||
throw new IllegalAccessException();
|
||||
}
|
||||
if (i == 5763) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResource(value = "apiFooWithFallback", blockHandler = "fooBlockHandler", fallback = "fooFallbackFunc",
|
||||
exceptionsToTrace = {IllegalArgumentException.class})
|
||||
public String fooWithFallback(int i) throws Exception {
|
||||
if (i == 5758) {
|
||||
throw new IllegalAccessException();
|
||||
}
|
||||
if (i == 5763) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResource(value = "apiAnotherFooWithDefaultFallback", defaultFallback = "globalDefaultFallback",
|
||||
fallbackClass = {FooUtil.class})
|
||||
public String anotherFoo(int i) {
|
||||
if (i == 5758) {
|
||||
throw new IllegalArgumentException("oops");
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResource(blockHandler = "globalBlockHandler", blockHandlerClass = FooUtil.class)
|
||||
public int random() {
|
||||
return ThreadLocalRandom.current().nextInt(0, 30000);
|
||||
}
|
||||
|
||||
@SentinelResource(value = "apiBaz", blockHandler = "bazBlockHandler",
|
||||
exceptionsToIgnore = {IllegalMonitorStateException.class})
|
||||
public String baz(String name) {
|
||||
if (name.equals("fail")) {
|
||||
throw new IllegalMonitorStateException("boom!");
|
||||
}
|
||||
return "cheers, " + name;
|
||||
}
|
||||
|
||||
@SentinelResource(value = "apiFooWithFallback", blockHandler = "fooBlockHandlerPrivate", fallback = "fooFallbackFuncPrivate",
|
||||
exceptionsToTrace = {IllegalArgumentException.class})
|
||||
public String fooWithPrivateFallback(int i) throws Exception {
|
||||
if (i == 5758) {
|
||||
throw new IllegalAccessException();
|
||||
}
|
||||
if (i == 5763) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
public String fooBlockHandler(int i, BlockException ex) {
|
||||
return "Oops, " + i;
|
||||
}
|
||||
|
||||
public String fooFallbackFunc(int i) {
|
||||
return "eee...";
|
||||
}
|
||||
|
||||
private String fooFallbackFuncPrivate(int i) {
|
||||
return "EEE...";
|
||||
}
|
||||
|
||||
private String fooBlockHandlerPrivate(int i, BlockException ex) {
|
||||
return "Oops, " + i;
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.annotation.aspectj.integration.service;
|
||||
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class FooUtil {
|
||||
|
||||
public static final int BLOCK_FLAG = 88888;
|
||||
public static final String FALLBACK_DEFAULT_RESULT = "fallback";
|
||||
|
||||
public static int globalBlockHandler(BlockException ex) {
|
||||
System.out.println("Oops: " + ex.getClass().getSimpleName());
|
||||
return BLOCK_FLAG;
|
||||
}
|
||||
|
||||
public static String globalDefaultFallback(Throwable t) {
|
||||
System.out.println("Fallback caught: " + t.getClass().getSimpleName());
|
||||
return FALLBACK_DEFAULT_RESULT;
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.aspectj.integration.service;
|
||||
|
||||
/**
|
||||
* @author zhaoyuguang
|
||||
*/
|
||||
public class GlobalFallback {
|
||||
|
||||
public static String doFallback(Throwable t) {
|
||||
return "GlobalFallback:doFallback";
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
# Sentinel Annotation CDI extension
|
||||
|
||||
This extension is an implementation using CDI interceptor for Sentinel annotations. [JSR 318: Enterprise JavaBeansTM 3.1/Interceptors 1.2](https://jcp.org/en/jsr/detail?id=318) define the javax interceptor and [CDI](http://www.cdi-spec.org/) related specifications extends the Java Interceptors specification and allows interceptor bindings to be applied to CDI stereotypes.
|
||||
|
||||
[CDI](http://www.cdi-spec.org/) is an abbreviation for Contexts and Dependency Injection, the related JSRs are : [JSR 365: Contexts and Dependency Injection for JavaTM 2.0](https://jcp.org/en/jsr/detail?id=365), [JSR 346: Contexts and Dependency Injection for JavaTM EE 1.1](https://jcp.org/en/jsr/detail?id=346), [JSR 299: Contexts and Dependency Injection for the JavaTM EE platform](https://jcp.org/en/jsr/detail?id=299)
|
||||
|
||||
## Annotation
|
||||
|
||||
The `@SentinelResourceBinding` is modified from `@SentinelResource` by adding `@InterceptorBinding` for CDI specification,
|
||||
and in order to intercept all kinds of `@SentinelResourceBinding` with different attributes in one interceptor,
|
||||
all attributes of `@SentinelResourceBinding` are annotated with `@Nonbinding`.
|
||||
|
||||
The `@SentinelResourceBinding` annotation indicates a resource definition, including:
|
||||
|
||||
- `value`: Resource name, required (cannot be empty)
|
||||
- `entryType`: Traffic type (inbound or outbound), `EntryType.OUT` by default
|
||||
- `fallback`: Fallback method when exceptions caught (including `BlockException`, but except the exceptions defined in `exceptionsToIgnore`). The fallback method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `fallbackClass` with corresponding `Class` (Note the method in other classes must be *static*). The method signature requirement:
|
||||
- The return type should match the origin method;
|
||||
- The parameter list should match the origin method, and an additional `Throwable` parameter can be provided to get the actual exception.
|
||||
- `defaultFallback`: The default fallback method when exceptions caught (including `BlockException`, but except the exceptions defined in `exceptionsToIgnore`). Its intended to be a universal common fallback method. The method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `fallbackClass` with corresponding `Class` (Note the method in other classes must be *static*). The default fallback method signature requirement:
|
||||
- The return type should match the origin method;
|
||||
- parameter list should be empty, and an additional `Throwable` parameter can be provided to get the actual exception.
|
||||
- `blockHandler`: Handler method that handles `BlockException` when blocked. The parameter list of the method should match original method, with the last additional parameter type `BlockException`. The return type should be same as the original method. The `blockHandler` method should be located in the same class with original method by default. If you want to use method in other classes, you can set the `blockHandlerClass` with corresponding `Class` (Note the method in other classes must be *static*).
|
||||
- `exceptionsToIgnore`: List of business exception classes that should not be traced and caught in fallback.
|
||||
- `exceptionsToTrace`: List of business exception classes to trace and record. In most cases, using `exceptionsToIgnore` is better. If both `exceptionsToTrace` and `exceptionsToIgnore` are present, only `exceptionsToIgnore` will be activated.
|
||||
|
||||
For example:
|
||||
|
||||
```java
|
||||
@SentinelResourceBinding(value = "abc", fallback = "doFallback")
|
||||
public String doSomething(long i) {
|
||||
return "Hello " + i;
|
||||
}
|
||||
|
||||
public String doFallback(long i, Throwable t) {
|
||||
// Return fallback value.
|
||||
return "fallback";
|
||||
}
|
||||
|
||||
public String defaultFallback(Throwable t) {
|
||||
return "default_fallback";
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
According to [9.4. Interceptor enablement and ordering](https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#enabled_interceptors), to enable the interceptor,
|
||||
we may add configuration in `resources/META-INF/beans.xml` like this:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" bean-discovery-mode="all" version="2.0">
|
||||
<interceptors>
|
||||
<class>com.alibaba.csp.sentinel.annotation.cdi.interceptor.SentinelResourceInterceptor</class>
|
||||
</interceptors>
|
||||
</beans>
|
||||
```
|
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-annotation-cdi-interceptor</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<jakarta.interceptor-api.version>1.2.5</jakarta.interceptor-api.version>
|
||||
<jakarta.enterprise.cdi-api.version>2.0.2</jakarta.enterprise.cdi-api.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.interceptor</groupId>
|
||||
<artifactId>jakarta.interceptor-api</artifactId>
|
||||
<version>${jakarta.interceptor-api.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.enterprise</groupId>
|
||||
<artifactId>jakarta.enterprise.cdi-api</artifactId>
|
||||
<version>${jakarta.enterprise.cdi-api.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.weld.se</groupId>
|
||||
<artifactId>weld-se-shaded</artifactId>
|
||||
<version>3.1.4.Final</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.Tracer;
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import com.alibaba.csp.sentinel.util.MethodUtil;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
import javax.interceptor.InvocationContext;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Some common functions for Sentinel annotation CDI extension.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
* @author seasidesky
|
||||
*/
|
||||
public abstract class AbstractSentinelInterceptorSupport {
|
||||
|
||||
protected void traceException(Throwable ex) {
|
||||
Tracer.trace(ex);
|
||||
}
|
||||
|
||||
protected void traceException(Throwable ex, SentinelResourceBinding annotation) {
|
||||
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
|
||||
// The ignore list will be checked first.
|
||||
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
|
||||
return;
|
||||
}
|
||||
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
|
||||
traceException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the exception is in provided list of exception classes.
|
||||
*
|
||||
* @param ex provided throwable
|
||||
* @param exceptions list of exceptions
|
||||
* @return true if it is in the list, otherwise false
|
||||
*/
|
||||
protected boolean exceptionBelongsTo(Throwable ex, Class<? extends Throwable>[] exceptions) {
|
||||
if (exceptions == null) {
|
||||
return false;
|
||||
}
|
||||
for (Class<? extends Throwable> exceptionClass : exceptions) {
|
||||
if (exceptionClass.isAssignableFrom(ex.getClass())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String getResourceName(String resourceName, /*@NonNull*/ Method method) {
|
||||
// If resource name is present in annotation, use this value.
|
||||
if (StringUtil.isNotBlank(resourceName)) {
|
||||
return resourceName;
|
||||
}
|
||||
// Parse name of target method.
|
||||
return MethodUtil.resolveMethodName(method);
|
||||
}
|
||||
|
||||
protected Object handleFallback(InvocationContext ctx, SentinelResourceBinding annotation, Throwable ex)
|
||||
throws Throwable {
|
||||
return handleFallback(ctx, annotation.fallback(), annotation.defaultFallback(), annotation.fallbackClass(), ex);
|
||||
}
|
||||
|
||||
protected Object handleFallback(InvocationContext ctx, String fallback, String defaultFallback,
|
||||
Class<?>[] fallbackClass, Throwable ex) throws Throwable {
|
||||
Object[] originArgs = ctx.getParameters();
|
||||
|
||||
// Execute fallback function if configured.
|
||||
Method fallbackMethod = extractFallbackMethod(ctx, fallback, fallbackClass);
|
||||
if (fallbackMethod != null) {
|
||||
// Construct args.
|
||||
int paramCount = fallbackMethod.getParameterTypes().length;
|
||||
Object[] args;
|
||||
if (paramCount == originArgs.length) {
|
||||
args = originArgs;
|
||||
} else {
|
||||
args = Arrays.copyOf(originArgs, originArgs.length + 1);
|
||||
args[args.length - 1] = ex;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isStatic(fallbackMethod)) {
|
||||
return fallbackMethod.invoke(null, args);
|
||||
}
|
||||
return fallbackMethod.invoke(ctx.getTarget(), args);
|
||||
} catch (InvocationTargetException e) {
|
||||
// throw the actual exception
|
||||
throw e.getTargetException();
|
||||
}
|
||||
}
|
||||
// If fallback is absent, we'll try the defaultFallback if provided.
|
||||
return handleDefaultFallback(ctx, defaultFallback, fallbackClass, ex);
|
||||
}
|
||||
|
||||
protected Object handleDefaultFallback(InvocationContext ctx, String defaultFallback,
|
||||
Class<?>[] fallbackClass, Throwable ex) throws Throwable {
|
||||
// Execute the default fallback function if configured.
|
||||
Method fallbackMethod = extractDefaultFallbackMethod(ctx, defaultFallback, fallbackClass);
|
||||
if (fallbackMethod != null) {
|
||||
// Construct args.
|
||||
Object[] args = fallbackMethod.getParameterTypes().length == 0 ? new Object[0] : new Object[] {ex};
|
||||
try {
|
||||
if (isStatic(fallbackMethod)) {
|
||||
return fallbackMethod.invoke(null, args);
|
||||
}
|
||||
return fallbackMethod.invoke(ctx.getTarget(), args);
|
||||
} catch (InvocationTargetException e) {
|
||||
// throw the actual exception
|
||||
throw e.getTargetException();
|
||||
}
|
||||
}
|
||||
|
||||
// If no any fallback is present, then directly throw the exception.
|
||||
throw ex;
|
||||
}
|
||||
|
||||
protected Object handleBlockException(InvocationContext ctx, SentinelResourceBinding annotation, BlockException ex)
|
||||
throws Throwable {
|
||||
|
||||
// Execute block handler if configured.
|
||||
Method blockHandlerMethod = extractBlockHandlerMethod(ctx, annotation.blockHandler(),
|
||||
annotation.blockHandlerClass());
|
||||
if (blockHandlerMethod != null) {
|
||||
Object[] originArgs = ctx.getParameters();
|
||||
// Construct args.
|
||||
Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
|
||||
args[args.length - 1] = ex;
|
||||
try {
|
||||
if (isStatic(blockHandlerMethod)) {
|
||||
return blockHandlerMethod.invoke(null, args);
|
||||
}
|
||||
return blockHandlerMethod.invoke(ctx.getTarget(), args);
|
||||
} catch (InvocationTargetException e) {
|
||||
// throw the actual exception
|
||||
throw e.getTargetException();
|
||||
}
|
||||
}
|
||||
|
||||
// If no block handler is present, then go to fallback.
|
||||
return handleFallback(ctx, annotation, ex);
|
||||
}
|
||||
|
||||
private Method extractFallbackMethod(InvocationContext ctx, String fallbackName, Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(fallbackName)) {
|
||||
return null;
|
||||
}
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz = mustStatic ? locationClass[0] : ctx.getTarget().getClass();
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupFallback(clazz, fallbackName);
|
||||
if (m == null) {
|
||||
// First time, resolve the fallback.
|
||||
Method method = resolveFallbackInternal(ctx, fallbackName, clazz, mustStatic);
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, fallbackName, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method extractDefaultFallbackMethod(InvocationContext ctx, String defaultFallback,
|
||||
Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(defaultFallback)) {
|
||||
SentinelResource annotationClass = ctx.getTarget().getClass().getAnnotation(SentinelResource.class);
|
||||
if (annotationClass != null && StringUtil.isNotBlank(annotationClass.defaultFallback())) {
|
||||
defaultFallback = annotationClass.defaultFallback();
|
||||
if (locationClass == null || locationClass.length < 1) {
|
||||
locationClass = annotationClass.fallbackClass();
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz = mustStatic ? locationClass[0] : ctx.getTarget().getClass();
|
||||
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupDefaultFallback(clazz, defaultFallback);
|
||||
if (m == null) {
|
||||
// First time, resolve the default fallback.
|
||||
Class<?> originReturnType = resolveMethod(ctx).getReturnType();
|
||||
// Default fallback allows two kinds of parameter list.
|
||||
// One is empty parameter list.
|
||||
Class<?>[] defaultParamTypes = new Class<?>[0];
|
||||
// The other is a single parameter {@link Throwable} to get relevant exception info.
|
||||
Class<?>[] paramTypeWithException = new Class<?>[] {Throwable.class};
|
||||
// We first find the default fallback with empty parameter list.
|
||||
Method method = findMethod(mustStatic, clazz, defaultFallback, originReturnType, defaultParamTypes);
|
||||
// If default fallback with empty params is absent, we then try to find the other one.
|
||||
if (method == null) {
|
||||
method = findMethod(mustStatic, clazz, defaultFallback, originReturnType, paramTypeWithException);
|
||||
}
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateDefaultFallbackFor(clazz, defaultFallback, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method resolveFallbackInternal(InvocationContext ctx, /*@NonNull*/ String name, Class<?> clazz,
|
||||
boolean mustStatic) {
|
||||
Method originMethod = resolveMethod(ctx);
|
||||
// Fallback function allows two kinds of parameter list.
|
||||
Class<?>[] defaultParamTypes = originMethod.getParameterTypes();
|
||||
Class<?>[] paramTypesWithException = Arrays.copyOf(defaultParamTypes, defaultParamTypes.length + 1);
|
||||
paramTypesWithException[paramTypesWithException.length - 1] = Throwable.class;
|
||||
// We first find the fallback matching the signature of origin method.
|
||||
Method method = findMethod(mustStatic, clazz, name, originMethod.getReturnType(), defaultParamTypes);
|
||||
// If fallback matching the origin method is absent, we then try to find the other one.
|
||||
if (method == null) {
|
||||
method = findMethod(mustStatic, clazz, name, originMethod.getReturnType(), paramTypesWithException);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private Method extractBlockHandlerMethod(InvocationContext ctx, String name, Class<?>[] locationClass) {
|
||||
if (StringUtil.isBlank(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean mustStatic = locationClass != null && locationClass.length >= 1;
|
||||
Class<?> clazz;
|
||||
if (mustStatic) {
|
||||
clazz = locationClass[0];
|
||||
} else {
|
||||
// By default current class.
|
||||
clazz = ctx.getTarget().getClass();
|
||||
}
|
||||
MethodWrapper m = ResourceMetadataRegistry.lookupBlockHandler(clazz, name);
|
||||
if (m == null) {
|
||||
// First time, resolve the block handler.
|
||||
Method method = resolveBlockHandlerInternal(ctx, name, clazz, mustStatic);
|
||||
// Cache the method instance.
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, name, method);
|
||||
return method;
|
||||
}
|
||||
if (!m.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
return m.getMethod();
|
||||
}
|
||||
|
||||
private Method resolveBlockHandlerInternal(InvocationContext ctx, /*@NonNull*/ String name, Class<?> clazz,
|
||||
boolean mustStatic) {
|
||||
Method originMethod = resolveMethod(ctx);
|
||||
Class<?>[] originList = originMethod.getParameterTypes();
|
||||
Class<?>[] parameterTypes = Arrays.copyOf(originList, originList.length + 1);
|
||||
parameterTypes[parameterTypes.length - 1] = BlockException.class;
|
||||
return findMethod(mustStatic, clazz, name, originMethod.getReturnType(), parameterTypes);
|
||||
}
|
||||
|
||||
private boolean checkStatic(boolean mustStatic, Method method) {
|
||||
return !mustStatic || isStatic(method);
|
||||
}
|
||||
|
||||
private Method findMethod(boolean mustStatic, Class<?> clazz, String name, Class<?> returnType,
|
||||
Class<?>... parameterTypes) {
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
if (name.equals(method.getName()) && checkStatic(mustStatic, method)
|
||||
&& returnType.isAssignableFrom(method.getReturnType())
|
||||
&& Arrays.equals(parameterTypes, method.getParameterTypes())) {
|
||||
|
||||
RecordLog.info("Resolved method [{}] in class [{}]", name, clazz.getCanonicalName());
|
||||
return method;
|
||||
}
|
||||
}
|
||||
// Current class not found, find in the super classes recursively.
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null && !Object.class.equals(superClass)) {
|
||||
return findMethod(mustStatic, superClass, name, returnType, parameterTypes);
|
||||
} else {
|
||||
String methodType = mustStatic ? " static" : "";
|
||||
RecordLog.warn("Cannot find{} method [{}] in class [{}] with parameters {}",
|
||||
methodType, name, clazz.getCanonicalName(), Arrays.toString(parameterTypes));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStatic(Method method) {
|
||||
return Modifier.isStatic(method.getModifiers());
|
||||
}
|
||||
|
||||
protected Method resolveMethod(InvocationContext ctx) {
|
||||
Class<?> targetClass = ctx.getTarget().getClass();
|
||||
|
||||
Method method = getDeclaredMethodFor(targetClass, ctx.getMethod().getName(),
|
||||
ctx.getMethod().getParameterTypes());
|
||||
if (method == null) {
|
||||
throw new IllegalStateException("Cannot resolve target method: " + ctx.getMethod().getName());
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get declared method with provided name and parameterTypes in given class and its super classes.
|
||||
* All parameters should be valid.
|
||||
*
|
||||
* @param clazz class where the method is located
|
||||
* @param name method name
|
||||
* @param parameterTypes method parameter type list
|
||||
* @return resolved method, null if not found
|
||||
*/
|
||||
private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
|
||||
try {
|
||||
return clazz.getDeclaredMethod(name, parameterTypes);
|
||||
} catch (NoSuchMethodException e) {
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null) {
|
||||
return getDeclaredMethodFor(superClass, name, parameterTypes);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
class MethodWrapper {
|
||||
|
||||
private final Method method;
|
||||
private final boolean present;
|
||||
|
||||
private MethodWrapper(Method method, boolean present) {
|
||||
this.method = method;
|
||||
this.present = present;
|
||||
}
|
||||
|
||||
static MethodWrapper wrap(Method method) {
|
||||
if (method == null) {
|
||||
return none();
|
||||
}
|
||||
return new MethodWrapper(method, true);
|
||||
}
|
||||
|
||||
static MethodWrapper none() {
|
||||
return new MethodWrapper(null, false);
|
||||
}
|
||||
|
||||
Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
boolean isPresent() {
|
||||
return present;
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Registry for resource configuration metadata (e.g. fallback method)
|
||||
*
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
final class ResourceMetadataRegistry {
|
||||
|
||||
private static final Map<String, MethodWrapper> FALLBACK_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, MethodWrapper> DEFAULT_FALLBACK_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, MethodWrapper> BLOCK_HANDLER_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
static MethodWrapper lookupFallback(Class<?> clazz, String name) {
|
||||
return FALLBACK_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static MethodWrapper lookupDefaultFallback(Class<?> clazz, String name) {
|
||||
return DEFAULT_FALLBACK_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static MethodWrapper lookupBlockHandler(Class<?> clazz, String name) {
|
||||
return BLOCK_HANDLER_MAP.get(getKey(clazz, name));
|
||||
}
|
||||
|
||||
static void updateFallbackFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
FALLBACK_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
static void updateDefaultFallbackFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
DEFAULT_FALLBACK_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
static void updateBlockHandlerFor(Class<?> clazz, String name, Method method) {
|
||||
if (clazz == null || StringUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Bad argument");
|
||||
}
|
||||
BLOCK_HANDLER_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method));
|
||||
}
|
||||
|
||||
private static String getKey(Class<?> clazz, String name) {
|
||||
return String.format("%s:%s", clazz.getCanonicalName(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for internal test.
|
||||
*/
|
||||
static void clearFallbackMap() {
|
||||
FALLBACK_MAP.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for internal test.
|
||||
*/
|
||||
static void clearBlockHandlerMap() {
|
||||
BLOCK_HANDLER_MAP.clear();
|
||||
}
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.EntryType;
|
||||
|
||||
import javax.enterprise.util.Nonbinding;
|
||||
import javax.interceptor.InterceptorBinding;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* The annotation indicates a definition of Sentinel resource.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
* @author seasidesky
|
||||
* @since 1.8.0
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface SentinelResourceBinding {
|
||||
|
||||
/**
|
||||
* @return name of the Sentinel resource
|
||||
*/
|
||||
@Nonbinding
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* @return the entry type (inbound or outbound), outbound by default
|
||||
*/
|
||||
@Nonbinding
|
||||
EntryType entryType() default EntryType.OUT;
|
||||
|
||||
/**
|
||||
* @return the classification (type) of the resource
|
||||
*/
|
||||
@Nonbinding
|
||||
int resourceType() default 0;
|
||||
|
||||
/**
|
||||
* @return name of the block exception function, empty by default
|
||||
*/
|
||||
@Nonbinding
|
||||
String blockHandler() default "";
|
||||
|
||||
/**
|
||||
* The {@code blockHandler} is located in the same class with the original method by default.
|
||||
* However, if some methods share the same signature and intend to set the same block handler,
|
||||
* then users can set the class where the block handler exists. Note that the block handler method
|
||||
* must be static.
|
||||
*
|
||||
* @return the class where the block handler exists, should not provide more than one classes
|
||||
*/
|
||||
@Nonbinding
|
||||
Class<?>[] blockHandlerClass() default {};
|
||||
|
||||
/**
|
||||
* @return name of the fallback function, empty by default
|
||||
*/
|
||||
@Nonbinding
|
||||
String fallback() default "";
|
||||
|
||||
/**
|
||||
* The {@code defaultFallback} is used as the default universal fallback method.
|
||||
* It should not accept any parameters, and the return type should be compatible
|
||||
* with the original method.
|
||||
*
|
||||
* @return name of the default fallback method, empty by default
|
||||
*/
|
||||
@Nonbinding
|
||||
String defaultFallback() default "";
|
||||
|
||||
/**
|
||||
* The {@code fallback} is located in the same class with the original method by default.
|
||||
* However, if some methods share the same signature and intend to set the same fallback,
|
||||
* then users can set the class where the fallback function exists. Note that the shared fallback method
|
||||
* must be static.
|
||||
*
|
||||
* @return the class where the fallback method is located (only single class)
|
||||
*/
|
||||
@Nonbinding
|
||||
Class<?>[] fallbackClass() default {};
|
||||
|
||||
/**
|
||||
* @return the list of exception classes to trace, {@link Throwable} by default
|
||||
*/
|
||||
@Nonbinding
|
||||
Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
|
||||
|
||||
/**
|
||||
* Indicates the exceptions to be ignored. Note that {@code exceptionsToTrace} should
|
||||
* not appear with {@code exceptionsToIgnore} at the same time, or {@code exceptionsToIgnore}
|
||||
* will be of higher precedence.
|
||||
*
|
||||
* @return the list of exception classes to ignore, empty by default
|
||||
*/
|
||||
@Nonbinding
|
||||
Class<? extends Throwable>[] exceptionsToIgnore() default {};
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.Entry;
|
||||
import com.alibaba.csp.sentinel.EntryType;
|
||||
import com.alibaba.csp.sentinel.SphU;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
|
||||
import javax.annotation.Priority;
|
||||
import javax.interceptor.AroundInvoke;
|
||||
import javax.interceptor.Interceptor;
|
||||
import javax.interceptor.InvocationContext;
|
||||
|
||||
/**
|
||||
* @author sea
|
||||
* @since 1.8.0
|
||||
*/
|
||||
@Interceptor
|
||||
@SentinelResourceBinding
|
||||
@Priority(0)
|
||||
public class SentinelResourceInterceptor extends AbstractSentinelInterceptorSupport {
|
||||
|
||||
@AroundInvoke
|
||||
Object aroundInvoke(InvocationContext ctx) throws Throwable {
|
||||
SentinelResourceBinding annotation = ctx.getMethod().getAnnotation(SentinelResourceBinding.class);
|
||||
if (annotation == null) {
|
||||
// Should not go through here.
|
||||
throw new IllegalStateException("Wrong state for SentinelResource annotation");
|
||||
}
|
||||
|
||||
String resourceName = getResourceName(annotation.value(), ctx.getMethod());
|
||||
EntryType entryType = annotation.entryType();
|
||||
int resourceType = annotation.resourceType();
|
||||
Entry entry = null;
|
||||
try {
|
||||
entry = SphU.entry(resourceName, resourceType, entryType, ctx.getParameters());
|
||||
Object result = ctx.proceed();
|
||||
return result;
|
||||
} catch (BlockException ex) {
|
||||
return handleBlockException(ctx, annotation, ex);
|
||||
} catch (Throwable ex) {
|
||||
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
|
||||
// The ignore list will be checked first.
|
||||
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
|
||||
throw ex;
|
||||
}
|
||||
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
|
||||
traceException(ex);
|
||||
return handleFallback(ctx, annotation, ex);
|
||||
}
|
||||
|
||||
// No fallback function can handle the exception, so throw it out.
|
||||
throw ex;
|
||||
} finally {
|
||||
if (entry != null) {
|
||||
entry.exit(1, ctx.getParameters());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" bean-discovery-mode="all" version="2.0">
|
||||
</beans>
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.cdi.interceptor.integration.service.FooService;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author sea
|
||||
*/
|
||||
public class AbstractSentinelInterceptorSupportTest extends AbstractSentinelInterceptorSupport {
|
||||
|
||||
@Test
|
||||
public void testGetResourceName() throws Exception {
|
||||
Method method = FooService.class.getMethod("random");
|
||||
String resourceName = "someRandom";
|
||||
String expectedResolvedName = FooService.class.getName() + ":random()";
|
||||
assertThat(getResourceName(resourceName, method)).isEqualTo(resourceName);
|
||||
assertThat(getResourceName(null, method)).isEqualTo(expectedResolvedName);
|
||||
assertThat(getResourceName("", method)).isEqualTo(expectedResolvedName);
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class MethodWrapperTest {
|
||||
|
||||
@Test
|
||||
public void testWrapMethod() {
|
||||
Method method = String.class.getMethods()[0];
|
||||
MethodWrapper m = MethodWrapper.wrap(method);
|
||||
assertThat(m.isPresent()).isTrue();
|
||||
assertThat(m.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNone() {
|
||||
MethodWrapper none = MethodWrapper.none();
|
||||
assertThat(none.isPresent()).isFalse();
|
||||
assertThat(none.getMethod()).isNull();
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.cdi.interceptor.integration.service.FooService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class ResourceMetadataRegistryTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ResourceMetadataRegistry.clearBlockHandlerMap();
|
||||
ResourceMetadataRegistry.clearFallbackMap();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
ResourceMetadataRegistry.clearBlockHandlerMap();
|
||||
ResourceMetadataRegistry.clearFallbackMap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateThenLookupFallback() {
|
||||
Class<?> clazz = FooService.class;
|
||||
String methodName = "someMethodFallback";
|
||||
Method method = clazz.getMethods()[0];
|
||||
assertThat(ResourceMetadataRegistry.lookupFallback(clazz, methodName)).isNull();
|
||||
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, methodName, null);
|
||||
assertThat(ResourceMetadataRegistry.lookupFallback(clazz, methodName).isPresent()).isFalse();
|
||||
|
||||
ResourceMetadataRegistry.updateFallbackFor(clazz, methodName, method);
|
||||
MethodWrapper wrapper = ResourceMetadataRegistry.lookupFallback(clazz, methodName);
|
||||
assertThat(wrapper.isPresent()).isTrue();
|
||||
assertThat(wrapper.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateThenLookupBlockHandler() {
|
||||
Class<?> clazz = FooService.class;
|
||||
String methodName = "someMethodBlockHand;er";
|
||||
Method method = clazz.getMethods()[1];
|
||||
assertThat(ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName)).isNull();
|
||||
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, methodName, null);
|
||||
assertThat(ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName).isPresent()).isFalse();
|
||||
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(clazz, methodName, method);
|
||||
MethodWrapper wrapper = ResourceMetadataRegistry.lookupBlockHandler(clazz, methodName);
|
||||
assertThat(wrapper.isPresent()).isTrue();
|
||||
assertThat(wrapper.getMethod()).isSameAs(method);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testUpdateBlockHandlerBadArgument() {
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(null, "sxs", String.class.getMethods()[0]);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testUpdateFallbackBadArgument() {
|
||||
ResourceMetadataRegistry.updateBlockHandlerFor(String.class, "", String.class.getMethods()[0]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor.integration;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.cdi.interceptor.integration.service.FooService;
|
||||
import com.alibaba.csp.sentinel.annotation.cdi.interceptor.integration.service.FooUtil;
|
||||
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||
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.MethodUtil;
|
||||
import org.junit.*;
|
||||
|
||||
import javax.enterprise.inject.se.SeContainer;
|
||||
import javax.enterprise.inject.se.SeContainerInitializer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author sea
|
||||
*/
|
||||
public class SentinelAnnotationInterceptorIntegrationTest {
|
||||
|
||||
static SeContainer container;
|
||||
|
||||
FooService fooService;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
SeContainerInitializer containerInit = SeContainerInitializer.newInstance();
|
||||
container = containerInit.initialize();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void shutdown() {
|
||||
container.close();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
FlowRuleManager.loadRules(new ArrayList<FlowRule>());
|
||||
ClusterBuilderSlot.resetClusterNodes();
|
||||
fooService = container.select(FooService.class).get();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
FlowRuleManager.loadRules(new ArrayList<FlowRule>());
|
||||
ClusterBuilderSlot.resetClusterNodes();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForeignBlockHandlerClass() throws Exception {
|
||||
assertThat(fooService.random()).isNotEqualTo(FooUtil.BLOCK_FLAG);
|
||||
String resourceName = MethodUtil.resolveMethodName(FooService.class.getDeclaredMethod("random"));
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
assertThat(fooService.random()).isEqualTo(FooUtil.BLOCK_FLAG);
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test(expected = FlowException.class)
|
||||
public void testBlockHandlerNotFound() {
|
||||
assertThat(fooService.baz("Sentinel")).isEqualTo("cheers, Sentinel");
|
||||
String resourceName = "apiBaz";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
fooService.baz("Sentinel");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotationExceptionsToIgnore() {
|
||||
assertThat(fooService.baz("Sentinel")).isEqualTo("cheers, Sentinel");
|
||||
String resourceName = "apiBaz";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
try {
|
||||
fooService.baz("fail");
|
||||
fail("should not reach here");
|
||||
} catch (IllegalMonitorStateException ex) {
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFallbackWithNoParams() throws Exception {
|
||||
assertThat(fooService.fooWithFallback(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiFooWithFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Fallback should be ignored for this.
|
||||
try {
|
||||
fooService.fooWithFallback(5758);
|
||||
fail("should not reach here");
|
||||
} catch (IllegalAccessException e) {
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
|
||||
// Fallback should take effect.
|
||||
assertThat(fooService.fooWithFallback(5763)).isEqualTo("eee...");
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
assertThat(cn.blockQps()).isZero();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
// Fallback should not take effect for BlockException, as blockHandler is configured.
|
||||
assertThat(fooService.fooWithFallback(2221)).isEqualTo("Oops, 2221");
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFallbackWithSingleParam() {
|
||||
assertThat(fooService.anotherFoo(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiAnotherFooWithDefaultFallback";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Default fallback should take effect.
|
||||
assertThat(fooService.anotherFoo(5758)).isEqualTo(FooUtil.FALLBACK_DEFAULT_RESULT);
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
assertThat(cn.blockQps()).isZero();
|
||||
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
// Default fallback should also take effect for BlockException.
|
||||
assertThat(fooService.anotherFoo(5758)).isEqualTo(FooUtil.FALLBACK_DEFAULT_RESULT);
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalBlockHandlerAndFallback() throws Exception {
|
||||
assertThat(fooService.foo(1)).isEqualTo("Hello for 1");
|
||||
String resourceName = "apiFoo";
|
||||
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||
assertThat(cn).isNotNull();
|
||||
assertThat(cn.passQps()).isPositive();
|
||||
|
||||
// Test for biz exception.
|
||||
try {
|
||||
fooService.foo(5758);
|
||||
fail("should not reach here");
|
||||
} catch (Exception ex) {
|
||||
// Should not be traced.
|
||||
assertThat(cn.exceptionQps()).isZero();
|
||||
}
|
||||
|
||||
try {
|
||||
fooService.foo(5763);
|
||||
fail("should not reach here");
|
||||
} catch (Exception ex) {
|
||||
assertThat(cn.exceptionQps()).isPositive();
|
||||
}
|
||||
|
||||
// Test for blockHandler
|
||||
FlowRuleManager.loadRules(Collections.singletonList(
|
||||
new FlowRule(resourceName).setCount(0)
|
||||
));
|
||||
assertThat(fooService.foo(1121)).isEqualTo("Oops, 1121");
|
||||
assertThat(cn.blockQps()).isPositive();
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor.integration.service;
|
||||
import com.alibaba.csp.sentinel.annotation.cdi.interceptor.SentinelResourceBinding;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
* @author sea
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class FooService {
|
||||
|
||||
@SentinelResourceBinding(value = "apiFoo", blockHandler = "fooBlockHandler",
|
||||
exceptionsToTrace = {IllegalArgumentException.class})
|
||||
public String foo(int i) throws Exception {
|
||||
if (i == 5758) {
|
||||
throw new IllegalAccessException();
|
||||
}
|
||||
if (i == 5763) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResourceBinding(value = "apiFooWithFallback", blockHandler = "fooBlockHandler", fallback = "fooFallbackFunc",
|
||||
exceptionsToTrace = {IllegalArgumentException.class})
|
||||
public String fooWithFallback(int i) throws Exception {
|
||||
if (i == 5758) {
|
||||
throw new IllegalAccessException();
|
||||
}
|
||||
if (i == 5763) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResourceBinding(value = "apiAnotherFooWithDefaultFallback", defaultFallback = "globalDefaultFallback",
|
||||
fallbackClass = {FooUtil.class})
|
||||
public String anotherFoo(int i) {
|
||||
if (i == 5758) {
|
||||
throw new IllegalArgumentException("oops");
|
||||
}
|
||||
return "Hello for " + i;
|
||||
}
|
||||
|
||||
@SentinelResourceBinding(blockHandler = "globalBlockHandler", blockHandlerClass = FooUtil.class)
|
||||
public int random() {
|
||||
return ThreadLocalRandom.current().nextInt(0, 30000);
|
||||
}
|
||||
|
||||
@SentinelResourceBinding(value = "apiBaz", blockHandler = "bazBlockHandler",
|
||||
exceptionsToIgnore = {IllegalMonitorStateException.class})
|
||||
public String baz(String name) {
|
||||
if (name.equals("fail")) {
|
||||
throw new IllegalMonitorStateException("boom!");
|
||||
}
|
||||
return "cheers, " + name;
|
||||
}
|
||||
|
||||
public String fooBlockHandler(int i, BlockException ex) {
|
||||
return "Oops, " + i;
|
||||
}
|
||||
|
||||
public String fooFallbackFunc(int i) {
|
||||
return "eee...";
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.annotation.cdi.interceptor.integration.service;
|
||||
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class FooUtil {
|
||||
|
||||
public static final int BLOCK_FLAG = 88888;
|
||||
public static final String FALLBACK_DEFAULT_RESULT = "fallback";
|
||||
|
||||
public static int globalBlockHandler(BlockException ex) {
|
||||
System.out.println("Oops: " + ex.getClass().getSimpleName());
|
||||
return BLOCK_FLAG;
|
||||
}
|
||||
|
||||
public static String globalDefaultFallback(Throwable t) {
|
||||
System.out.println("Fallback caught: " + t.getClass().getSimpleName());
|
||||
return FALLBACK_DEFAULT_RESULT;
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" bean-discovery-mode="all" version="2.0">
|
||||
<interceptors>
|
||||
<class>com.alibaba.csp.sentinel.annotation.cdi.interceptor.SentinelResourceInterceptor</class>
|
||||
</interceptors>
|
||||
</beans>
|
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-apollo</artifactId>
|
||||
|
||||
<properties>
|
||||
<apollo.version>1.5.0</apollo.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ctrip.framework.apollo</groupId>
|
||||
<artifactId>apollo-client</artifactId>
|
||||
<version>${apollo.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,100 @@
|
||||
package com.alibaba.csp.sentinel.datasource.apollo;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
import com.ctrip.framework.apollo.Config;
|
||||
import com.ctrip.framework.apollo.ConfigChangeListener;
|
||||
import com.ctrip.framework.apollo.ConfigService;
|
||||
import com.ctrip.framework.apollo.model.ConfigChange;
|
||||
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* A read-only {@code DataSource} with <a href="http://github.com/ctripcorp/apollo">Apollo</a> as its configuration
|
||||
* source.
|
||||
* <br />
|
||||
* When the rule is changed in Apollo, it will take effect in real time.
|
||||
*
|
||||
* @author Jason Song
|
||||
* @author Haojun Ren
|
||||
*/
|
||||
public class ApolloDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private final Config config;
|
||||
private final String ruleKey;
|
||||
private final String defaultRuleValue;
|
||||
|
||||
private ConfigChangeListener configChangeListener;
|
||||
|
||||
/**
|
||||
* Constructs the Apollo data source
|
||||
*
|
||||
* @param namespaceName the namespace name in Apollo, should not be null or empty
|
||||
* @param ruleKey the rule key in the namespace, should not be null or empty
|
||||
* @param defaultRuleValue the default rule value when the ruleKey is not found or any error
|
||||
* occurred
|
||||
* @param parser the parser to transform string configuration to actual flow rules
|
||||
*/
|
||||
public ApolloDataSource(String namespaceName, String ruleKey, String defaultRuleValue,
|
||||
Converter<String, T> parser) {
|
||||
super(parser);
|
||||
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(namespaceName), "Namespace name could not be null or empty");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(ruleKey), "RuleKey could not be null or empty!");
|
||||
|
||||
this.ruleKey = ruleKey;
|
||||
this.defaultRuleValue = defaultRuleValue;
|
||||
|
||||
this.config = ConfigService.getConfig(namespaceName);
|
||||
|
||||
initialize();
|
||||
|
||||
RecordLog.info("Initialized rule for namespace: {}, rule key: {}", namespaceName, ruleKey);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
initializeConfigChangeListener();
|
||||
loadAndUpdateRules();
|
||||
}
|
||||
|
||||
private void loadAndUpdateRules() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn("[ApolloDataSource] WARN: rule config is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Throwable ex) {
|
||||
RecordLog.warn("[ApolloDataSource] Error when loading rule config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeConfigChangeListener() {
|
||||
configChangeListener = new ConfigChangeListener() {
|
||||
@Override
|
||||
public void onChange(ConfigChangeEvent changeEvent) {
|
||||
ConfigChange change = changeEvent.getChange(ruleKey);
|
||||
//change is never null because the listener will only notify for this key
|
||||
if (change != null) {
|
||||
RecordLog.info("[ApolloDataSource] Received config changes: {}", change);
|
||||
}
|
||||
loadAndUpdateRules();
|
||||
}
|
||||
};
|
||||
config.addChangeListener(configChangeListener, Sets.newHashSet(ruleKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
return config.getProperty(ruleKey, defaultRuleValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
config.removeChangeListener(configChangeListener);
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
# Sentinel DataSource Consul
|
||||
|
||||
Sentinel DataSource Consul provides integration with Consul. The data source leverages blocking query (backed by
|
||||
long polling) of Consul.
|
||||
|
||||
## Usage
|
||||
|
||||
To use Sentinel DataSource Consul, you could add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-consul</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
Then you can create a `ConsulDataSource` and register to rule managers.
|
||||
For instance:
|
||||
|
||||
```java
|
||||
ReadableDataSource<String, List<FlowRule>> dataSource = new ConsulDataSource<>(host, port, ruleKey, waitTimeoutInSecond, flowConfigParser);
|
||||
FlowRuleManager.register2Property(dataSource.getProperty());
|
||||
```
|
||||
|
||||
- `ruleKey`: the rule persistence key
|
||||
- `waitTimeoutInSecond`: long polling timeout (in second) of the Consul API client
|
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-consul</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.source.version>1.8</java.source.version>
|
||||
<java.target.version>1.8</java.target.version>
|
||||
<consul.version>1.4.5</consul.version>
|
||||
<consul.process.version>2.2.0</consul.process.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.groovy</groupId>
|
||||
<artifactId>groovy-xml</artifactId>
|
||||
<version>3.0.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ecwid.consul</groupId>
|
||||
<artifactId>consul-api</artifactId>
|
||||
<version>${consul.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.pszymczyk.consul</groupId>
|
||||
<artifactId>embedded-consul</artifactId>
|
||||
<version>${consul.process.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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.datasource.consul;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
import com.ecwid.consul.v1.QueryParams;
|
||||
import com.ecwid.consul.v1.Response;
|
||||
import com.ecwid.consul.v1.kv.model.GetValue;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A read-only {@code DataSource} with Consul backend.
|
||||
* <p>
|
||||
* <p>
|
||||
* The data source first initial rules from a Consul during initialization.
|
||||
* Then it start a watcher to observe the updates of rule date and update to memory.
|
||||
*
|
||||
* Consul do not provide http api to watch the update of KV,so it use a long polling and
|
||||
* <a href="https://www.consul.io/api/features/blocking.html">blocking queries</a> of the Consul's feature
|
||||
* to watch and update value easily.When Querying data by index will blocking until change or timeout. If
|
||||
* the index of the current query is larger than before, it means that the data has changed.
|
||||
* </p>
|
||||
*
|
||||
* @author wavesZh
|
||||
* @author Zhiguo.Chen
|
||||
*/
|
||||
public class ConsulDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private static final int DEFAULT_PORT = 8500;
|
||||
|
||||
private final String address;
|
||||
private final String token;
|
||||
private final String ruleKey;
|
||||
/**
|
||||
* Request of query will hang until timeout (in second) or get updated value.
|
||||
*/
|
||||
private final int watchTimeout;
|
||||
|
||||
/**
|
||||
* Record the data's index in Consul to watch the change.
|
||||
* If lastIndex is smaller than the index of next query, it means that rule data has updated.
|
||||
*/
|
||||
private volatile long lastIndex;
|
||||
|
||||
private final ConsulClient client;
|
||||
|
||||
private final ConsulKVWatcher watcher = new ConsulKVWatcher();
|
||||
|
||||
@SuppressWarnings("PMD.ThreadPoolCreationRule")
|
||||
private final ExecutorService watcherService = Executors.newSingleThreadExecutor(
|
||||
new NamedThreadFactory("sentinel-consul-ds-watcher", true));
|
||||
|
||||
public ConsulDataSource(String host, String ruleKey, int watchTimeoutInSecond, Converter<String, T> parser) {
|
||||
this(host, DEFAULT_PORT, ruleKey, watchTimeoutInSecond, parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of {@code ConsulDataSource}.
|
||||
*
|
||||
* @param parser customized data parser, cannot be empty
|
||||
* @param host consul agent host
|
||||
* @param port consul agent port
|
||||
* @param ruleKey data key in Consul
|
||||
* @param watchTimeout request for querying data will be blocked until new data or timeout. The unit is second (s)
|
||||
*/
|
||||
public ConsulDataSource(String host, int port, String ruleKey, int watchTimeout, Converter<String, T> parser) {
|
||||
this(host, port, null, ruleKey, watchTimeout, parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of {@code ConsulDataSource}.
|
||||
*
|
||||
* @param parser customized data parser, cannot be empty
|
||||
* @param host consul agent host
|
||||
* @param port consul agent port
|
||||
* @param token consul agent acl token
|
||||
* @param ruleKey data key in Consul
|
||||
* @param watchTimeout request for querying data will be blocked until new data or timeout. The unit is second (s)
|
||||
*/
|
||||
public ConsulDataSource(String host, int port, String token, String ruleKey, int watchTimeout, Converter<String, T> parser) {
|
||||
super(parser);
|
||||
AssertUtil.notNull(host, "Consul host can not be null");
|
||||
AssertUtil.notEmpty(ruleKey, "Consul ruleKey can not be empty");
|
||||
AssertUtil.isTrue(watchTimeout >= 0, "watchTimeout should not be negative");
|
||||
this.client = new ConsulClient(host, port);
|
||||
this.address = host + ":" + port;
|
||||
this.token = token;
|
||||
this.ruleKey = ruleKey;
|
||||
this.watchTimeout = watchTimeout;
|
||||
loadInitialConfig();
|
||||
startKVWatcher();
|
||||
}
|
||||
|
||||
private void startKVWatcher() {
|
||||
watcherService.submit(watcher);
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn(
|
||||
"[ConsulDataSource] WARN: initial config is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[ConsulDataSource] Error when loading initial config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
if (this.client == null) {
|
||||
throw new IllegalStateException("Consul has not been initialized or error occurred");
|
||||
}
|
||||
Response<GetValue> response = getValueImmediately(ruleKey);
|
||||
if (response != null) {
|
||||
GetValue value = response.getValue();
|
||||
lastIndex = response.getConsulIndex();
|
||||
return value != null ? value.getDecodedValue() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
watcher.stop();
|
||||
watcherService.shutdown();
|
||||
}
|
||||
|
||||
private class ConsulKVWatcher implements Runnable {
|
||||
private volatile boolean running = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (running) {
|
||||
// It will be blocked until watchTimeout(s) if rule data has no update.
|
||||
Response<GetValue> response = getValue(ruleKey, lastIndex, watchTimeout);
|
||||
if (response == null) {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(watchTimeout * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
GetValue getValue = response.getValue();
|
||||
Long currentIndex = response.getConsulIndex();
|
||||
if (currentIndex == null || currentIndex <= lastIndex) {
|
||||
continue;
|
||||
}
|
||||
lastIndex = currentIndex;
|
||||
if (getValue != null) {
|
||||
String newValue = getValue.getDecodedValue();
|
||||
try {
|
||||
getProperty().updateValue(parser.convert(newValue));
|
||||
RecordLog.info("[ConsulDataSource] New property value received for ({}, {}): {}",
|
||||
address, ruleKey, newValue);
|
||||
} catch (Exception ex) {
|
||||
// In case of parsing error.
|
||||
RecordLog.warn("[ConsulDataSource] Failed to update value for ({}, {}), raw value: {}",
|
||||
address, ruleKey, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from Consul immediately.
|
||||
*
|
||||
* @param key data key in Consul
|
||||
* @return the value associated to the key, or null if error occurs
|
||||
*/
|
||||
private Response<GetValue> getValueImmediately(String key) {
|
||||
return getValue(key, -1, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from Consul (blocking).
|
||||
*
|
||||
* @param key data key in Consul
|
||||
* @param index the index of data in Consul.
|
||||
* @param waitTime time(second) for waiting get updated value.
|
||||
* @return the value associated to the key, or null if error occurs
|
||||
*/
|
||||
private Response<GetValue> getValue(String key, long index, long waitTime) {
|
||||
try {
|
||||
if (StringUtil.isNotBlank(token)) {
|
||||
return client.getKVValue(key, token, new QueryParams(waitTime, index));
|
||||
} else {
|
||||
return client.getKVValue(key, new QueryParams(waitTime, index));
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
RecordLog.warn("[ConsulDataSource] Failed to get value for key: " + key, t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.datasource.consul;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
import com.ecwid.consul.v1.Response;
|
||||
import com.pszymczyk.consul.ConsulProcess;
|
||||
import com.pszymczyk.consul.ConsulStarterBuilder;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author wavesZh
|
||||
*/
|
||||
public class ConsulDataSourceTest {
|
||||
|
||||
private final String ruleKey = "sentinel.rules.flow.ruleKey";
|
||||
private final int waitTimeoutInSecond = 1;
|
||||
|
||||
private ConsulProcess consul;
|
||||
private ConsulClient client;
|
||||
|
||||
private ReadableDataSource<String, List<FlowRule>> consulDataSource;
|
||||
|
||||
private List<FlowRule> rules;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
this.consul = ConsulStarterBuilder.consulStarter()
|
||||
.build()
|
||||
.start();
|
||||
int port = consul.getHttpPort();
|
||||
String host = "127.0.0.1";
|
||||
client = new ConsulClient(host, port);
|
||||
Converter<String, List<FlowRule>> flowConfigParser = buildFlowConfigParser();
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":500, \"controller\":null}]";
|
||||
initConsulRuleData(flowRulesJson);
|
||||
rules = flowConfigParser.convert(flowRulesJson);
|
||||
consulDataSource = new ConsulDataSource<>(host, port, ruleKey, waitTimeoutInSecond, flowConfigParser);
|
||||
FlowRuleManager.register2Property(consulDataSource.getProperty());
|
||||
}
|
||||
|
||||
@After
|
||||
public void clean() throws Exception {
|
||||
if (consulDataSource != null) {
|
||||
consulDataSource.close();
|
||||
}
|
||||
if (consul != null) {
|
||||
consul.close();
|
||||
}
|
||||
FlowRuleManager.loadRules(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsulDataSourceWhenInit() {
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(this.rules, rules);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsulDataSourceWhenUpdate() throws InterruptedException {
|
||||
rules.get(0).setMaxQueueingTimeMs(new Random().nextInt());
|
||||
client.setKVValue(ruleKey, JSON.toJSONString(rules));
|
||||
TimeUnit.SECONDS.sleep(waitTimeoutInSecond);
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(this.rules, rules);
|
||||
}
|
||||
|
||||
private Converter<String, List<FlowRule>> buildFlowConfigParser() {
|
||||
return source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {});
|
||||
}
|
||||
|
||||
private void initConsulRuleData(String flowRulesJson) {
|
||||
Response<Boolean> response = client.setKVValue(ruleKey, flowRulesJson);
|
||||
Assert.assertEquals(Boolean.TRUE, response.getValue());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
# Sentinel DataSource Etcd
|
||||
|
||||
Sentinel DataSource Etcd provides integration with etcd so that etcd
|
||||
can be the dynamic rule data source of Sentinel. The data source uses push model (watcher).
|
||||
|
||||
To use Sentinel DataSource Etcd, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-etcd</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
We could configure Etcd connection configuration by config file (for example `sentinel.properties`):
|
||||
|
||||
```
|
||||
csp.sentinel.etcd.endpoints=http://ip1:port1,http://ip2:port2
|
||||
csp.sentinel.etcd.user=your_user
|
||||
csp.sentinel.etcd.password=your_password
|
||||
csp.sentinel.etcd.charset=your_charset
|
||||
csp.sentinel.etcd.auth.enable=true # if ture, then open user/password or ssl check
|
||||
csp.sentinel.etcd.authority=authority # ssl
|
||||
```
|
||||
|
||||
Or we could configure via JVM -D args or via `SentinelConfig.setConfig(key, value)`.
|
||||
|
||||
Then we can create an `EtcdDataSource` and register to rule managers. For instance:
|
||||
|
||||
```java
|
||||
// `rule_key` is the rule config key
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleEtcdDataSource = new EtcdDataSource<>(rule_key, (rule) -> JSON.parseArray(rule, FlowRule.class));
|
||||
FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty());
|
||||
```
|
||||
|
||||
We've also provided an example: [sentinel-demo-etcd-datasource](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-etcd-datasource)
|
63
sentinel/sentinel-extension/sentinel-datasource-etcd/pom.xml
Normal file
63
sentinel/sentinel-extension/sentinel-datasource-etcd/pom.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-etcd</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<jetcd.version>0.3.0</jetcd.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.etcd</groupId>
|
||||
<artifactId>jetcd-core</artifactId>
|
||||
<version>${jetcd.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.datasource.etcd;
|
||||
|
||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Etcd connection configuration.
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public final class EtcdConfig {
|
||||
|
||||
public final static String END_POINTS = "csp.sentinel.etcd.endpoints";
|
||||
public final static String USER = "csp.sentinel.etcd.user";
|
||||
public final static String PASSWORD = "csp.sentinel.etcd.password";
|
||||
public final static String CHARSET = "csp.sentinel.etcd.charset";
|
||||
public final static String AUTH_ENABLE = "csp.sentinel.etcd.auth.enable";
|
||||
public final static String AUTHORITY = "csp.sentinel.etcd.authority";
|
||||
|
||||
private final static String ENABLED = "true";
|
||||
|
||||
public static String getEndPoints() {
|
||||
return SentinelConfig.getConfig(END_POINTS);
|
||||
}
|
||||
|
||||
public static String getUser() {
|
||||
return SentinelConfig.getConfig(USER);
|
||||
}
|
||||
|
||||
public static String getPassword() {
|
||||
return SentinelConfig.getConfig(PASSWORD);
|
||||
}
|
||||
|
||||
public static String getCharset() {
|
||||
String etcdCharset = SentinelConfig.getConfig(CHARSET);
|
||||
if (StringUtil.isNotBlank(etcdCharset)) {
|
||||
return etcdCharset;
|
||||
}
|
||||
return SentinelConfig.charset();
|
||||
}
|
||||
|
||||
public static boolean isAuthEnable() {
|
||||
return ENABLED.equalsIgnoreCase(SentinelConfig.getConfig(AUTH_ENABLE));
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return SentinelConfig.getConfig(AUTHORITY);
|
||||
}
|
||||
|
||||
private EtcdConfig() {}
|
||||
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.datasource.etcd;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
import io.etcd.jetcd.ByteSequence;
|
||||
import io.etcd.jetcd.Client;
|
||||
import io.etcd.jetcd.KeyValue;
|
||||
import io.etcd.jetcd.Watch;
|
||||
import io.etcd.jetcd.kv.GetResponse;
|
||||
import io.etcd.jetcd.watch.WatchEvent;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* A read-only {@code DataSource} with Etcd backend. When the data in Etcd backend has been modified,
|
||||
* Etcd will automatically push the new value so that the dynamic configuration can be real-time.
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public class EtcdDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private final Client client;
|
||||
private Watch.Watcher watcher;
|
||||
|
||||
private final String key;
|
||||
private Charset charset = Charset.forName(EtcdConfig.getCharset());
|
||||
|
||||
/**
|
||||
* Create an etcd data-source. The connection configuration will be retrieved from {@link EtcdConfig}.
|
||||
*
|
||||
* @param key config key
|
||||
* @param parser data parser
|
||||
*/
|
||||
public EtcdDataSource(String key, Converter<String, T> parser) {
|
||||
super(parser);
|
||||
if (!EtcdConfig.isAuthEnable()) {
|
||||
this.client = Client.builder()
|
||||
.endpoints(EtcdConfig.getEndPoints().split(",")).build();
|
||||
} else {
|
||||
this.client = Client.builder()
|
||||
.endpoints(EtcdConfig.getEndPoints().split(","))
|
||||
.user(ByteSequence.from(EtcdConfig.getUser(), charset))
|
||||
.password(ByteSequence.from(EtcdConfig.getPassword(), charset))
|
||||
.authority(EtcdConfig.getAuthority())
|
||||
.build();
|
||||
}
|
||||
this.key = key;
|
||||
loadInitialConfig();
|
||||
initWatcher();
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn(
|
||||
"[EtcdDataSource] Initial configuration is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[EtcdDataSource] Error when loading initial configuration", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initWatcher() {
|
||||
watcher = client.getWatchClient().watch(ByteSequence.from(key, charset), (watchResponse) -> {
|
||||
for (WatchEvent watchEvent : watchResponse.getEvents()) {
|
||||
WatchEvent.EventType eventType = watchEvent.getEventType();
|
||||
if (eventType == WatchEvent.EventType.PUT) {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[EtcdDataSource] Failed to update config", e);
|
||||
}
|
||||
} else if (eventType == WatchEvent.EventType.DELETE) {
|
||||
RecordLog.info("[EtcdDataSource] Cleaning config for key <{}>", key);
|
||||
getProperty().updateValue(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, charset));
|
||||
List<KeyValue> kvs = responseFuture.get().getKvs();
|
||||
return kvs.size() == 0 ? null : kvs.get(0).getValue().toString(charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (watcher != null) {
|
||||
try {
|
||||
watcher.close();
|
||||
} catch (Exception ex) {
|
||||
RecordLog.info("[EtcdDataSource] Failed to close watcher", ex);
|
||||
}
|
||||
}
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.datasource.etcd;
|
||||
|
||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import io.etcd.jetcd.ByteSequence;
|
||||
import io.etcd.jetcd.Client;
|
||||
import io.etcd.jetcd.KV;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@Ignore(value = "Before run this test, you need to set up your etcd server.")
|
||||
public class EtcdDataSourceTest {
|
||||
|
||||
|
||||
private final String endPoints = "http://127.0.0.1:2379";
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
SentinelConfig.setConfig(EtcdConfig.END_POINTS, endPoints);
|
||||
FlowRuleManager.loadRules(new ArrayList<>());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
SentinelConfig.setConfig(EtcdConfig.END_POINTS, "");
|
||||
FlowRuleManager.loadRules(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadSource() throws Exception {
|
||||
EtcdDataSource dataSource = new EtcdDataSource("foo", value -> value);
|
||||
KV kvClient = Client.builder()
|
||||
.endpoints(endPoints)
|
||||
.build().getKVClient();
|
||||
|
||||
kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test".getBytes()));
|
||||
Assert.assertNotNull(dataSource.readSource().equals("test"));
|
||||
|
||||
kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test2".getBytes()));
|
||||
Assert.assertNotNull(dataSource.getProperty().equals("test2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDynamicUpdate() throws InterruptedException {
|
||||
String demo_key = "etcd_demo_key";
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleEtcdDataSource = new EtcdDataSource<>(demo_key, (value) -> JSON.parseArray(value, FlowRule.class));
|
||||
FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty());
|
||||
|
||||
KV kvClient = Client.builder()
|
||||
.endpoints(endPoints)
|
||||
.build().getKVClient();
|
||||
|
||||
final String rule1 = "[\n"
|
||||
+ " {\n"
|
||||
+ " \"resource\": \"TestResource\",\n"
|
||||
+ " \"controlBehavior\": 0,\n"
|
||||
+ " \"count\": 5.0,\n"
|
||||
+ " \"grade\": 1,\n"
|
||||
+ " \"limitApp\": \"default\",\n"
|
||||
+ " \"strategy\": 0\n"
|
||||
+ " }\n"
|
||||
+ "]";
|
||||
|
||||
kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule1.getBytes()));
|
||||
Thread.sleep(1000);
|
||||
|
||||
FlowRule flowRule = FlowRuleManager.getRules().get(0);
|
||||
Assert.assertTrue(flowRule.getResource().equals("TestResource"));
|
||||
Assert.assertTrue(flowRule.getCount() == 5.0);
|
||||
Assert.assertTrue(flowRule.getGrade() == 1);
|
||||
|
||||
final String rule2 = "[\n"
|
||||
+ " {\n"
|
||||
+ " \"resource\": \"TestResource\",\n"
|
||||
+ " \"controlBehavior\": 0,\n"
|
||||
+ " \"count\": 6.0,\n"
|
||||
+ " \"grade\": 3,\n"
|
||||
+ " \"limitApp\": \"default\",\n"
|
||||
+ " \"strategy\": 0\n"
|
||||
+ " }\n"
|
||||
+ "]";
|
||||
|
||||
kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule2.getBytes()));
|
||||
Thread.sleep(1000);
|
||||
|
||||
flowRule = FlowRuleManager.getRules().get(0);
|
||||
Assert.assertTrue(flowRule.getResource().equals("TestResource"));
|
||||
Assert.assertTrue(flowRule.getCount() == 6.0);
|
||||
Assert.assertTrue(flowRule.getGrade() == 3);
|
||||
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
# Sentinel DataSource Eureka
|
||||
|
||||
Sentinel DataSource Eureka provides integration with [Eureka](https://github.com/Netflix/eureka) so that Eureka
|
||||
can be the dynamic rule data source of Sentinel.
|
||||
|
||||
To use Sentinel DataSource Eureka, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-eureka</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Then you can create an `EurekaDataSource` and register to rule managers.
|
||||
|
||||
SDK usage:
|
||||
|
||||
```java
|
||||
EurekaDataSource<List<FlowRule>> eurekaDataSource = new EurekaDataSource("app-id", "instance-id",
|
||||
Arrays.asList("http://localhost:8761/eureka", "http://localhost:8762/eureka", "http://localhost:8763/eureka"),
|
||||
"rule-key", flowRuleParser);
|
||||
FlowRuleManager.register2Property(eurekaDataSource.getProperty());
|
||||
```
|
||||
|
||||
Example for Spring Cloud Application:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public EurekaDataSource<List<FlowRule>> eurekaDataSource(EurekaInstanceConfig eurekaInstanceConfig, EurekaClientConfig eurekaClientConfig) {
|
||||
|
||||
List<String> serviceUrls = EndpointUtils.getServiceUrlsFromConfig(eurekaClientConfig,
|
||||
eurekaInstanceConfig.getMetadataMap().get("zone"), eurekaClientConfig.shouldPreferSameZoneEureka());
|
||||
|
||||
EurekaDataSource<List<FlowRule>> eurekaDataSource = new EurekaDataSource(eurekaInstanceConfig.getAppname(),
|
||||
eurekaInstanceConfig.getInstanceId(), serviceUrls, "flowrules", new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String o) {
|
||||
return JSON.parseObject(o, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
FlowRuleManager.register2Property(eurekaDataSource.getProperty());
|
||||
return eurekaDataSource;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To refresh the rule dynamically, you need to call [Eureka-REST-operations](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations)
|
||||
to update instance metadata:
|
||||
|
||||
```
|
||||
PUT /eureka/apps/{appID}/{instanceID}/metadata?{ruleKey}={json of the rules}
|
||||
```
|
||||
|
||||
Note: don't forget to encode your JSON string in the url.
|
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-eureka</artifactId>
|
||||
|
||||
<properties>
|
||||
<spring.cloud.version>2.1.2.RELEASE</spring.cloud.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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
|
||||
*
|
||||
* https://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.datasource.eureka;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.AutoRefreshDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link ReadableDataSource} based on Eureka. This class will automatically
|
||||
* fetches the metadata of the instance every period.
|
||||
* </p>
|
||||
* <p>
|
||||
* Limitations: Default refresh interval is 10s. Because there is synchronization between eureka servers,
|
||||
* it may take longer to take effect.
|
||||
* </p>
|
||||
*
|
||||
* @author liyang
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public class EurekaDataSource<T> extends AutoRefreshDataSource<String, T> {
|
||||
|
||||
private static final long DEFAULT_REFRESH_MS = 10000;
|
||||
|
||||
/**
|
||||
* Default connect timeout: 3s
|
||||
*/
|
||||
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 3000;
|
||||
|
||||
/**
|
||||
* Default read timeout: 30s
|
||||
*/
|
||||
private static final int DEFAULT_READ_TIMEOUT_MS = 30000;
|
||||
|
||||
private final int connectTimeoutMills;
|
||||
private final int readTimeoutMills;
|
||||
|
||||
/**
|
||||
* Eureka instance app ID.
|
||||
*/
|
||||
private final String appId;
|
||||
/**
|
||||
* Eureka instance id.
|
||||
*/
|
||||
private final String instanceId;
|
||||
|
||||
/**
|
||||
* Eureka server URL list.
|
||||
*/
|
||||
private final List<String> serviceUrls;
|
||||
|
||||
/**
|
||||
* Metadata key of the rule source.
|
||||
*/
|
||||
private final String ruleKey;
|
||||
|
||||
public EurekaDataSource(String appId, String instanceId, List<String> serviceUrls, String ruleKey,
|
||||
Converter<String, T> configParser) {
|
||||
this(appId, instanceId, serviceUrls, ruleKey, configParser, DEFAULT_REFRESH_MS, DEFAULT_CONNECT_TIMEOUT_MS,
|
||||
DEFAULT_READ_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
public EurekaDataSource(String appId, String instanceId, List<String> serviceUrls, String ruleKey,
|
||||
Converter<String, T> configParser, long refreshMs, int connectTimeoutMills,
|
||||
int readTimeoutMills) {
|
||||
super(configParser, refreshMs);
|
||||
AssertUtil.notNull(appId, "appId can't be null");
|
||||
AssertUtil.notNull(instanceId, "instanceId can't be null");
|
||||
AssertUtil.assertNotEmpty(serviceUrls, "serviceUrls can't be empty");
|
||||
AssertUtil.notNull(ruleKey, "ruleKey can't be null");
|
||||
AssertUtil.assertState(connectTimeoutMills > 0, "connectTimeoutMills must be greater than 0");
|
||||
AssertUtil.assertState(readTimeoutMills > 0, "readTimeoutMills must be greater than 0");
|
||||
|
||||
this.appId = appId;
|
||||
this.instanceId = instanceId;
|
||||
this.serviceUrls = ensureEndWithSlash(serviceUrls);
|
||||
AssertUtil.assertNotEmpty(this.serviceUrls, "No available service url");
|
||||
this.ruleKey = ruleKey;
|
||||
this.connectTimeoutMills = connectTimeoutMills;
|
||||
this.readTimeoutMills = readTimeoutMills;
|
||||
}
|
||||
|
||||
private List<String> ensureEndWithSlash(List<String> serviceUrls) {
|
||||
List<String> newServiceUrls = new ArrayList<>();
|
||||
for (String serviceUrl : serviceUrls) {
|
||||
if (StringUtil.isBlank(serviceUrl)) {
|
||||
continue;
|
||||
}
|
||||
if (!serviceUrl.endsWith("/")) {
|
||||
serviceUrl = serviceUrl + "/";
|
||||
}
|
||||
newServiceUrls.add(serviceUrl);
|
||||
}
|
||||
return newServiceUrls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
return fetchStringSourceFromEurekaMetadata(this.appId, this.instanceId, this.serviceUrls, ruleKey);
|
||||
}
|
||||
|
||||
private String fetchStringSourceFromEurekaMetadata(String appId, String instanceId, List<String> serviceUrls,
|
||||
String ruleKey) throws Exception {
|
||||
List<String> shuffleUrls = new ArrayList<>(serviceUrls.size());
|
||||
shuffleUrls.addAll(serviceUrls);
|
||||
Collections.shuffle(shuffleUrls);
|
||||
for (int i = 0; i < shuffleUrls.size(); i++) {
|
||||
String serviceUrl = shuffleUrls.get(i) + String.format("apps/%s/%s", appId, instanceId);
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
conn = (HttpURLConnection) new URL(serviceUrl).openConnection();
|
||||
conn.addRequestProperty("Accept", "application/json;charset=utf-8");
|
||||
|
||||
conn.setConnectTimeout(connectTimeoutMills);
|
||||
conn.setReadTimeout(readTimeoutMills);
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setDoOutput(true);
|
||||
conn.connect();
|
||||
RecordLog.debug("[EurekaDataSource] Request from eureka server: " + serviceUrl);
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
String s = toString(conn.getInputStream());
|
||||
String ruleString = JSON.parseObject(s)
|
||||
.getJSONObject("instance")
|
||||
.getJSONObject("metadata")
|
||||
.getString(ruleKey);
|
||||
return ruleString;
|
||||
}
|
||||
RecordLog.warn("[EurekaDataSource] Warn: retrying on another server if available " +
|
||||
"due to response code: {}, response message: {}", conn.getResponseCode(),
|
||||
toString(conn.getErrorStream()));
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
if (conn != null) {
|
||||
RecordLog.warn("[EurekaDataSource] Warn: failed to request " + conn.getURL() + " from "
|
||||
+ InetAddress.getByName(conn.getURL().getHost()).getHostAddress(), e);
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
RecordLog.warn("[EurekaDataSource] Warn: failed to request ", e1);
|
||||
//ignore
|
||||
}
|
||||
RecordLog.warn("[EurekaDataSource] Warn: failed to request,retrying on another server if available");
|
||||
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new EurekaMetadataFetchException("Can't get any data");
|
||||
}
|
||||
|
||||
public static class EurekaMetadataFetchException extends Exception {
|
||||
|
||||
public EurekaMetadataFetchException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(InputStream input) throws IOException {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
InputStreamReader inputStreamReader = new InputStreamReader(input, "utf-8");
|
||||
CharArrayWriter sw = new CharArrayWriter();
|
||||
copy(inputStreamReader, sw);
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
private long copy(Reader input, Writer output) throws IOException {
|
||||
char[] buffer = new char[1 << 12];
|
||||
long count = 0;
|
||||
for (int n = 0; (n = input.read(buffer)) >= 0; ) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.datasource.eureka;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* @author liyang
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@EnableEurekaServer
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
public class EurekaDataSourceTest {
|
||||
|
||||
private static final String SENTINEL_KEY = "sentinel-rules";
|
||||
|
||||
@Value("${server.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${eureka.instance.appname}")
|
||||
private String appname;
|
||||
|
||||
@Value("${eureka.instance.instance-id}")
|
||||
private String instanceId;
|
||||
|
||||
|
||||
@Test
|
||||
public void testEurekaDataSource() throws Exception {
|
||||
String url = "http://localhost:" + port + "/eureka";
|
||||
|
||||
EurekaDataSource<List<FlowRule>> eurekaDataSource = new EurekaDataSource(appname, instanceId, Arrays.asList(url)
|
||||
, SENTINEL_KEY, new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
FlowRuleManager.register2Property(eurekaDataSource.getProperty());
|
||||
|
||||
await().timeout(15, TimeUnit.SECONDS)
|
||||
.until(new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return FlowRuleManager.getRules().size() > 0;
|
||||
}
|
||||
});
|
||||
Assert.assertTrue(FlowRuleManager.getRules().size() > 0);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2018 the original author or authors.
|
||||
*
|
||||
* 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.datasource.eureka;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author liyang
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class SimpleSpringApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SimpleSpringApplication.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
server:
|
||||
port: 8761
|
||||
eureka:
|
||||
instance:
|
||||
instance-id: instance-0
|
||||
appname: testapp
|
||||
metadata-map:
|
||||
sentinel-rules: "[{'clusterMode':false,'controlBehavior':0,'count':20.0,'grade':1,'limitApp':'default','maxQueueingTimeMs':500,'resource':'resource-demo-name','strategy':0,'warmUpPeriodSec':10}]"
|
||||
|
||||
client:
|
||||
register-with-eureka: true
|
||||
fetch-registry: false
|
||||
service-url:
|
||||
defaultZone: http://localhost:8761/eureka/
|
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import com.alibaba.csp.sentinel.property.DynamicSentinelProperty;
|
||||
import com.alibaba.csp.sentinel.property.SentinelProperty;
|
||||
|
||||
/**
|
||||
* The abstract readable data source provides basic functionality for loading and parsing config.
|
||||
*
|
||||
* @param <S> source data type
|
||||
* @param <T> target data type
|
||||
* @author Carpenter Lee
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public abstract class AbstractDataSource<S, T> implements ReadableDataSource<S, T> {
|
||||
|
||||
protected final Converter<S, T> parser;
|
||||
protected final SentinelProperty<T> property;
|
||||
|
||||
public AbstractDataSource(Converter<S, T> parser) {
|
||||
if (parser == null) {
|
||||
throw new IllegalArgumentException("parser can't be null");
|
||||
}
|
||||
this.parser = parser;
|
||||
this.property = new DynamicSentinelProperty<T>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T loadConfig() throws Exception {
|
||||
return loadConfig(readSource());
|
||||
}
|
||||
|
||||
public T loadConfig(S conf) throws Exception {
|
||||
T value = parser.convert(conf);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SentinelProperty<T> getProperty() {
|
||||
return property;
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
/**
|
||||
* A {@link ReadableDataSource} automatically fetches the backend data.
|
||||
*
|
||||
* @param <S> source data type
|
||||
* @param <T> target data type
|
||||
* @author Carpenter Lee
|
||||
*/
|
||||
public abstract class AutoRefreshDataSource<S, T> extends AbstractDataSource<S, T> {
|
||||
|
||||
private ScheduledExecutorService service;
|
||||
protected long recommendRefreshMs = 3000;
|
||||
|
||||
public AutoRefreshDataSource(Converter<S, T> configParser) {
|
||||
super(configParser);
|
||||
startTimerService();
|
||||
}
|
||||
|
||||
public AutoRefreshDataSource(Converter<S, T> configParser, final long recommendRefreshMs) {
|
||||
super(configParser);
|
||||
if (recommendRefreshMs <= 0) {
|
||||
throw new IllegalArgumentException("recommendRefreshMs must > 0, but " + recommendRefreshMs + " get");
|
||||
}
|
||||
this.recommendRefreshMs = recommendRefreshMs;
|
||||
startTimerService();
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.ThreadPoolCreationRule")
|
||||
private void startTimerService() {
|
||||
service = Executors.newScheduledThreadPool(1,
|
||||
new NamedThreadFactory("sentinel-datasource-auto-refresh-task", true));
|
||||
service.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!isModified()) {
|
||||
return;
|
||||
}
|
||||
T newValue = loadConfig();
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Throwable e) {
|
||||
RecordLog.info("loadConfig exception", e);
|
||||
}
|
||||
}
|
||||
}, recommendRefreshMs, recommendRefreshMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (service != null) {
|
||||
service.shutdownNow();
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isModified() {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
/**
|
||||
* Convert an object from source type {@code S} to target type {@code T}.
|
||||
*
|
||||
* @author leyou
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public interface Converter<S, T> {
|
||||
|
||||
/**
|
||||
* Convert {@code source} to the target type.
|
||||
*
|
||||
* @param source the source object
|
||||
* @return the target object
|
||||
*/
|
||||
T convert(S source);
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import com.alibaba.csp.sentinel.property.NoOpSentinelProperty;
|
||||
import com.alibaba.csp.sentinel.property.SentinelProperty;
|
||||
|
||||
/**
|
||||
* A {@link ReadableDataSource} based on nothing. {@link EmptyDataSource#getProperty()} will always return the same cached
|
||||
* {@link SentinelProperty} that doing nothing.
|
||||
* <br/>
|
||||
* This class is used when we want to use default settings instead of configs from the {@link ReadableDataSource}.
|
||||
*
|
||||
* @author leyou
|
||||
*/
|
||||
public final class EmptyDataSource implements ReadableDataSource<Object, Object> {
|
||||
|
||||
public static final ReadableDataSource<Object, Object> EMPTY_DATASOURCE = new EmptyDataSource();
|
||||
|
||||
private static final SentinelProperty<Object> PROPERTY = new NoOpSentinelProperty();
|
||||
|
||||
private EmptyDataSource() { }
|
||||
|
||||
@Override
|
||||
public Object loadConfig() throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readSource() throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SentinelProperty<Object> getProperty() {
|
||||
return PROPERTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception { }
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link ReadableDataSource} based on jar file. This class can only read file initially when it loads file.
|
||||
* </p>
|
||||
* <p>
|
||||
* Limitations: Default read buffer size is 1 MB, while max allowed buffer size is 4MB.
|
||||
* File size should not exceed the buffer size, or exception will be thrown. Default charset is UTF-8.
|
||||
* </p>
|
||||
*
|
||||
* @author dingq
|
||||
* @author Eric Zhao
|
||||
* @since 1.6.0
|
||||
*/
|
||||
public class FileInJarReadableDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private static final int MAX_SIZE = 1024 * 1024 * 4;
|
||||
private static final int DEFAULT_BUF_SIZE = 1024 * 1024;
|
||||
private static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");
|
||||
|
||||
private final Charset charset;
|
||||
private final String jarName;
|
||||
private final String fileInJarName;
|
||||
|
||||
private byte[] buf;
|
||||
private JarEntry jarEntry;
|
||||
private JarFile jarFile;
|
||||
|
||||
/**
|
||||
* @param jarName the jar to read
|
||||
* @param fileInJarName the file in jar to read
|
||||
* @param configParser the config decoder (parser)
|
||||
* @throws IOException if IO failure occurs
|
||||
*/
|
||||
public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter<String, T> configParser)
|
||||
throws IOException {
|
||||
this(jarName, fileInJarName, configParser, DEFAULT_BUF_SIZE, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter<String, T> configParser,
|
||||
int bufSize) throws IOException {
|
||||
this(jarName, fileInJarName, configParser, bufSize, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter<String, T> configParser,
|
||||
Charset charset) throws IOException {
|
||||
this(jarName, fileInJarName, configParser, DEFAULT_BUF_SIZE, charset);
|
||||
}
|
||||
|
||||
public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter<String, T> configParser,
|
||||
int bufSize, Charset charset) throws IOException {
|
||||
super(configParser);
|
||||
AssertUtil.assertNotBlank(jarName, "jarName cannot be blank");
|
||||
AssertUtil.assertNotBlank(fileInJarName, "fileInJarName cannot be blank");
|
||||
if (bufSize <= 0 || bufSize > MAX_SIZE) {
|
||||
throw new IllegalArgumentException("bufSize must between (0, " + MAX_SIZE + "], but " + bufSize + " get");
|
||||
}
|
||||
AssertUtil.notNull(charset, "charset can't be null");
|
||||
this.buf = new byte[bufSize];
|
||||
this.charset = charset;
|
||||
this.jarName = jarName;
|
||||
this.fileInJarName = fileInJarName;
|
||||
initializeJar();
|
||||
firstLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
if (null == jarEntry) {
|
||||
// Will throw FileNotFoundException later.
|
||||
RecordLog.warn(String.format("[FileInJarReadableDataSource] File does not exist: %s", jarFile.getName()));
|
||||
}
|
||||
try (InputStream inputStream = jarFile.getInputStream(jarEntry)) {
|
||||
if (inputStream.available() > buf.length) {
|
||||
throw new IllegalStateException(String.format("Size of file <%s> exceeds the bufSize (%d): %d",
|
||||
jarFile.getName(), buf.length, inputStream.available()));
|
||||
}
|
||||
int len = inputStream.read(buf);
|
||||
return new String(buf, 0, len, charset);
|
||||
}
|
||||
}
|
||||
|
||||
private void firstLoad() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Throwable e) {
|
||||
RecordLog.warn("[FileInJarReadableDataSource] Error when loading config", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
buf = null;
|
||||
}
|
||||
|
||||
private void initializeJar() throws IOException {
|
||||
this.jarFile = new JarFile(jarName);
|
||||
this.jarEntry = jarFile.getJarEntry(fileInJarName);
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link ReadableDataSource} based on file. This class will automatically
|
||||
* fetches the backend file every isModified period.
|
||||
* </p>
|
||||
* <p>
|
||||
* Limitations: Default read buffer size is 1 MB. If file size is greater than
|
||||
* buffer size, exceeding bytes will be ignored. Default charset is UTF-8.
|
||||
* </p>
|
||||
*
|
||||
* @author Carpenter Lee
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class FileRefreshableDataSource<T> extends AutoRefreshDataSource<String, T> {
|
||||
|
||||
private static final int MAX_SIZE = 1024 * 1024 * 4;
|
||||
private static final long DEFAULT_REFRESH_MS = 3000;
|
||||
private static final int DEFAULT_BUF_SIZE = 1024 * 1024;
|
||||
private static final Charset DEFAULT_CHAR_SET = Charset.forName("utf-8");
|
||||
|
||||
private byte[] buf;
|
||||
private final Charset charset;
|
||||
private final File file;
|
||||
|
||||
private long lastModified = 0L;
|
||||
|
||||
/**
|
||||
* Create a file based {@link ReadableDataSource} whose read buffer size is
|
||||
* 1MB, charset is UTF8, and read interval is 3 seconds.
|
||||
*
|
||||
* @param file the file to read
|
||||
* @param configParser the config decoder (parser)
|
||||
*/
|
||||
public FileRefreshableDataSource(File file, Converter<String, T> configParser) throws FileNotFoundException {
|
||||
this(file, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, DEFAULT_CHAR_SET);
|
||||
}
|
||||
|
||||
public FileRefreshableDataSource(String fileName, Converter<String, T> configParser) throws FileNotFoundException {
|
||||
this(new File(fileName), configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, DEFAULT_CHAR_SET);
|
||||
}
|
||||
|
||||
public FileRefreshableDataSource(File file, Converter<String, T> configParser, int bufSize)
|
||||
throws FileNotFoundException {
|
||||
this(file, configParser, DEFAULT_REFRESH_MS, bufSize, DEFAULT_CHAR_SET);
|
||||
}
|
||||
|
||||
public FileRefreshableDataSource(File file, Converter<String, T> configParser, Charset charset)
|
||||
throws FileNotFoundException {
|
||||
this(file, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, charset);
|
||||
}
|
||||
|
||||
public FileRefreshableDataSource(File file, Converter<String, T> configParser, long recommendRefreshMs, int bufSize,
|
||||
Charset charset) throws FileNotFoundException {
|
||||
super(configParser, recommendRefreshMs);
|
||||
if (bufSize <= 0 || bufSize > MAX_SIZE) {
|
||||
throw new IllegalArgumentException("bufSize must between (0, " + MAX_SIZE + "], but " + bufSize + " get");
|
||||
}
|
||||
if (file == null || file.isDirectory()) {
|
||||
throw new IllegalArgumentException("File can't be null or a directory");
|
||||
}
|
||||
if (charset == null) {
|
||||
throw new IllegalArgumentException("charset can't be null");
|
||||
}
|
||||
this.buf = new byte[bufSize];
|
||||
this.file = file;
|
||||
this.charset = charset;
|
||||
// If the file does not exist, the last modified will be 0.
|
||||
this.lastModified = file.lastModified();
|
||||
firstLoad();
|
||||
}
|
||||
|
||||
private void firstLoad() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Throwable e) {
|
||||
RecordLog.info("loadConfig exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
if (!file.exists()) {
|
||||
// Will throw FileNotFoundException later.
|
||||
RecordLog.warn(String.format("[FileRefreshableDataSource] File does not exist: %s", file.getAbsolutePath()));
|
||||
}
|
||||
FileInputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(file);
|
||||
FileChannel channel = inputStream.getChannel();
|
||||
if (channel.size() > buf.length) {
|
||||
throw new IllegalStateException(file.getAbsolutePath() + " file size=" + channel.size()
|
||||
+ ", is bigger than bufSize=" + buf.length + ". Can't read");
|
||||
}
|
||||
int len = inputStream.read(buf);
|
||||
return new String(buf, 0, len, charset);
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isModified() {
|
||||
long curLastModified = file.lastModified();
|
||||
if (curLastModified != this.lastModified) {
|
||||
this.lastModified = curLastModified;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
super.close();
|
||||
buf = null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
/**
|
||||
* A {@link WritableDataSource} based on file.
|
||||
*
|
||||
* @param <T> data type
|
||||
* @author Eric Zhao
|
||||
* @since 0.2.0
|
||||
*/
|
||||
public class FileWritableDataSource<T> implements WritableDataSource<T> {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
private final Converter<T, String> configEncoder;
|
||||
private final File file;
|
||||
private final Charset charset;
|
||||
|
||||
private final Lock lock = new ReentrantLock(true);
|
||||
|
||||
public FileWritableDataSource(String filePath, Converter<T, String> configEncoder) {
|
||||
this(new File(filePath), configEncoder);
|
||||
}
|
||||
|
||||
public FileWritableDataSource(File file, Converter<T, String> configEncoder) {
|
||||
this(file, configEncoder, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public FileWritableDataSource(File file, Converter<T, String> configEncoder, Charset charset) {
|
||||
if (file == null || file.isDirectory()) {
|
||||
throw new IllegalArgumentException("Bad file");
|
||||
}
|
||||
if (configEncoder == null) {
|
||||
throw new IllegalArgumentException("Config encoder cannot be null");
|
||||
}
|
||||
if (charset == null) {
|
||||
throw new IllegalArgumentException("Charset cannot be null");
|
||||
}
|
||||
this.configEncoder = configEncoder;
|
||||
this.file = file;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(T value) throws Exception {
|
||||
lock.lock();
|
||||
try {
|
||||
String convertResult = configEncoder.convert(value);
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(file);
|
||||
byte[] bytesArray = convertResult.getBytes(charset);
|
||||
|
||||
RecordLog.info("[FileWritableDataSource] Writing to file {}: {}", file, convertResult);
|
||||
outputStream.write(bytesArray);
|
||||
outputStream.flush();
|
||||
} finally {
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (Exception ignore) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
// Nothing
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
import com.alibaba.csp.sentinel.property.SentinelProperty;
|
||||
|
||||
/**
|
||||
* The readable data source is responsible for retrieving configs (read-only).
|
||||
*
|
||||
* @param <S> source data type
|
||||
* @param <T> target data type
|
||||
* @author leyou
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public interface ReadableDataSource<S, T> {
|
||||
|
||||
/**
|
||||
* Load data data source as the target type.
|
||||
*
|
||||
* @return the target data.
|
||||
* @throws Exception IO or other error occurs
|
||||
*/
|
||||
T loadConfig() throws Exception;
|
||||
|
||||
/**
|
||||
* Read original data from the data source.
|
||||
*
|
||||
* @return the original data.
|
||||
* @throws Exception IO or other error occurs
|
||||
*/
|
||||
S readSource() throws Exception;
|
||||
|
||||
/**
|
||||
* Get {@link SentinelProperty} of the data source.
|
||||
*
|
||||
* @return the property.
|
||||
*/
|
||||
SentinelProperty<T> getProperty();
|
||||
|
||||
/**
|
||||
* Close the data source.
|
||||
*
|
||||
* @throws Exception IO or other error occurs
|
||||
*/
|
||||
void close() throws Exception;
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.datasource;
|
||||
|
||||
/**
|
||||
* Interface of writable data source support.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
* @since 0.2.0
|
||||
*/
|
||||
public interface WritableDataSource<T> {
|
||||
|
||||
/**
|
||||
* Write the {@code value} to the data source.
|
||||
*
|
||||
* @param value value to write
|
||||
* @throws Exception IO or other error occurs
|
||||
*/
|
||||
void write(T value) throws Exception;
|
||||
|
||||
/**
|
||||
* Close the data source.
|
||||
*
|
||||
* @throws Exception IO or other error occurs
|
||||
*/
|
||||
void close() throws Exception;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
# Sentinel DataSource Nacos
|
||||
|
||||
Sentinel DataSource Nacos provides integration with [Nacos](http://nacos.io) so that Nacos
|
||||
can be the dynamic rule data source of Sentinel.
|
||||
|
||||
To use Sentinel DataSource Nacos, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Then you can create an `NacosDataSource` and register to rule managers.
|
||||
For instance:
|
||||
|
||||
```java
|
||||
// remoteAddress is the address of Nacos
|
||||
// groupId and dataId are concepts of Nacos
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
|
||||
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
|
||||
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
|
||||
```
|
||||
|
||||
We've also provided an example: [sentinel-demo-nacos-datasource](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-nacos-datasource).
|
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<nacos.version>1.3.0</nacos.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.nacos</groupId>
|
||||
<artifactId>nacos-client</artifactId>
|
||||
<version>${nacos.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.datasource.nacos;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import com.alibaba.nacos.api.NacosFactory;
|
||||
import com.alibaba.nacos.api.PropertyKeyConst;
|
||||
import com.alibaba.nacos.api.config.ConfigService;
|
||||
import com.alibaba.nacos.api.config.listener.Listener;
|
||||
|
||||
/**
|
||||
* A read-only {@code DataSource} with Nacos backend. When the data in Nacos backend has been modified,
|
||||
* Nacos will automatically push the new value so that the dynamic configuration can be real-time.
|
||||
*
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class NacosDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private static final int DEFAULT_TIMEOUT = 3000;
|
||||
|
||||
/**
|
||||
* Single-thread pool. Once the thread pool is blocked, we throw up the old task.
|
||||
*/
|
||||
private final ExecutorService pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<Runnable>(1), new NamedThreadFactory("sentinel-nacos-ds-update", true),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private final Listener configListener;
|
||||
private final String groupId;
|
||||
private final String dataId;
|
||||
private final Properties properties;
|
||||
|
||||
/**
|
||||
* Note: The Nacos config might be null if its initialization failed.
|
||||
*/
|
||||
private ConfigService configService = null;
|
||||
|
||||
/**
|
||||
* Constructs an read-only DataSource with Nacos backend.
|
||||
*
|
||||
* @param serverAddr server address of Nacos, cannot be empty
|
||||
* @param groupId group ID, cannot be empty
|
||||
* @param dataId data ID, cannot be empty
|
||||
* @param parser customized data parser, cannot be empty
|
||||
*/
|
||||
public NacosDataSource(final String serverAddr, final String groupId, final String dataId,
|
||||
Converter<String, T> parser) {
|
||||
this(NacosDataSource.buildProperties(serverAddr), groupId, dataId, parser);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param properties properties for construct {@link ConfigService} using {@link NacosFactory#createConfigService(Properties)}
|
||||
* @param groupId group ID, cannot be empty
|
||||
* @param dataId data ID, cannot be empty
|
||||
* @param parser customized data parser, cannot be empty
|
||||
*/
|
||||
public NacosDataSource(final Properties properties, final String groupId, final String dataId,
|
||||
Converter<String, T> parser) {
|
||||
super(parser);
|
||||
if (StringUtil.isBlank(groupId) || StringUtil.isBlank(dataId)) {
|
||||
throw new IllegalArgumentException(String.format("Bad argument: groupId=[%s], dataId=[%s]",
|
||||
groupId, dataId));
|
||||
}
|
||||
AssertUtil.notNull(properties, "Nacos properties must not be null, you could put some keys from PropertyKeyConst");
|
||||
this.groupId = groupId;
|
||||
this.dataId = dataId;
|
||||
this.properties = properties;
|
||||
this.configListener = new Listener() {
|
||||
@Override
|
||||
public Executor getExecutor() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveConfigInfo(final String configInfo) {
|
||||
RecordLog.info("[NacosDataSource] New property value received for (properties: {}) (dataId: {}, groupId: {}): {}",
|
||||
properties, dataId, groupId, configInfo);
|
||||
T newValue = NacosDataSource.this.parser.convert(configInfo);
|
||||
// Update the new value to the property.
|
||||
getProperty().updateValue(newValue);
|
||||
}
|
||||
};
|
||||
initNacosListener();
|
||||
loadInitialConfig();
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn("[NacosDataSource] WARN: initial config is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[NacosDataSource] Error when loading initial config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initNacosListener() {
|
||||
try {
|
||||
this.configService = NacosFactory.createConfigService(this.properties);
|
||||
// Add config listener.
|
||||
configService.addListener(dataId, groupId, configListener);
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[NacosDataSource] Error occurred when initializing Nacos data source", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
if (configService == null) {
|
||||
throw new IllegalStateException("Nacos config service has not been initialized or error occurred");
|
||||
}
|
||||
return configService.getConfig(dataId, groupId, DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (configService != null) {
|
||||
configService.removeListener(dataId, groupId, configListener);
|
||||
}
|
||||
pool.shutdownNow();
|
||||
}
|
||||
|
||||
private static Properties buildProperties(String serverAddr) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty(PropertyKeyConst.SERVER_ADDR, serverAddr);
|
||||
return properties;
|
||||
}
|
||||
}
|
107
sentinel/sentinel-extension/sentinel-datasource-redis/README.md
Normal file
107
sentinel/sentinel-extension/sentinel-datasource-redis/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Sentinel DataSource Redis
|
||||
|
||||
Sentinel DataSource Redis provides integration with Redis. The data source leverages Redis pub-sub feature to implement push model (listener).
|
||||
|
||||
The data source uses [Lettuce](https://lettuce.io/) as the Redis client, which requires JDK 1.8 or later.
|
||||
|
||||
> **NOTE**: Currently we do not support Redis Cluster now.
|
||||
|
||||
## Usage
|
||||
|
||||
To use Sentinel DataSource Redis, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-redis</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
Then you can create an `RedisDataSource` and register to rule managers.
|
||||
For instance:
|
||||
|
||||
```java
|
||||
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<List<FlowRule>>(redisConnectionConfig, ruleKey, channel, flowConfigParser);
|
||||
FlowRuleManager.register2Property(redisDataSource.getProperty());
|
||||
```
|
||||
|
||||
- `redisConnectionConfig`: use `RedisConnectionConfig` class to build your Redis connection config
|
||||
- `ruleKey`: the rule persistence key of a Redis String
|
||||
- `channel`: the channel to subscribe
|
||||
|
||||
You can also create multi data sources to subscribe for different rule type.
|
||||
|
||||
Note that the data source first loads initial rules from a Redis String (provided `ruleKey`) during initialization.
|
||||
So for consistency, users should publish the value and save the value to the `ruleKey` simultaneously like this (using Redis transaction):
|
||||
|
||||
```
|
||||
MULTI
|
||||
SET ruleKey value
|
||||
PUBLISH channel value
|
||||
EXEC
|
||||
```
|
||||
|
||||
An example using Lettuce Redis client:
|
||||
|
||||
```java
|
||||
public <T> void pushRules(List<T> rules, Converter<List<T>, String> encoder) {
|
||||
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub();
|
||||
RedisPubSubCommands<String, String> subCommands = connection.sync();
|
||||
String value = encoder.convert(rules);
|
||||
subCommands.multi();
|
||||
subCommands.set(ruleKey, value);
|
||||
subCommands.publish(ruleChannel, value);
|
||||
subCommands.exec();
|
||||
}
|
||||
```
|
||||
|
||||
Transaction can be handled in Redis Cluster when just using the same key.
|
||||
|
||||
An example using Lettuce Redis Cluster client:
|
||||
|
||||
```java
|
||||
public <T> void pushRules(List<T> rules, Converter<List<T>, String> encoder) {
|
||||
RedisAdvancedClusterCommands<String, String> subCommands = client.connect().sync();
|
||||
int slot = SlotHash.getSlot(ruleKey);
|
||||
NodeSelection<String, String> nodes = subCommands.nodes((n)->n.hasSlot(slot));
|
||||
RedisCommands<String, String> commands = nodes.commands(0);
|
||||
String value = encoder.convert(rules);
|
||||
commands.multi();
|
||||
commands.set(ruleKey, value);
|
||||
commands.publish(channel, value);
|
||||
commands.exec();
|
||||
}
|
||||
```
|
||||
|
||||
## How to build RedisConnectionConfig
|
||||
|
||||
### Build with Redis standalone mode
|
||||
|
||||
```java
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withHost("localhost")
|
||||
.withPort(6379)
|
||||
.withPassword("pwd")
|
||||
.withDataBase(2)
|
||||
.build();
|
||||
|
||||
```
|
||||
|
||||
### Build with Redis Sentinel mode
|
||||
|
||||
```java
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withRedisSentinel("redisSentinelServer1",5000)
|
||||
.withRedisSentinel("redisSentinelServer2",5001)
|
||||
.withRedisSentinelMasterId("redisSentinelMasterId").build();
|
||||
```
|
||||
|
||||
### Build with Redis Cluster mode
|
||||
|
||||
```java
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withRedisCluster("redisSentinelServer1",5000)
|
||||
.withRedisCluster("redisSentinelServer2",5001).build();
|
||||
```
|
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-redis</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.source.version>1.8</java.source.version>
|
||||
<java.target.version>1.8</java.target.version>
|
||||
<lettuce.version>5.0.1.RELEASE</lettuce.version>
|
||||
<redis.mock.version>0.1.6</redis.mock.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
<version>${lettuce.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.grakn</groupId>
|
||||
<artifactId>redis-mock</artifactId>
|
||||
<version>${redis.mock.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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.datasource.redis;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig;
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import io.lettuce.core.cluster.RedisClusterClient;
|
||||
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection;
|
||||
import io.lettuce.core.pubsub.RedisPubSubAdapter;
|
||||
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
|
||||
import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A read-only {@code DataSource} with Redis backend.
|
||||
* </p>
|
||||
* <p>
|
||||
* The data source first loads initial rules from a Redis String during initialization.
|
||||
* Then the data source subscribe from specific channel. When new rules is published to the channel,
|
||||
* the data source will observe the change in realtime and update to memory.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that for consistency, users should publish the value and save the value to the ruleKey simultaneously
|
||||
* like this (using Redis transaction):
|
||||
* <pre>
|
||||
* MULTI
|
||||
* SET ruleKey value
|
||||
* PUBLISH channel value
|
||||
* EXEC
|
||||
* </pre>
|
||||
* </p>
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
public class RedisDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private final RedisClient redisClient;
|
||||
|
||||
private final RedisClusterClient redisClusterClient;
|
||||
|
||||
private final String ruleKey;
|
||||
|
||||
/**
|
||||
* Constructor of {@code RedisDataSource}.
|
||||
*
|
||||
* @param connectionConfig Redis connection config
|
||||
* @param ruleKey data key in Redis
|
||||
* @param channel channel to subscribe in Redis
|
||||
* @param parser customized data parser, cannot be empty
|
||||
*/
|
||||
public RedisDataSource(RedisConnectionConfig connectionConfig, String ruleKey, String channel,
|
||||
Converter<String, T> parser) {
|
||||
super(parser);
|
||||
AssertUtil.notNull(connectionConfig, "Redis connection config can not be null");
|
||||
AssertUtil.notEmpty(ruleKey, "Redis ruleKey can not be empty");
|
||||
AssertUtil.notEmpty(channel, "Redis subscribe channel can not be empty");
|
||||
if (connectionConfig.getRedisClusters().size() == 0) {
|
||||
this.redisClient = getRedisClient(connectionConfig);
|
||||
this.redisClusterClient = null;
|
||||
} else {
|
||||
this.redisClusterClient = getRedisClusterClient(connectionConfig);
|
||||
this.redisClient = null;
|
||||
}
|
||||
this.ruleKey = ruleKey;
|
||||
loadInitialConfig();
|
||||
subscribeFromChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Redis client fromm {@code RedisConnectionConfig}.
|
||||
*
|
||||
* @return a new {@link RedisClient}
|
||||
*/
|
||||
private RedisClient getRedisClient(RedisConnectionConfig connectionConfig) {
|
||||
if (connectionConfig.getRedisSentinels().size() == 0) {
|
||||
RecordLog.info("[RedisDataSource] Creating stand-alone mode Redis client");
|
||||
return getRedisStandaloneClient(connectionConfig);
|
||||
} else {
|
||||
RecordLog.info("[RedisDataSource] Creating Redis Sentinel mode Redis client");
|
||||
return getRedisSentinelClient(connectionConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private RedisClusterClient getRedisClusterClient(RedisConnectionConfig connectionConfig) {
|
||||
char[] password = connectionConfig.getPassword();
|
||||
String clientName = connectionConfig.getClientName();
|
||||
|
||||
//If any uri is successful for connection, the others are not tried anymore
|
||||
List<RedisURI> redisUris = new ArrayList<>();
|
||||
for (RedisConnectionConfig config : connectionConfig.getRedisClusters()) {
|
||||
RedisURI.Builder clusterRedisUriBuilder = RedisURI.builder();
|
||||
clusterRedisUriBuilder.withHost(config.getHost())
|
||||
.withPort(config.getPort())
|
||||
.withTimeout(Duration.ofMillis(connectionConfig.getTimeout()));
|
||||
//All redis nodes must have same password
|
||||
if (password != null) {
|
||||
clusterRedisUriBuilder.withPassword(connectionConfig.getPassword());
|
||||
}
|
||||
redisUris.add(clusterRedisUriBuilder.build());
|
||||
}
|
||||
return RedisClusterClient.create(redisUris);
|
||||
}
|
||||
|
||||
private RedisClient getRedisStandaloneClient(RedisConnectionConfig connectionConfig) {
|
||||
char[] password = connectionConfig.getPassword();
|
||||
String clientName = connectionConfig.getClientName();
|
||||
RedisURI.Builder redisUriBuilder = RedisURI.builder();
|
||||
redisUriBuilder.withHost(connectionConfig.getHost())
|
||||
.withPort(connectionConfig.getPort())
|
||||
.withDatabase(connectionConfig.getDatabase())
|
||||
.withTimeout(Duration.ofMillis(connectionConfig.getTimeout()));
|
||||
if (password != null) {
|
||||
redisUriBuilder.withPassword(connectionConfig.getPassword());
|
||||
}
|
||||
if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
|
||||
redisUriBuilder.withClientName(clientName);
|
||||
}
|
||||
return RedisClient.create(redisUriBuilder.build());
|
||||
}
|
||||
|
||||
private RedisClient getRedisSentinelClient(RedisConnectionConfig connectionConfig) {
|
||||
char[] password = connectionConfig.getPassword();
|
||||
String clientName = connectionConfig.getClientName();
|
||||
RedisURI.Builder sentinelRedisUriBuilder = RedisURI.builder();
|
||||
for (RedisConnectionConfig config : connectionConfig.getRedisSentinels()) {
|
||||
sentinelRedisUriBuilder.withSentinel(config.getHost(), config.getPort());
|
||||
}
|
||||
if (password != null) {
|
||||
sentinelRedisUriBuilder.withPassword(connectionConfig.getPassword());
|
||||
}
|
||||
if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
|
||||
sentinelRedisUriBuilder.withClientName(clientName);
|
||||
}
|
||||
sentinelRedisUriBuilder.withSentinelMasterId(connectionConfig.getRedisSentinelMasterId())
|
||||
.withTimeout(connectionConfig.getTimeout(), TimeUnit.MILLISECONDS);
|
||||
return RedisClient.create(sentinelRedisUriBuilder.build());
|
||||
}
|
||||
|
||||
private void subscribeFromChannel(String channel) {
|
||||
RedisPubSubAdapter<String, String> adapterListener = new DelegatingRedisPubSubListener();
|
||||
if (redisClient != null) {
|
||||
StatefulRedisPubSubConnection<String, String> pubSubConnection = redisClient.connectPubSub();
|
||||
pubSubConnection.addListener(adapterListener);
|
||||
RedisPubSubCommands<String, String> sync = pubSubConnection.sync();
|
||||
sync.subscribe(channel);
|
||||
} else {
|
||||
StatefulRedisClusterPubSubConnection<String, String> pubSubConnection = redisClusterClient.connectPubSub();
|
||||
pubSubConnection.addListener(adapterListener);
|
||||
RedisPubSubCommands<String, String> sync = pubSubConnection.sync();
|
||||
sync.subscribe(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn("[RedisDataSource] WARN: initial config is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[RedisDataSource] Error when loading initial config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() {
|
||||
if (this.redisClient == null && this.redisClusterClient == null) {
|
||||
throw new IllegalStateException("Redis client or Redis Cluster client has not been initialized or error occurred");
|
||||
}
|
||||
|
||||
if (redisClient != null) {
|
||||
RedisCommands<String, String> stringRedisCommands = redisClient.connect().sync();
|
||||
return stringRedisCommands.get(ruleKey);
|
||||
} else {
|
||||
RedisAdvancedClusterCommands<String, String> stringRedisCommands = redisClusterClient.connect().sync();
|
||||
return stringRedisCommands.get(ruleKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (redisClient != null) {
|
||||
redisClient.shutdown();
|
||||
} else {
|
||||
redisClusterClient.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class DelegatingRedisPubSubListener extends RedisPubSubAdapter<String, String> {
|
||||
|
||||
DelegatingRedisPubSubListener() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void message(String channel, String message) {
|
||||
RecordLog.info("[RedisDataSource] New property value received for channel {}: {}", channel, message);
|
||||
getProperty().updateValue(parser.convert(message));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
* 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.datasource.redis.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This class provide a builder to build redis client connection config.
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
public class RedisConnectionConfig {
|
||||
|
||||
/**
|
||||
* The default redisSentinel port.
|
||||
*/
|
||||
public static final int DEFAULT_SENTINEL_PORT = 26379;
|
||||
|
||||
/**
|
||||
* The default redisCluster port.
|
||||
*/
|
||||
public static final int DEFAULT_CLUSTER_PORT = 6379;
|
||||
|
||||
/**
|
||||
* The default redis port.
|
||||
*/
|
||||
public static final int DEFAULT_REDIS_PORT = 6379;
|
||||
|
||||
/**
|
||||
* Default timeout: 60 sec
|
||||
*/
|
||||
public static final long DEFAULT_TIMEOUT_MILLISECONDS = 60 * 1000;
|
||||
|
||||
private String host;
|
||||
private String redisSentinelMasterId;
|
||||
private int port;
|
||||
private int database;
|
||||
private String clientName;
|
||||
private char[] password;
|
||||
private long timeout = DEFAULT_TIMEOUT_MILLISECONDS;
|
||||
private final List<RedisConnectionConfig> redisSentinels = new ArrayList<RedisConnectionConfig>();
|
||||
private final List<RedisConnectionConfig> redisClusters = new ArrayList<RedisConnectionConfig>();
|
||||
|
||||
/**
|
||||
* Default empty constructor.
|
||||
*/
|
||||
public RedisConnectionConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with host/port and timeout.
|
||||
*
|
||||
* @param host the host
|
||||
* @param port the port
|
||||
* @param timeout timeout value . unit is mill seconds
|
||||
*/
|
||||
public RedisConnectionConfig(String host, int port, long timeout) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.notNull(timeout, "Timeout duration must not be null");
|
||||
AssertUtil.isTrue(timeout >= 0, "Timeout duration must be greater or equal to zero");
|
||||
|
||||
setHost(host);
|
||||
setPort(port);
|
||||
setTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link RedisConnectionConfig.Builder} to construct a {@link RedisConnectionConfig}.
|
||||
*
|
||||
* @return a new {@link RedisConnectionConfig.Builder} to construct a {@link RedisConnectionConfig}.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder builder() {
|
||||
return new RedisConnectionConfig.Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the host.
|
||||
*
|
||||
* @return the host.
|
||||
*/
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Redis host.
|
||||
*
|
||||
* @param host the host
|
||||
*/
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Sentinel Master Id.
|
||||
*
|
||||
* @return the Sentinel Master Id.
|
||||
*/
|
||||
public String getRedisSentinelMasterId() {
|
||||
return redisSentinelMasterId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Sentinel Master Id.
|
||||
*
|
||||
* @param redisSentinelMasterId the Sentinel Master Id.
|
||||
*/
|
||||
public void setRedisSentinelMasterId(String redisSentinelMasterId) {
|
||||
this.redisSentinelMasterId = redisSentinelMasterId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Redis port.
|
||||
*
|
||||
* @return the Redis port
|
||||
*/
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Redis port. Defaults to {@link #DEFAULT_REDIS_PORT}.
|
||||
*
|
||||
* @param port the Redis port
|
||||
*/
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password.
|
||||
*
|
||||
* @return the password
|
||||
*/
|
||||
public char[] getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password. Use empty string to skip authentication.
|
||||
*
|
||||
* @param password the password, must not be {@literal null}.
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
|
||||
AssertUtil.notNull(password, "Password must not be null");
|
||||
this.password = password.toCharArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password. Use empty char array to skip authentication.
|
||||
*
|
||||
* @param password the password, must not be {@literal null}.
|
||||
*/
|
||||
public void setPassword(char[] password) {
|
||||
|
||||
AssertUtil.notNull(password, "Password must not be null");
|
||||
this.password = Arrays.copyOf(password, password.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command timeout for synchronous command execution.
|
||||
*
|
||||
* @return the Timeout
|
||||
*/
|
||||
public long getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the command timeout for synchronous command execution.
|
||||
*
|
||||
* @param timeout the command timeout for synchronous command execution.
|
||||
*/
|
||||
public void setTimeout(Long timeout) {
|
||||
|
||||
AssertUtil.notNull(timeout, "Timeout must not be null");
|
||||
AssertUtil.isTrue(timeout >= 0, "Timeout must be greater or equal 0");
|
||||
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Redis database number. Databases are only available for Redis Standalone and Redis Master/Slave.
|
||||
*
|
||||
* @return database
|
||||
*/
|
||||
public int getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Redis database number. Databases are only available for Redis Standalone and Redis Master/Slave.
|
||||
*
|
||||
* @param database the Redis database number.
|
||||
*/
|
||||
public void setDatabase(int database) {
|
||||
|
||||
AssertUtil.isTrue(database >= 0, "Invalid database number: " + database);
|
||||
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client name.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getClientName() {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client name to be applied on Redis connections.
|
||||
*
|
||||
* @param clientName the client name.
|
||||
*/
|
||||
public void setClientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of {@link RedisConnectionConfig Redis Sentinel URIs}.
|
||||
*/
|
||||
public List<RedisConnectionConfig> getRedisSentinels() {
|
||||
return redisSentinels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of {@link RedisConnectionConfig Redis Cluster URIs}.
|
||||
*/
|
||||
public List<RedisConnectionConfig> getRedisClusters() {
|
||||
return redisClusters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(getClass().getSimpleName());
|
||||
|
||||
sb.append(" [");
|
||||
|
||||
if (host != null) {
|
||||
sb.append("host='").append(host).append('\'');
|
||||
sb.append(", port=").append(port);
|
||||
}
|
||||
if (redisSentinelMasterId != null) {
|
||||
sb.append("redisSentinels=").append(getRedisSentinels());
|
||||
sb.append(", redisSentinelMasterId=").append(redisSentinelMasterId);
|
||||
}
|
||||
|
||||
if (redisClusters.size() > 0) {
|
||||
sb.append("redisClusters=").append(getRedisClusters());
|
||||
}
|
||||
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof RedisConnectionConfig)) {
|
||||
return false;
|
||||
}
|
||||
RedisConnectionConfig redisURI = (RedisConnectionConfig)o;
|
||||
|
||||
if (port != redisURI.port) {
|
||||
return false;
|
||||
}
|
||||
if (database != redisURI.database) {
|
||||
return false;
|
||||
}
|
||||
if (host != null ? !host.equals(redisURI.host) : redisURI.host != null) {
|
||||
return false;
|
||||
}
|
||||
if (redisSentinelMasterId != null ? !redisSentinelMasterId.equals(redisURI.redisSentinelMasterId)
|
||||
: redisURI.redisSentinelMasterId != null) {
|
||||
return false;
|
||||
}
|
||||
if (redisClusters != null ? !redisClusters.equals(redisURI.redisClusters)
|
||||
: redisURI.redisClusters != null) {
|
||||
return false;
|
||||
}
|
||||
return !(redisSentinels != null ? !redisSentinels.equals(redisURI.redisSentinels)
|
||||
: redisURI.redisSentinels != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = host != null ? host.hashCode() : 0;
|
||||
result = 31 * result + (redisSentinelMasterId != null ? redisSentinelMasterId.hashCode() : 0);
|
||||
result = 31 * result + port;
|
||||
result = 31 * result + database;
|
||||
result = 31 * result + (redisSentinels != null ? redisSentinels.hashCode() : 0);
|
||||
result = 31 * result + (redisClusters != null ? redisClusters.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for Redis RedisConnectionConfig.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private String host;
|
||||
private String redisSentinelMasterId;
|
||||
private int port;
|
||||
private int database;
|
||||
private String clientName;
|
||||
private char[] password;
|
||||
private long timeout = DEFAULT_TIMEOUT_MILLISECONDS;
|
||||
private final List<RedisHostAndPort> redisSentinels = new ArrayList<RedisHostAndPort>();
|
||||
private final List<RedisHostAndPort> redisClusters = new ArrayList<RedisHostAndPort>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Redis host. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @return New builder with Redis host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redis(String host) {
|
||||
return redis(host, DEFAULT_REDIS_PORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Redis host and port. Creates a new builder
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @return New builder with Redis host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redis(String host, int port) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withHost(host).withPort(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Sentinel host. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @return New builder with Sentinel host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisSentinel(String host) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
|
||||
RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withRedisSentinel(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Sentinel host and port. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @return New builder with Sentinel host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisSentinel(String host, int port) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withRedisSentinel(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Sentinel host and master id. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param masterId redisSentinel master id
|
||||
* @return New builder with Sentinel host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisSentinel(String host, String masterId) {
|
||||
return redisSentinel(host, DEFAULT_SENTINEL_PORT, masterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Sentinel host, port and master id. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @param masterId redisSentinel master id
|
||||
* @return New builder with Sentinel host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisSentinel(String host, int port, String masterId) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withSentinelMasterId(masterId).withRedisSentinel(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a withRedisSentinel host to the existing builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withRedisSentinel(String host) {
|
||||
return withRedisSentinel(host, DEFAULT_SENTINEL_PORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a withRedisSentinel host/port to the existing builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withRedisSentinel(String host, int port) {
|
||||
|
||||
AssertUtil.assertState(this.host == null, "Cannot use with Redis mode.");
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
redisSentinels.add(RedisHostAndPort.of(host, port));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Cluster host. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @return New builder with Cluster host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisCluster(String host) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
|
||||
RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withRedisCluster(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Cluster host and port. Creates a new builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @return New builder with Cluster host/port.
|
||||
*/
|
||||
public static RedisConnectionConfig.Builder redisCluster(String host, int port) {
|
||||
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder();
|
||||
return builder.withRedisCluster(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a withRedisCluster host to the existing builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withRedisCluster(String host) {
|
||||
return withRedisCluster(host, DEFAULT_CLUSTER_PORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a withRedisCluster host/port to the existing builder.
|
||||
*
|
||||
* @param host the host name
|
||||
* @param port the port
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withRedisCluster(String host, int port) {
|
||||
|
||||
AssertUtil.assertState(this.host == null, "Cannot use with Redis mode.");
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
redisClusters.add(RedisHostAndPort.of(host, port));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds host information to the builder. Does only affect Redis URI, cannot be used with Sentinel connections.
|
||||
*
|
||||
* @param host the port
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withHost(String host) {
|
||||
|
||||
AssertUtil.assertState(this.redisSentinels.isEmpty(),
|
||||
"Sentinels are non-empty. Cannot use in Sentinel mode.");
|
||||
AssertUtil.notEmpty(host, "Host must not be empty");
|
||||
|
||||
this.host = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds port information to the builder. Does only affect Redis URI, cannot be used with Sentinel connections.
|
||||
*
|
||||
* @param port the port
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withPort(int port) {
|
||||
|
||||
AssertUtil.assertState(this.host != null, "Host is null. Cannot use in Sentinel mode.");
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
|
||||
this.port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the database number.
|
||||
*
|
||||
* @param database the database number
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withDatabase(int database) {
|
||||
|
||||
AssertUtil.isTrue(database >= 0, "Invalid database number: " + database);
|
||||
|
||||
this.database = database;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a client name.
|
||||
*
|
||||
* @param clientName the client name
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withClientName(String clientName) {
|
||||
|
||||
AssertUtil.notNull(clientName, "Client name must not be null");
|
||||
|
||||
this.clientName = clientName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures authentication.
|
||||
*
|
||||
* @param password the password
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withPassword(String password) {
|
||||
|
||||
AssertUtil.notNull(password, "Password must not be null");
|
||||
|
||||
return withPassword(password.toCharArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures authentication.
|
||||
*
|
||||
* @param password the password
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withPassword(char[] password) {
|
||||
|
||||
AssertUtil.notNull(password, "Password must not be null");
|
||||
|
||||
this.password = Arrays.copyOf(password, password.length);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a timeout.
|
||||
*
|
||||
* @param timeout must not be {@literal null} or negative.
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withTimeout(long timeout) {
|
||||
|
||||
AssertUtil.notNull(timeout, "Timeout must not be null");
|
||||
AssertUtil.notNull(timeout >= 0, "Timeout must be greater or equal 0");
|
||||
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a redisSentinel master Id.
|
||||
*
|
||||
* @param sentinelMasterId redisSentinel master id, must not be empty or {@literal null}
|
||||
* @return the builder
|
||||
*/
|
||||
public RedisConnectionConfig.Builder withSentinelMasterId(String sentinelMasterId) {
|
||||
|
||||
AssertUtil.notEmpty(sentinelMasterId, "Sentinel master id must not empty");
|
||||
|
||||
this.redisSentinelMasterId = sentinelMasterId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the RedisConnectionConfig.
|
||||
*/
|
||||
public RedisConnectionConfig build() {
|
||||
|
||||
if (redisSentinels.isEmpty() && redisClusters.isEmpty() && StringUtil.isEmpty(host)) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot build a RedisConnectionConfig. One of the following must be provided Host, Socket, Cluster or "
|
||||
+ "Sentinel");
|
||||
}
|
||||
|
||||
RedisConnectionConfig redisURI = new RedisConnectionConfig();
|
||||
redisURI.setHost(host);
|
||||
redisURI.setPort(port);
|
||||
|
||||
if (password != null) {
|
||||
redisURI.setPassword(password);
|
||||
}
|
||||
|
||||
redisURI.setDatabase(database);
|
||||
redisURI.setClientName(clientName);
|
||||
|
||||
redisURI.setRedisSentinelMasterId(redisSentinelMasterId);
|
||||
|
||||
for (RedisHostAndPort sentinel : redisSentinels) {
|
||||
redisURI.getRedisSentinels().add(
|
||||
new RedisConnectionConfig(sentinel.getHost(), sentinel.getPort(), timeout));
|
||||
}
|
||||
|
||||
for (RedisHostAndPort sentinel : redisClusters) {
|
||||
redisURI.getRedisClusters().add(
|
||||
new RedisConnectionConfig(sentinel.getHost(), sentinel.getPort(), timeout));
|
||||
}
|
||||
|
||||
redisURI.setTimeout(timeout);
|
||||
|
||||
return redisURI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true for valid port numbers.
|
||||
*/
|
||||
private static boolean isValidPort(int port) {
|
||||
return port >= 0 && port <= 65535;
|
||||
}
|
||||
}
|
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.datasource.redis.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
|
||||
/**
|
||||
* An immutable representation of a host and port.
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
public class RedisHostAndPort {
|
||||
|
||||
private static final int NO_PORT = -1;
|
||||
|
||||
public final String host;
|
||||
public final int port;
|
||||
|
||||
/**
|
||||
* @param host must not be empty or {@literal null}.
|
||||
* @param port
|
||||
*/
|
||||
private RedisHostAndPort(String host, int port) {
|
||||
AssertUtil.notNull(host, "host must not be null");
|
||||
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RedisHostAndPort} of {@code host} and {@code port}
|
||||
*
|
||||
* @param host the hostname
|
||||
* @param port a valid port
|
||||
* @return the {@link RedisHostAndPort} of {@code host} and {@code port}
|
||||
*/
|
||||
public static RedisHostAndPort of(String host, int port) {
|
||||
AssertUtil.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
|
||||
return new RedisHostAndPort(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if has a port.
|
||||
*/
|
||||
public boolean hasPort() {
|
||||
return port != NO_PORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the host text.
|
||||
*/
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the port.
|
||||
*/
|
||||
public int getPort() {
|
||||
if (!hasPort()) {
|
||||
throw new IllegalStateException("No port present.");
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof RedisHostAndPort)) {
|
||||
return false;
|
||||
}
|
||||
RedisHostAndPort that = (RedisHostAndPort)o;
|
||||
return port == that.port && (host != null ? host.equals(that.host) : that.host == null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = host != null ? host.hashCode() : 0;
|
||||
result = 31 * result + port;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param port the port number
|
||||
* @return {@literal true} for valid port numbers.
|
||||
*/
|
||||
private static boolean isValidPort(int port) {
|
||||
return port >= 0 && port <= 65535;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(host);
|
||||
if (hasPort()) {
|
||||
sb.append(':').append(port);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 1999-2020 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.datasource.redis;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import io.lettuce.core.cluster.RedisClusterClient;
|
||||
import io.lettuce.core.cluster.SlotHash;
|
||||
import io.lettuce.core.cluster.api.sync.NodeSelection;
|
||||
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Redis redisCluster mode test cases for {@link RedisDataSource}.
|
||||
*
|
||||
* @author liqiangz
|
||||
*/
|
||||
@Ignore(value = "Before run this test, you need to set up your Redis Cluster.")
|
||||
public class ClusterModeRedisDataSourceTest {
|
||||
|
||||
private String host = "localhost";
|
||||
|
||||
private int redisSentinelPort = 7000;
|
||||
private final RedisClusterClient client = RedisClusterClient.create(RedisURI.Builder.redis(host, redisSentinelPort).build());
|
||||
private String ruleKey = "sentinel.rules.flow.ruleKey";
|
||||
private String channel = "sentinel.rules.flow.channel";
|
||||
|
||||
@Before
|
||||
public void initData() {
|
||||
Converter<String, List<FlowRule>> flowConfigParser = buildFlowConfigParser();
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withRedisCluster(host, redisSentinelPort).build();
|
||||
initRedisRuleData();
|
||||
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<>(config,
|
||||
ruleKey, channel, flowConfigParser);
|
||||
FlowRuleManager.register2Property(redisDataSource.getProperty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectToSentinelAndPubMsgSuccess() {
|
||||
int maxQueueingTimeMs = new Random().nextInt();
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":" + maxQueueingTimeMs
|
||||
+ ", \"controller\":null}]";
|
||||
RedisAdvancedClusterCommands<String, String> subCommands = client.connect().sync();
|
||||
int slot = SlotHash.getSlot(ruleKey);
|
||||
NodeSelection<String, String> nodes = subCommands.nodes((n) -> n.hasSlot(slot));
|
||||
RedisCommands<String, String> commands = nodes.commands(0);
|
||||
commands.multi();
|
||||
commands.set(ruleKey, flowRulesJson);
|
||||
commands.publish(channel, flowRulesJson);
|
||||
commands.exec();
|
||||
|
||||
await().timeout(2, TimeUnit.SECONDS)
|
||||
.until(new Callable<List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> call() throws Exception {
|
||||
return FlowRuleManager.getRules();
|
||||
}
|
||||
}, Matchers.hasSize(1));
|
||||
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(rules.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
String value = subCommands.get(ruleKey);
|
||||
List<FlowRule> flowRulesValuesInRedis = buildFlowConfigParser().convert(value);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.size(), 1);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
}
|
||||
|
||||
@After
|
||||
public void clearResource() {
|
||||
RedisAdvancedClusterCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
stringRedisCommands.del(ruleKey);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
private Converter<String, List<FlowRule>> buildFlowConfigParser() {
|
||||
return source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
|
||||
private void initRedisRuleData() {
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":500, \"controller\":null}]";
|
||||
RedisAdvancedClusterCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
String ok = stringRedisCommands.set(ruleKey, flowRulesJson);
|
||||
Assert.assertEquals("OK", ok);
|
||||
}
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.datasource.redis;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test cases for {@link RedisConnectionConfig}.
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
public class RedisConnectionConfigTest {
|
||||
|
||||
@Test
|
||||
public void testRedisDefaultPropertySuccess() {
|
||||
String host = "localhost";
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redis(host).build();
|
||||
Assert.assertEquals(host, redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(RedisConnectionConfig.DEFAULT_REDIS_PORT, redisConnectionConfig.getPort());
|
||||
Assert.assertEquals(RedisConnectionConfig.DEFAULT_TIMEOUT_MILLISECONDS, redisConnectionConfig.getTimeout());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisClientNamePropertySuccess() {
|
||||
String host = "localhost";
|
||||
String clientName = "clientName";
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redis(host)
|
||||
.withClientName("clientName")
|
||||
.build();
|
||||
Assert.assertEquals(redisConnectionConfig.getClientName(), clientName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisTimeOutPropertySuccess() {
|
||||
String host = "localhost";
|
||||
long timeout = 70 * 1000;
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redis(host)
|
||||
.withTimeout(timeout)
|
||||
.build();
|
||||
Assert.assertEquals(redisConnectionConfig.getTimeout(), timeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisSentinelDefaultPortSuccess() {
|
||||
String host = "localhost";
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisSentinel(host)
|
||||
.withPassword("211233")
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(1, redisConnectionConfig.getRedisSentinels().size());
|
||||
Assert.assertEquals(RedisConnectionConfig.DEFAULT_SENTINEL_PORT,
|
||||
redisConnectionConfig.getRedisSentinels().get(0).getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisSentinelMoreThanOneServerSuccess() {
|
||||
String host = "localhost";
|
||||
String host2 = "server2";
|
||||
int port2 = 1879;
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisSentinel(host)
|
||||
.withRedisSentinel(host2, port2)
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(2, redisConnectionConfig.getRedisSentinels().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisSentinelMoreThanOneDuplicateServerSuccess() {
|
||||
String host = "localhost";
|
||||
String host2 = "server2";
|
||||
int port2 = 1879;
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisSentinel(host)
|
||||
.withRedisSentinel(host2, port2)
|
||||
.withRedisSentinel(host2, port2)
|
||||
.withPassword("211233")
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(3, redisConnectionConfig.getRedisSentinels().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisClusterDefaultPortSuccess() {
|
||||
String host = "localhost";
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisCluster(host)
|
||||
.withPassword("211233")
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(1, redisConnectionConfig.getRedisClusters().size());
|
||||
Assert.assertEquals(RedisConnectionConfig.DEFAULT_CLUSTER_PORT,
|
||||
redisConnectionConfig.getRedisClusters().get(0).getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisClusterMoreThanOneServerSuccess() {
|
||||
String host = "localhost";
|
||||
String host2 = "server2";
|
||||
int port1 = 1879;
|
||||
int port2 = 1880;
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisCluster(host, port1)
|
||||
.withRedisCluster(host2, port2)
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(2, redisConnectionConfig.getRedisClusters().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedisClusterMoreThanOneDuplicateServerSuccess() {
|
||||
String host = "localhost";
|
||||
String host2 = "server2";
|
||||
int port2 = 1879;
|
||||
RedisConnectionConfig redisConnectionConfig = RedisConnectionConfig.Builder.redisCluster(host)
|
||||
.withRedisCluster(host2, port2)
|
||||
.withRedisCluster(host2, port2)
|
||||
.withPassword("211233")
|
||||
.build();
|
||||
Assert.assertNull(redisConnectionConfig.getHost());
|
||||
Assert.assertEquals(3, redisConnectionConfig.getRedisClusters().size());
|
||||
}
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.datasource.redis;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Redis redisSentinel mode test cases for {@link RedisDataSource}.
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
@Ignore(value = "Before run this test, you need to set up your Redis Sentinel.")
|
||||
public class SentinelModeRedisDataSourceTest {
|
||||
|
||||
private String host = "localhost";
|
||||
|
||||
private int redisSentinelPort = 5000;
|
||||
|
||||
private String redisSentinelMasterId = "myMaster";
|
||||
|
||||
private String ruleKey = "sentinel.rules.flow.ruleKey";
|
||||
private String channel = "sentinel.rules.flow.channel";
|
||||
|
||||
private final RedisClient client = RedisClient.create(RedisURI.Builder.sentinel(host, redisSentinelPort)
|
||||
.withSentinelMasterId(redisSentinelMasterId).build());
|
||||
|
||||
@Before
|
||||
public void initData() {
|
||||
Converter<String, List<FlowRule>> flowConfigParser = buildFlowConfigParser();
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withRedisSentinel(host, redisSentinelPort)
|
||||
.withRedisSentinel(host, redisSentinelPort)
|
||||
.withSentinelMasterId(redisSentinelMasterId).build();
|
||||
initRedisRuleData();
|
||||
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<>(config,
|
||||
ruleKey, channel, flowConfigParser);
|
||||
FlowRuleManager.register2Property(redisDataSource.getProperty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectToSentinelAndPubMsgSuccess() {
|
||||
int maxQueueingTimeMs = new Random().nextInt();
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":" + maxQueueingTimeMs
|
||||
+ ", \"controller\":null}]";
|
||||
RedisCommands<String, String> subCommands = client.connect().sync();
|
||||
subCommands.multi();
|
||||
subCommands.set(ruleKey, flowRulesJson);
|
||||
subCommands.publish(channel, flowRulesJson);
|
||||
subCommands.exec();
|
||||
|
||||
await().timeout(2, TimeUnit.SECONDS)
|
||||
.until(new Callable<List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> call() throws Exception {
|
||||
return FlowRuleManager.getRules();
|
||||
}
|
||||
}, Matchers.hasSize(1));
|
||||
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(rules.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
String value = subCommands.get(ruleKey);
|
||||
List<FlowRule> flowRulesValuesInRedis = buildFlowConfigParser().convert(value);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.size(), 1);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
}
|
||||
|
||||
@After
|
||||
public void clearResource() {
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
stringRedisCommands.del(ruleKey);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
private Converter<String, List<FlowRule>> buildFlowConfigParser() {
|
||||
return source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {});
|
||||
}
|
||||
|
||||
private void initRedisRuleData() {
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":500, \"controller\":null}]";
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
String ok = stringRedisCommands.set(ruleKey, flowRulesJson);
|
||||
Assert.assertEquals("OK", ok);
|
||||
}
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.datasource.redis;
|
||||
|
||||
import ai.grakn.redismock.RedisServer;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
|
||||
import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Redis stand-alone mode test cases for {@link RedisDataSource}.
|
||||
*
|
||||
* @author tiger
|
||||
*/
|
||||
public class StandaloneRedisDataSourceTest {
|
||||
|
||||
private static RedisServer server = null;
|
||||
|
||||
private RedisClient client;
|
||||
|
||||
private String ruleKey = "sentinel.rules.flow.ruleKey";
|
||||
private String channel = "sentinel.rules.flow.channel";
|
||||
|
||||
@Before
|
||||
public void buildResource() {
|
||||
try {
|
||||
// Bind to a random port.
|
||||
server = RedisServer.newRedisServer();
|
||||
server.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Converter<String, List<FlowRule>> flowConfigParser = buildFlowConfigParser();
|
||||
client = RedisClient.create(RedisURI.create(server.getHost(), server.getBindPort()));
|
||||
RedisConnectionConfig config = RedisConnectionConfig.builder()
|
||||
.withHost(server.getHost())
|
||||
.withPort(server.getBindPort())
|
||||
.build();
|
||||
initRedisRuleData();
|
||||
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<List<FlowRule>>(config,
|
||||
ruleKey, channel, flowConfigParser);
|
||||
FlowRuleManager.register2Property(redisDataSource.getProperty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPubMsgAndReceiveSuccess() {
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(1, rules.size());
|
||||
int maxQueueingTimeMs = new Random().nextInt();
|
||||
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub();
|
||||
String flowRules =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":" + maxQueueingTimeMs
|
||||
+ ", \"controller\":null}]";
|
||||
RedisPubSubCommands<String, String> subCommands = connection.sync();
|
||||
subCommands.multi();
|
||||
subCommands.set(ruleKey, flowRules);
|
||||
subCommands.publish(channel, flowRules);
|
||||
subCommands.exec();
|
||||
|
||||
await().timeout(2, TimeUnit.SECONDS)
|
||||
.until(new Callable<List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> call() throws Exception {
|
||||
return FlowRuleManager.getRules();
|
||||
}
|
||||
}, Matchers.hasSize(1));
|
||||
|
||||
rules = FlowRuleManager.getRules();
|
||||
Assert.assertEquals(rules.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
String value = subCommands.get(ruleKey);
|
||||
List<FlowRule> flowRulesValuesInRedis = buildFlowConfigParser().convert(value);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.size(), 1);
|
||||
Assert.assertEquals(flowRulesValuesInRedis.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitAndParseFlowRuleSuccess() {
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
String value = stringRedisCommands.get(ruleKey);
|
||||
List<FlowRule> flowRules = buildFlowConfigParser().convert(value);
|
||||
Assert.assertEquals(flowRules.size(), 1);
|
||||
stringRedisCommands.del(ruleKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadResourceFail() {
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
stringRedisCommands.del(ruleKey);
|
||||
String value = stringRedisCommands.get(ruleKey);
|
||||
Assert.assertNull(value);
|
||||
}
|
||||
|
||||
@After
|
||||
public void clearResource() {
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
stringRedisCommands.del(ruleKey);
|
||||
client.shutdown();
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
|
||||
private Converter<String, List<FlowRule>> buildFlowConfigParser() {
|
||||
return source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {});
|
||||
}
|
||||
|
||||
private void initRedisRuleData() {
|
||||
String flowRulesJson =
|
||||
"[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, "
|
||||
+ "\"refResource\":null, "
|
||||
+
|
||||
"\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":500, \"controller\":null}]";
|
||||
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
|
||||
String ok = stringRedisCommands.set(ruleKey, flowRulesJson);
|
||||
Assert.assertEquals(ok, "OK");
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
# Sentinel DataSource Spring Cloud Config
|
||||
|
||||
Sentinel DataSource Spring Cloud Config provides integration with Spring Cloud Config
|
||||
so that Spring Cloud Config can be the dynamic rule data source of Sentinel.
|
||||
|
||||
To use Sentinel DataSource Spring Cloud Config, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-spring-cloud-config</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Then you can create an `SpringCloudConfigDataSource` and register to rule managers.
|
||||
For instance:
|
||||
|
||||
```Java
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleDs = new SpringCloudConfigDataSource<>(ruleKey, s -> JSON.parseArray(s, FlowRule.class));
|
||||
FlowRuleManager.register2Property(flowRuleDs.getProperty());
|
||||
```
|
||||
|
||||
To notify the client that the remote config has changed, we could bind a git webhook callback with the
|
||||
`com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator.refresh` API.
|
||||
We may refer to the the sample `com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SpringCouldDataSourceTest#refresh` in test cases.
|
||||
|
||||
We offer test cases and demo in the package: `com.alibaba.csp.sentinel.datasource.spring.cloud.config.test`.
|
||||
When you are running test cases, please follow the steps:
|
||||
|
||||
```
|
||||
// First, start the Spring Cloud config server
|
||||
com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer
|
||||
|
||||
// Second, start the Spring Cloud config client
|
||||
com.alibaba.csp.sentinel.datasource.spring.cloud.config.client.ConfigClient
|
||||
|
||||
// Third, run the test cases and demo
|
||||
com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SentinelRuleLocatorTests
|
||||
com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SpringCouldDataSourceTest
|
||||
```
|
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-spring-cloud-config</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<spring.cloud.version>2.0.0.RELEASE</spring.cloud.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-config</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
<version>1.2.4.RELEASE</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-server</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-expression</artifactId>
|
||||
<version>5.1.8.RELEASE</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 1999-2019 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.datasource.spring.cloud.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.cloud.config.client.ConfigClientStateHolder;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.cloud.config.client.ConfigClientProperties.*;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* {@link SentinelRuleLocator} which pulls Sentinel rules from remote server.
|
||||
* It retrieves configurations of spring-cloud-config client configurations from
|
||||
* {@link org.springframework.core.env.Environment}, such as {@code spring.cloud.config.uri=uri},
|
||||
* {@code spring.cloud.config.profile=profile}, and so on.
|
||||
* When rules are pulled successfully, it will be stored to {@link SentinelRuleStorage}.
|
||||
* </p>
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@Order(0)
|
||||
public class SentinelRuleLocator implements PropertySourceLocator {
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
private ConfigClientProperties defaultProperties;
|
||||
private org.springframework.core.env.Environment environment;
|
||||
|
||||
public SentinelRuleLocator(ConfigClientProperties defaultProperties,
|
||||
org.springframework.core.env.Environment environment) {
|
||||
this.defaultProperties = defaultProperties;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for pull data from remote server
|
||||
*
|
||||
* @param environment
|
||||
* @return correct data if success else a empty propertySource or null
|
||||
*/
|
||||
@Override
|
||||
@Retryable(interceptor = "configServerRetryInterceptor")
|
||||
public org.springframework.core.env.PropertySource<?> locate(
|
||||
org.springframework.core.env.Environment environment) {
|
||||
ConfigClientProperties properties = this.defaultProperties.override(environment);
|
||||
CompositePropertySource composite = new CompositePropertySource("configService");
|
||||
RestTemplate restTemplate = this.restTemplate == null
|
||||
? getSecureRestTemplate(properties)
|
||||
: this.restTemplate;
|
||||
Exception error = null;
|
||||
String errorBody = null;
|
||||
try {
|
||||
String[] labels = new String[] {""};
|
||||
if (StringUtils.hasText(properties.getLabel())) {
|
||||
labels = StringUtils
|
||||
.commaDelimitedListToStringArray(properties.getLabel());
|
||||
}
|
||||
String state = ConfigClientStateHolder.getState();
|
||||
// Try all the labels until one works
|
||||
for (String label : labels) {
|
||||
Environment result = getRemoteEnvironment(restTemplate, properties,
|
||||
label.trim(), state);
|
||||
if (result != null) {
|
||||
log(result);
|
||||
// result.getPropertySources() can be null if using xml
|
||||
if (result.getPropertySources() != null) {
|
||||
for (PropertySource source : result.getPropertySources()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>)source
|
||||
.getSource();
|
||||
composite.addPropertySource(
|
||||
new MapPropertySource(source.getName(), map));
|
||||
}
|
||||
}
|
||||
SentinelRuleStorage.setRulesSource(composite);
|
||||
return composite;
|
||||
}
|
||||
}
|
||||
} catch (HttpServerErrorException e) {
|
||||
error = e;
|
||||
if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders().getContentType())) {
|
||||
errorBody = e.getResponseBodyAsString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = e;
|
||||
}
|
||||
if (properties.isFailFast()) {
|
||||
throw new IllegalStateException(
|
||||
"Could not locate PropertySource and the fail fast property is set, failing",
|
||||
error);
|
||||
}
|
||||
RecordLog.warn("Could not locate PropertySource: " + (errorBody == null
|
||||
? error == null ? "label not found" : error.getMessage()
|
||||
: errorBody));
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public org.springframework.core.env.PropertySource<?> refresh() {
|
||||
return locate(environment);
|
||||
}
|
||||
|
||||
private void log(Environment result) {
|
||||
|
||||
RecordLog.info("Located environment: name={}, profiles={}, label={}, version={}, state={}",
|
||||
result.getName(),
|
||||
result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()),
|
||||
result.getLabel(), result.getVersion(), result.getState());
|
||||
|
||||
List<PropertySource> propertySourceList = result.getPropertySources();
|
||||
if (propertySourceList != null) {
|
||||
int propertyCount = 0;
|
||||
for (PropertySource propertySource : propertySourceList) {
|
||||
propertyCount += propertySource.getSource().size();
|
||||
}
|
||||
RecordLog.info("[SentinelRuleLocator] Environment {} has {} property sources with {} properties",
|
||||
result.getName(), result.getPropertySources().size(), propertyCount);
|
||||
}
|
||||
}
|
||||
|
||||
private Environment getRemoteEnvironment(RestTemplate restTemplate,
|
||||
ConfigClientProperties properties, String label, String state) {
|
||||
String path = "/{name}/{profile}";
|
||||
String name = properties.getName();
|
||||
String profile = properties.getProfile();
|
||||
String token = properties.getToken();
|
||||
int noOfUrls = properties.getUri().length;
|
||||
if (noOfUrls > 1) {
|
||||
RecordLog.debug("[SentinelRuleLocator] Multiple Config Server Urls found listed.");
|
||||
}
|
||||
|
||||
RecordLog.info("[SentinelRuleLocator] getRemoteEnvironment, properties={}, label={}, state={}",
|
||||
properties, label, state);
|
||||
|
||||
Object[] args = new String[] {name, profile};
|
||||
if (StringUtils.hasText(label)) {
|
||||
if (label.contains("/")) {
|
||||
label = label.replace("/", "(_)");
|
||||
}
|
||||
args = new String[] {name, profile, label};
|
||||
path = path + "/{label}";
|
||||
}
|
||||
ResponseEntity<Environment> response = null;
|
||||
|
||||
for (int i = 0; i < noOfUrls; i++) {
|
||||
Credentials credentials = properties.getCredentials(i);
|
||||
String uri = credentials.getUri();
|
||||
String username = credentials.getUsername();
|
||||
String password = credentials.getPassword();
|
||||
|
||||
RecordLog.info("[SentinelRuleLocator] Fetching config from server at: {}", uri);
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
addAuthorizationToken(properties, headers, username, password);
|
||||
if (StringUtils.hasText(token)) {
|
||||
headers.add(TOKEN_HEADER, token);
|
||||
}
|
||||
if (StringUtils.hasText(state) && properties.isSendState()) {
|
||||
headers.add(STATE_HEADER, state);
|
||||
}
|
||||
|
||||
final HttpEntity<Void> entity = new HttpEntity<>((Void)null, headers);
|
||||
response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
|
||||
Environment.class, args);
|
||||
} catch (HttpClientErrorException e) {
|
||||
if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
|
||||
throw e;
|
||||
}
|
||||
} catch (ResourceAccessException e) {
|
||||
RecordLog.warn("[SentinelRuleLocator] ConnectTimeoutException on url <{}>."
|
||||
+ " Will be trying the next url if available", uri);
|
||||
if (i == noOfUrls - 1) {
|
||||
throw e;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null || response.getStatusCode() != HttpStatus.OK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Environment result = response.getBody();
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
if (client.getRequestReadTimeout() < 0) {
|
||||
throw new IllegalStateException("Invalid Value for Read Timeout set.");
|
||||
}
|
||||
requestFactory.setReadTimeout(client.getRequestReadTimeout());
|
||||
RestTemplate template = new RestTemplate(requestFactory);
|
||||
Map<String, String> headers = new HashMap<>(client.getHeaders());
|
||||
if (headers.containsKey(AUTHORIZATION)) {
|
||||
// To avoid redundant addition of header
|
||||
headers.remove(AUTHORIZATION);
|
||||
}
|
||||
if (!headers.isEmpty()) {
|
||||
template.setInterceptors(Arrays.<ClientHttpRequestInterceptor>asList(
|
||||
new GenericRequestHeaderInterceptor(headers)));
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private void addAuthorizationToken(ConfigClientProperties configClientProperties,
|
||||
HttpHeaders httpHeaders, String username, String password) {
|
||||
String authorization = configClientProperties.getHeaders().get(AUTHORIZATION);
|
||||
|
||||
if (password != null && authorization != null) {
|
||||
throw new IllegalStateException(
|
||||
"You must set either 'password' or 'authorization'");
|
||||
}
|
||||
|
||||
if (password != null) {
|
||||
byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
|
||||
httpHeaders.add("Authorization", "Basic " + new String(token));
|
||||
} else if (authorization != null) {
|
||||
httpHeaders.add("Authorization", authorization);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setRestTemplate(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
public static class GenericRequestHeaderInterceptor
|
||||
implements ClientHttpRequestInterceptor {
|
||||
|
||||
private final Map<String, String> headers;
|
||||
|
||||
public GenericRequestHeaderInterceptor(Map<String, String> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
request.getHeaders().add(header.getKey(), header.getValue());
|
||||
}
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
|
||||
protected Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config;
|
||||
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
/**
|
||||
* Storage data pull from spring-config-cloud server
|
||||
* And notice ${@link SpringCloudConfigDataSource} update latest values
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public class SentinelRuleStorage {
|
||||
|
||||
public static PropertySource<?> rulesSource;
|
||||
|
||||
public static void setRulesSource(PropertySource<?> source) {
|
||||
rulesSource = source;
|
||||
noticeSpringCloudDataSource();
|
||||
}
|
||||
|
||||
public static String retrieveRule(String ruleKey) {
|
||||
return rulesSource == null ? null : (String) rulesSource.getProperty(ruleKey);
|
||||
}
|
||||
|
||||
private static void noticeSpringCloudDataSource(){
|
||||
SpringCloudConfigDataSource.updateValues();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* <p>A read-only {@code DataSource} with Spring Cloud Config backend.</p>
|
||||
* <p>
|
||||
* It retrieves the Spring Cloud Config data stored in {@link SentinelRuleStorage}.
|
||||
* When the data in the backend has been modified, {@link SentinelRuleStorage} will
|
||||
* invoke {@link SpringCloudConfigDataSource#updateValues()} to update values dynamically.
|
||||
* </p>
|
||||
* <p>
|
||||
* To notify the client that the remote config has changed, users could bind a git
|
||||
* webhook callback with the {@link SentinelRuleLocator#refresh()} API.
|
||||
* </p>
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public class SpringCloudConfigDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private final static Map<SpringCloudConfigDataSource, SpringConfigListener> listeners;
|
||||
|
||||
static {
|
||||
listeners = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
private final String ruleKey;
|
||||
|
||||
public SpringCloudConfigDataSource(final String ruleKey, Converter<String, T> converter) {
|
||||
super(converter);
|
||||
if (StringUtil.isBlank(ruleKey)) {
|
||||
throw new IllegalArgumentException(String.format("Bad argument: ruleKey=[%s]", ruleKey));
|
||||
}
|
||||
|
||||
this.ruleKey = ruleKey;
|
||||
loadInitialConfig();
|
||||
initListener();
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn("[SpringCloudConfigDataSource] WARN: initial application is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[SpringCloudConfigDataSource] Error when loading initial application", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initListener() {
|
||||
listeners.put(this, new SpringConfigListener(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() {
|
||||
return SentinelRuleStorage.retrieveRule(ruleKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
listeners.remove(this);
|
||||
}
|
||||
|
||||
public static void updateValues() {
|
||||
for (SpringConfigListener listener : listeners.values()) {
|
||||
listener.listenChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpringConfigListener {
|
||||
|
||||
private SpringCloudConfigDataSource dataSource;
|
||||
|
||||
public SpringConfigListener(SpringCloudConfigDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public void listenChanged() {
|
||||
try {
|
||||
Object newValue = dataSource.loadConfig();
|
||||
dataSource.getProperty().updateValue(newValue);
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[SpringConfigListener] load config error: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Define the configuration Loaded when spring application start.
|
||||
* Put it in META-INF/spring.factories, it will be auto loaded by Spring
|
||||
* </p>
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@Configuration
|
||||
public class DataSourceBootstrapConfiguration {
|
||||
|
||||
@Autowired
|
||||
private ConfigurableEnvironment environment;
|
||||
|
||||
@Bean
|
||||
public SentinelRuleLocator sentinelPropertySourceLocator(ConfigClientProperties properties) {
|
||||
SentinelRuleLocator locator = new SentinelRuleLocator(
|
||||
properties, environment);
|
||||
return locator;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
com.alibaba.csp.sentinel.datasource.spring.cloud.config.config.DataSourceBootstrapConfiguration
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2018 the original author or authors.
|
||||
*
|
||||
* 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.datasource.spring.cloud.config;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public abstract class SimpleSpringApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SimpleSpringApplication.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config.client;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
/**
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan("com.alibaba.csp.sentinel.datasource.spring.cloud.config.test")
|
||||
@PropertySource("classpath:config-client-application.properties")
|
||||
public class ConfigClient {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ConfigClient.class);
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config.server;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.config.server.EnableConfigServer;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
/**
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@EnableConfigServer
|
||||
@SpringBootApplication
|
||||
@PropertySource("classpath:config-server-application.properties")
|
||||
public class ConfigServer {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ConfigServer.class);
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2018-2019 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.datasource.spring.cloud.config.test;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleStorage;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.config.DataSourceBootstrapConfiguration;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = DataSourceBootstrapConfiguration.class, properties = {
|
||||
"spring.application.name=sentinel"
|
||||
})
|
||||
public class SentinelRuleLocatorTests {
|
||||
|
||||
|
||||
@Autowired
|
||||
private SentinelRuleLocator sentinelRulesSourceLocator;
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Test
|
||||
public void testAutoLoad() {
|
||||
Assert.assertTrue(sentinelRulesSourceLocator != null);
|
||||
Assert.assertTrue(environment != null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Before run this test case, please start the Config Server ${@link ConfigServer}
|
||||
*/
|
||||
public void testLocate() {
|
||||
ConfigClientProperties configClientProperties = new ConfigClientProperties(environment);
|
||||
configClientProperties.setLabel("master");
|
||||
configClientProperties.setProfile("dev");
|
||||
configClientProperties.setUri(new String[]{"http://localhost:10086/"});
|
||||
SentinelRuleLocator sentinelRulesSourceLocator = new SentinelRuleLocator(configClientProperties, environment);
|
||||
sentinelRulesSourceLocator.locate(environment);
|
||||
Assert.assertTrue(StringUtil.isNotBlank(SentinelRuleStorage.retrieveRule("flow_rule")));
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.datasource.spring.cloud.config.test;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SpringCloudConfigDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.client.ConfigClient;
|
||||
import com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Before test, please start ${@link ConfigServer} and ${@link ConfigClient}
|
||||
*
|
||||
* @author lianglin
|
||||
* @since 1.7.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/test/dataSource/")
|
||||
public class SpringCouldDataSourceTest {
|
||||
|
||||
|
||||
@Autowired
|
||||
private SentinelRuleLocator locator;
|
||||
|
||||
Converter<String, List<FlowRule>> converter = new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseArray(source, FlowRule.class);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@GetMapping("/get")
|
||||
@ResponseBody
|
||||
public List<FlowRule> get() {
|
||||
SpringCloudConfigDataSource dataSource = new SpringCloudConfigDataSource("flow_rule", converter);
|
||||
FlowRuleManager.register2Property(dataSource.getProperty());
|
||||
return FlowRuleManager.getRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* WebHook refresh config
|
||||
*/
|
||||
@GetMapping("/refresh")
|
||||
@ResponseBody
|
||||
public List<FlowRule> refresh() {
|
||||
locator.refresh();
|
||||
return FlowRuleManager.getRules();
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
spring:
|
||||
application:
|
||||
name: sentinel
|
||||
cloud:
|
||||
config:
|
||||
uri: http://localhost:10086/
|
||||
profile: dev
|
||||
label: master
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
spring.application.name=sentinel
|
||||
server.port=8080
|
||||
|
@@ -0,0 +1,4 @@
|
||||
spring.cloud.config.server.git.uri=git@github.com:linlinisme/spring-cloud-config-datasource.git
|
||||
spring.cloud.config.server.git.search-paths=sentinel
|
||||
server.port=10086
|
||||
spring.cloud.config.label=master
|
@@ -0,0 +1,27 @@
|
||||
# Sentinel DataSource ZooKeeper
|
||||
|
||||
Sentinel DataSource ZooKeeper provides integration with ZooKeeper so that ZooKeeper
|
||||
can be the dynamic rule data source of Sentinel. The data source uses push model (listener).
|
||||
|
||||
To use Sentinel DataSource ZooKeeper, you should add the following dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-zookeeper</artifactId>
|
||||
<version>x.y.z</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Then you can create an `ZookeeperDataSource` and register to rule managers.
|
||||
For instance:
|
||||
|
||||
```java
|
||||
// `path` is the data path in ZooKeeper
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(remoteAddress, path, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
|
||||
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
|
||||
```
|
||||
|
||||
> Note: It's not recommended to add a large amount of rules to a single path (has limitation, also leads to bad performance).
|
||||
|
||||
We've also provided an example: [sentinel-demo-zookeeper-datasource](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-zookeeper-datasource).
|
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-datasource-zookeeper</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<zookeeper.version>3.4.14</zookeeper.version>
|
||||
<curator.version>4.0.1</curator.version>
|
||||
<curator.test.version>2.12.0</curator.test.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-extension</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.zookeeper</groupId>
|
||||
<artifactId>zookeeper</artifactId>
|
||||
<version>${zookeeper.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-recipes</artifactId>
|
||||
<version>${curator.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.zookeeper</groupId>
|
||||
<artifactId>zookeeper</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-test</artifactId>
|
||||
<version>${curator.test.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.zookeeper</groupId>
|
||||
<artifactId>zookeeper</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,216 @@
|
||||
package com.alibaba.csp.sentinel.datasource.zookeeper;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||
import org.apache.curator.framework.AuthInfo;
|
||||
import org.apache.curator.framework.CuratorFramework;
|
||||
import org.apache.curator.framework.CuratorFrameworkFactory;
|
||||
import org.apache.curator.framework.recipes.cache.ChildData;
|
||||
import org.apache.curator.framework.recipes.cache.NodeCache;
|
||||
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
|
||||
import org.apache.curator.retry.ExponentialBackoffRetry;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A read-only {@code DataSource} with ZooKeeper backend.
|
||||
*
|
||||
* @author guonanjun
|
||||
*/
|
||||
public class ZookeeperDataSource<T> extends AbstractDataSource<String, T> {
|
||||
|
||||
private static final int RETRY_TIMES = 3;
|
||||
private static final int SLEEP_TIME = 1000;
|
||||
|
||||
private static volatile Map<String, CuratorFramework> zkClientMap = new HashMap<>();
|
||||
private static final Object lock = new Object();
|
||||
|
||||
|
||||
private final ExecutorService pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<Runnable>(1), new NamedThreadFactory("sentinel-zookeeper-ds-update", true),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private NodeCacheListener listener;
|
||||
private final String path;
|
||||
|
||||
private CuratorFramework zkClient = null;
|
||||
private NodeCache nodeCache = null;
|
||||
|
||||
public ZookeeperDataSource(final String serverAddr, final String path, Converter<String, T> parser) {
|
||||
super(parser);
|
||||
if (StringUtil.isBlank(serverAddr) || StringUtil.isBlank(path)) {
|
||||
throw new IllegalArgumentException(String.format("Bad argument: serverAddr=[%s], path=[%s]", serverAddr, path));
|
||||
}
|
||||
this.path = path;
|
||||
|
||||
init(serverAddr, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor is Nacos-style.
|
||||
*/
|
||||
public ZookeeperDataSource(final String serverAddr, final String groupId, final String dataId,
|
||||
Converter<String, T> parser) {
|
||||
super(parser);
|
||||
if (StringUtil.isBlank(serverAddr) || StringUtil.isBlank(groupId) || StringUtil.isBlank(dataId)) {
|
||||
throw new IllegalArgumentException(String.format("Bad argument: serverAddr=[%s], groupId=[%s], dataId=[%s]", serverAddr, groupId, dataId));
|
||||
}
|
||||
this.path = getPath(groupId, dataId);
|
||||
|
||||
init(serverAddr, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor adds authentication information.
|
||||
*/
|
||||
public ZookeeperDataSource(final String serverAddr, final List<AuthInfo> authInfos, final String groupId, final String dataId,
|
||||
Converter<String, T> parser) {
|
||||
super(parser);
|
||||
if (StringUtil.isBlank(serverAddr) || StringUtil.isBlank(groupId) || StringUtil.isBlank(dataId)) {
|
||||
throw new IllegalArgumentException(String.format("Bad argument: serverAddr=[%s], authInfos=[%s], groupId=[%s], dataId=[%s]", serverAddr, authInfos, groupId, dataId));
|
||||
}
|
||||
this.path = getPath(groupId, dataId);
|
||||
|
||||
init(serverAddr, authInfos);
|
||||
}
|
||||
|
||||
private void init(final String serverAddr, final List<AuthInfo> authInfos) {
|
||||
initZookeeperListener(serverAddr, authInfos);
|
||||
loadInitialConfig();
|
||||
}
|
||||
|
||||
private void loadInitialConfig() {
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
if (newValue == null) {
|
||||
RecordLog.warn("[ZookeeperDataSource] WARN: initial config is null, you may have to check your data source");
|
||||
}
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[ZookeeperDataSource] Error when loading initial config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initZookeeperListener(final String serverAddr, final List<AuthInfo> authInfos) {
|
||||
try {
|
||||
|
||||
this.listener = new NodeCacheListener() {
|
||||
@Override
|
||||
public void nodeChanged() {
|
||||
|
||||
try {
|
||||
T newValue = loadConfig();
|
||||
RecordLog.info("[ZookeeperDataSource] New property value received for ({}, {}): {}",
|
||||
serverAddr, path, newValue);
|
||||
// Update the new value to the property.
|
||||
getProperty().updateValue(newValue);
|
||||
} catch (Exception ex) {
|
||||
RecordLog.warn("[ZookeeperDataSource] loadConfig exception", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
String zkKey = getZkKey(serverAddr, authInfos);
|
||||
if (zkClientMap.containsKey(zkKey)) {
|
||||
this.zkClient = zkClientMap.get(zkKey);
|
||||
} else {
|
||||
synchronized (lock) {
|
||||
if (!zkClientMap.containsKey(zkKey)) {
|
||||
CuratorFramework zc = null;
|
||||
if (authInfos == null || authInfos.size() == 0) {
|
||||
zc = CuratorFrameworkFactory.newClient(serverAddr, new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES));
|
||||
} else {
|
||||
zc = CuratorFrameworkFactory.builder().
|
||||
connectString(serverAddr).
|
||||
retryPolicy(new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES)).
|
||||
authorization(authInfos).
|
||||
build();
|
||||
}
|
||||
this.zkClient = zc;
|
||||
this.zkClient.start();
|
||||
Map<String, CuratorFramework> newZkClientMap = new HashMap<>(zkClientMap.size());
|
||||
newZkClientMap.putAll(zkClientMap);
|
||||
newZkClientMap.put(zkKey, zc);
|
||||
zkClientMap = newZkClientMap;
|
||||
} else {
|
||||
this.zkClient = zkClientMap.get(zkKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeCache = new NodeCache(this.zkClient, this.path);
|
||||
this.nodeCache.getListenable().addListener(this.listener, this.pool);
|
||||
this.nodeCache.start();
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[ZookeeperDataSource] Error occurred when initializing Zookeeper data source", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSource() throws Exception {
|
||||
if (this.zkClient == null) {
|
||||
throw new IllegalStateException("Zookeeper has not been initialized or error occurred");
|
||||
}
|
||||
String configInfo = null;
|
||||
ChildData childData = nodeCache.getCurrentData();
|
||||
if (null != childData && childData.getData() != null) {
|
||||
|
||||
configInfo = new String(childData.getData());
|
||||
}
|
||||
return configInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (this.nodeCache != null) {
|
||||
this.nodeCache.getListenable().removeListener(listener);
|
||||
this.nodeCache.close();
|
||||
}
|
||||
if (this.zkClient != null) {
|
||||
this.zkClient.close();
|
||||
}
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
private String getPath(String groupId, String dataId) {
|
||||
return String.format("/%s/%s", groupId, dataId);
|
||||
}
|
||||
|
||||
private String getZkKey(final String serverAddr, final List<AuthInfo> authInfos) {
|
||||
if (authInfos == null || authInfos.size() == 0) {
|
||||
return serverAddr;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(64);
|
||||
builder.append(serverAddr).append(getAuthInfosKey(authInfos));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String getAuthInfosKey(List<AuthInfo> authInfos) {
|
||||
StringBuilder builder = new StringBuilder(32);
|
||||
for (AuthInfo authInfo : authInfos) {
|
||||
if (authInfo == null) {
|
||||
builder.append("{}");
|
||||
} else {
|
||||
builder.append("{" + "sc=" + authInfo.getScheme() + ",au=" + Arrays.toString(authInfo.getAuth()) + "}");
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
protected CuratorFramework getZkClient() {
|
||||
return this.zkClient;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,224 @@
|
||||
package com.alibaba.csp.sentinel.datasource.zookeeper;
|
||||
|
||||
import com.alibaba.csp.sentinel.datasource.Converter;
|
||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
|
||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import org.apache.curator.framework.AuthInfo;
|
||||
import org.apache.curator.framework.CuratorFramework;
|
||||
import org.apache.curator.framework.CuratorFrameworkFactory;
|
||||
import org.apache.curator.retry.ExponentialBackoffRetry;
|
||||
import org.apache.curator.test.TestingServer;
|
||||
import org.apache.zookeeper.CreateMode;
|
||||
import org.apache.zookeeper.ZooDefs;
|
||||
import org.apache.zookeeper.data.ACL;
|
||||
import org.apache.zookeeper.data.Id;
|
||||
import org.apache.zookeeper.data.Stat;
|
||||
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Eric Zhao
|
||||
*/
|
||||
public class ZookeeperDataSourceTest {
|
||||
|
||||
@Test
|
||||
public void testZooKeeperDataSource() throws Exception {
|
||||
TestingServer server = new TestingServer(21812);
|
||||
server.start();
|
||||
|
||||
final String remoteAddress = server.getConnectString();
|
||||
final String path = "/sentinel-zk-ds-demo/flow-HK";
|
||||
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<List<FlowRule>>(remoteAddress, path,
|
||||
new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
|
||||
|
||||
CuratorFramework zkClient = CuratorFrameworkFactory.newClient(remoteAddress,
|
||||
new ExponentialBackoffRetry(3, 1000));
|
||||
zkClient.start();
|
||||
Stat stat = zkClient.checkExists().forPath(path);
|
||||
if (stat == null) {
|
||||
zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
|
||||
}
|
||||
|
||||
final String resourceName = "HK";
|
||||
publishThenTestFor(zkClient, path, resourceName, 10);
|
||||
publishThenTestFor(zkClient, path, resourceName, 15);
|
||||
|
||||
zkClient.close();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testZooKeeperDataSourceAuthorization() throws Exception {
|
||||
TestingServer server = new TestingServer(21812);
|
||||
server.start();
|
||||
|
||||
final String remoteAddress = server.getConnectString();
|
||||
final String groupId = "sentinel-zk-ds-demo";
|
||||
final String dataId = "flow-HK";
|
||||
final String path = "/" + groupId + "/" + dataId;
|
||||
final String scheme = "digest";
|
||||
final String auth = "root:123456";
|
||||
|
||||
AuthInfo authInfo = new AuthInfo(scheme, auth.getBytes());
|
||||
List<AuthInfo> authInfoList = Collections.singletonList(authInfo);
|
||||
|
||||
CuratorFramework zkClient = CuratorFrameworkFactory.builder().
|
||||
connectString(remoteAddress).
|
||||
retryPolicy(new ExponentialBackoffRetry(3, 100)).
|
||||
authorization(authInfoList).
|
||||
build();
|
||||
zkClient.start();
|
||||
Stat stat = zkClient.checkExists().forPath(path);
|
||||
if (stat == null) {
|
||||
ACL acl = new ACL(ZooDefs.Perms.ALL, new Id(scheme, DigestAuthenticationProvider.generateDigest(auth)));
|
||||
zkClient.create().creatingParentContainersIfNeeded().withACL(Collections.singletonList(acl)).forPath(path, null);
|
||||
}
|
||||
|
||||
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<List<FlowRule>>(remoteAddress,
|
||||
authInfoList, groupId, dataId,
|
||||
new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
|
||||
|
||||
|
||||
final String resourceName = "HK";
|
||||
publishThenTestFor(zkClient, path, resourceName, 10);
|
||||
publishThenTestFor(zkClient, path, resourceName, 15);
|
||||
|
||||
zkClient.close();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
private void publishThenTestFor(CuratorFramework zkClient, String path, String resourceName, long count) throws Exception {
|
||||
FlowRule rule = new FlowRule().setResource(resourceName)
|
||||
.setLimitApp("default")
|
||||
.as(FlowRule.class)
|
||||
.setCount(count)
|
||||
.setGrade(RuleConstant.FLOW_GRADE_QPS);
|
||||
String ruleString = JSON.toJSONString(Collections.singletonList(rule));
|
||||
zkClient.setData().forPath(path, ruleString.getBytes());
|
||||
|
||||
await().timeout(5, TimeUnit.SECONDS)
|
||||
.until(new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
return rules != null && !rules.isEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
List<FlowRule> rules = FlowRuleManager.getRules();
|
||||
boolean exists = false;
|
||||
for (FlowRule r : rules) {
|
||||
if (resourceName.equals(r.getResource())) {
|
||||
exists = true;
|
||||
assertEquals(count, new Double(r.getCount()).longValue());
|
||||
}
|
||||
}
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test whether different dataSources can share the same zkClient when the connection parameters are the same.
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testZooKeeperDataSourceSameZkClient() throws Exception {
|
||||
TestingServer server = new TestingServer(21813);
|
||||
server.start();
|
||||
|
||||
final String remoteAddress = server.getConnectString();
|
||||
final String flowPath = "/sentinel-zk-ds-demo/flow-HK";
|
||||
final String degradePath = "/sentinel-zk-ds-demo/degrade-HK";
|
||||
|
||||
|
||||
ZookeeperDataSource<List<FlowRule>> flowRuleZkDataSource = new ZookeeperDataSource<>(remoteAddress, flowPath,
|
||||
new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
ZookeeperDataSource<List<DegradeRule>> degradeRuleZkDataSource = new ZookeeperDataSource<>(remoteAddress, degradePath,
|
||||
new Converter<String, List<DegradeRule>>() {
|
||||
@Override
|
||||
public List<DegradeRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Assert.assertTrue(flowRuleZkDataSource.getZkClient() == degradeRuleZkDataSource.getZkClient());
|
||||
|
||||
|
||||
final String groupId = "sentinel-zk-ds-demo";
|
||||
final String flowDataId = "flow-HK";
|
||||
final String degradeDataId = "degrade-HK";
|
||||
final String scheme = "digest";
|
||||
final String auth = "root:123456";
|
||||
AuthInfo authInfo = new AuthInfo(scheme, auth.getBytes());
|
||||
List<AuthInfo> authInfoList = Collections.singletonList(authInfo);
|
||||
|
||||
|
||||
ZookeeperDataSource<List<FlowRule>> flowRuleZkAutoDataSource = new ZookeeperDataSource<List<FlowRule>>(remoteAddress,
|
||||
authInfoList, groupId, flowDataId,
|
||||
new Converter<String, List<FlowRule>>() {
|
||||
@Override
|
||||
public List<FlowRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ZookeeperDataSource<List<DegradeRule>> degradeRuleZkAutoDataSource = new ZookeeperDataSource<List<DegradeRule>>(remoteAddress,
|
||||
authInfoList, groupId, degradeDataId,
|
||||
new Converter<String, List<DegradeRule>>() {
|
||||
@Override
|
||||
public List<DegradeRule> convert(String source) {
|
||||
return JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Assert.assertTrue(flowRuleZkAutoDataSource.getZkClient() == degradeRuleZkAutoDataSource.getZkClient());
|
||||
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
27
sentinel/sentinel-extension/sentinel-metric-exporter/pom.xml
Normal file
27
sentinel/sentinel-extension/sentinel-metric-exporter/pom.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>sentinel-extension</artifactId>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<version>1.8.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sentinel-metric-exporter</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric;
|
||||
|
||||
import com.alibaba.csp.sentinel.init.InitFunc;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.metric.collector.MetricCollector;
|
||||
import com.alibaba.csp.sentinel.metric.exporter.MetricExporter;
|
||||
import com.alibaba.csp.sentinel.metric.exporter.jmx.JMXMetricExporter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The{@link MetricExporterInit} work on load Metric exporters.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 19:58
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class MetricExporterInit implements InitFunc {
|
||||
|
||||
/**
|
||||
* the list of metric exporters.
|
||||
*/
|
||||
private static List<MetricExporter> metricExporters = new ArrayList<>();
|
||||
|
||||
/*
|
||||
load metric exporters.
|
||||
*/
|
||||
static {
|
||||
// now we use this simple way to load MetricExporter.
|
||||
metricExporters.add(new JMXMetricExporter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
RecordLog.info("[MetricExporterInit] MetricExporter start init.");
|
||||
// start the metric exporters.
|
||||
for (MetricExporter metricExporter : metricExporters) {
|
||||
try {
|
||||
metricExporter.start();
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[MetricExporterInit] MetricExporterInit start the metricExport[{}] failed, will ignore it.",
|
||||
metricExporter.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// add shutdown hook.
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(
|
||||
() -> metricExporters.forEach(metricExporter -> {
|
||||
try {
|
||||
metricExporter.shutdown();
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[MetricExporterInit] MetricExporterInit shutdown the metricExport[{}] failed, will ignore it.",
|
||||
metricExporter.getClass().getName(), e);
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.collector;
|
||||
|
||||
import com.alibaba.csp.sentinel.Constants;
|
||||
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||
import com.alibaba.csp.sentinel.node.metric.MetricNode;
|
||||
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper;
|
||||
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
|
||||
import com.alibaba.csp.sentinel.util.TimeUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The {@link MetricCollector} work on collecting metrics in {@link MetricNode}.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:01
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class MetricCollector {
|
||||
|
||||
/**
|
||||
* collect the metrics in {@link MetricNode}.
|
||||
*
|
||||
* @return the metric grouped by resource name.
|
||||
*/
|
||||
public Map<String, MetricNode> collectMetric() {
|
||||
final long currentTime = TimeUtil.currentTimeMillis();
|
||||
final long maxTime = currentTime - currentTime % 1000;
|
||||
final long minTime = maxTime - 1000;
|
||||
Map<String, MetricNode> metricNodeMap = new HashMap<>();
|
||||
for (Map.Entry<ResourceWrapper, ClusterNode> e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) {
|
||||
ClusterNode node = e.getValue();
|
||||
List<MetricNode> metrics = getLastMetrics(node, minTime, maxTime);
|
||||
aggregate(metricNodeMap, metrics, node);
|
||||
}
|
||||
aggregate(metricNodeMap, getLastMetrics(Constants.ENTRY_NODE, minTime, maxTime), Constants.ENTRY_NODE);
|
||||
return metricNodeMap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the last second {@link MetricNode} of {@link ClusterNode}
|
||||
* @param node {@link ClusterNode}
|
||||
* @param minTime the min time.
|
||||
* @param maxTime the max time.
|
||||
* @return the list of {@link MetricNode}
|
||||
*/
|
||||
private List<MetricNode> getLastMetrics(ClusterNode node, long minTime, long maxTime) {
|
||||
return node.rawMetricsInMin(time -> time >= minTime && time < maxTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* aggregate the metrics, the metrics under the same resource will left the lasted value
|
||||
* @param metricNodeMap metrics map
|
||||
* @param metrics metrics info group by timestamp
|
||||
* @param node the node
|
||||
*/
|
||||
private void aggregate(Map<String, MetricNode> metricNodeMap, List<MetricNode> metrics, ClusterNode node) {
|
||||
if (metrics == null || metrics.size() == 0) {
|
||||
return;
|
||||
}
|
||||
for (MetricNode metricNode : metrics) {
|
||||
String resource = node.getName();
|
||||
metricNode.setResource(resource);
|
||||
metricNode.setClassification(node.getResourceType());
|
||||
MetricNode existMetricNode = metricNodeMap.get(resource);
|
||||
// always keep the MetricNode is the last
|
||||
if (existMetricNode != null && existMetricNode.getTimestamp() > metricNode.getTimestamp()) {
|
||||
continue;
|
||||
}
|
||||
metricNodeMap.put(resource, metricNode);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter;
|
||||
|
||||
/**
|
||||
* {@link MetricExporter} work on export metric to target monitor.
|
||||
* you can implement your export ways by this class.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 21:16
|
||||
*/
|
||||
public interface MetricExporter {
|
||||
|
||||
/**
|
||||
* start the {@link MetricExporter}.
|
||||
*
|
||||
* @throws Exception start exception.
|
||||
*/
|
||||
void start() throws Exception;
|
||||
|
||||
/**
|
||||
* export the data to target monitor by the implement.
|
||||
*
|
||||
* @throws Exception export exception.
|
||||
*/
|
||||
void export() throws Exception;
|
||||
|
||||
/**
|
||||
* shutdown the {@link MetricExporter}.
|
||||
*
|
||||
* @throws Exception shutdown exception.
|
||||
*/
|
||||
void shutdown() throws Exception;
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter.jmx;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.metric.collector.MetricCollector;
|
||||
import com.alibaba.csp.sentinel.metric.exporter.MetricExporter;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The JMX metric exporter, mainly for write metric datas to JMX bean. It implement {@link MetricExporter}, provide method
|
||||
* start, export and shutdown. The mainly design for the jmx is refresh the JMX bean data scheduled.
|
||||
* {@link JMXExportTask} work on export data to {@link MetricBean}.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:02
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class JMXMetricExporter implements MetricExporter {
|
||||
|
||||
/**
|
||||
* schedule executor.
|
||||
*/
|
||||
private final ScheduledExecutorService jmxExporterSchedule;
|
||||
|
||||
/**
|
||||
* JMX metric writer, write metric datas to {@link MetricBean}.
|
||||
*/
|
||||
private final MetricBeanWriter metricBeanWriter = new MetricBeanWriter();
|
||||
|
||||
/**
|
||||
* global metrics collector.
|
||||
*/
|
||||
private final MetricCollector metricCollector = new MetricCollector();
|
||||
|
||||
public JMXMetricExporter() {
|
||||
jmxExporterSchedule = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("sentinel-metrics-jmx-exporter-task", true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
jmxExporterSchedule.scheduleAtFixedRate(new JMXExportTask(), 1, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void export() throws Exception {
|
||||
metricBeanWriter.write(metricCollector.collectMetric());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() throws Exception {
|
||||
jmxExporterSchedule.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* JMXExportTask mainly work on execute the JMX metric export.
|
||||
*/
|
||||
class JMXExportTask implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
export();
|
||||
} catch (Exception e) {
|
||||
RecordLog.warn("[JMX Metric Exporter] export to JMX MetricBean failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter.jmx;
|
||||
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
|
||||
import javax.management.JMException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MBeanServerFactory;
|
||||
import javax.management.ObjectName;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* This class provides a unified interface for registering/unregistering of Metric MBean.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:02
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class MBeanRegistry {
|
||||
|
||||
private static volatile MBeanRegistry instance = new MBeanRegistry();
|
||||
|
||||
private Map<MetricBean, String> mapBean2Name= new ConcurrentHashMap<>(8);
|
||||
|
||||
private Map<String, MetricBean> mapName2Bean = new ConcurrentHashMap<>(8);
|
||||
|
||||
private MBeanServer mBeanServer;
|
||||
|
||||
public static MBeanRegistry getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public MBeanRegistry() {
|
||||
try {
|
||||
mBeanServer = ManagementFactory.getPlatformMBeanServer();
|
||||
} catch (Error e) {
|
||||
// Account for running within IKVM and create a new MBeanServer
|
||||
// if the PlatformMBeanServer does not exist.
|
||||
mBeanServer = MBeanServerFactory.createMBeanServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new MBean with the platform MBean server.
|
||||
* @param bean the bean being registered
|
||||
* @param mBeanName the mBeanName
|
||||
* @throws JMException MBean can not register exception
|
||||
*/
|
||||
public void register(MetricBean bean, String mBeanName) throws JMException {
|
||||
assert bean != null;
|
||||
try {
|
||||
ObjectName oname = new ObjectName(mBeanName);
|
||||
mBeanServer.registerMBean(bean, oname);
|
||||
mapBean2Name.put(bean, mBeanName);
|
||||
mapName2Bean.put(mBeanName, bean);
|
||||
} catch (JMException e) {
|
||||
RecordLog.warn("[MBeanRegistry] Failed to register MBean " + mBeanName, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister the MetricBean
|
||||
* @param bean MetricBean
|
||||
*/
|
||||
public void unRegister(MetricBean bean) {
|
||||
assert bean != null;
|
||||
String beanName = mapBean2Name.get(bean);
|
||||
if (beanName == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ObjectName objectName = new ObjectName(beanName);
|
||||
mBeanServer.unregisterMBean(objectName);
|
||||
mapBean2Name.remove(bean);
|
||||
mapName2Bean.remove(beanName);
|
||||
} catch (JMException e) {
|
||||
RecordLog.warn("[MBeanRegistry] UnRegister the MetricBean fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find the MBean by BeanName
|
||||
* @param mBeanName mBeanName
|
||||
* @return MetricMBean
|
||||
*/
|
||||
public MetricBean findMBean(String mBeanName) {
|
||||
if (mBeanName == null) {
|
||||
return null;
|
||||
}
|
||||
return mapName2Bean.get(mBeanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* list all MBeans which is registered into MBeanRegistry
|
||||
* @return MetricBeans
|
||||
*/
|
||||
public List<MetricBean> listAllMBeans() {
|
||||
return new ArrayList<>(mapName2Bean.values());
|
||||
}
|
||||
}
|
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter.jmx;
|
||||
|
||||
import com.alibaba.csp.sentinel.node.metric.MetricNode;
|
||||
|
||||
/**
|
||||
* the MetricBean for JMX expose.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:02
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class MetricBean implements MetricMXBean {
|
||||
|
||||
private String resource;
|
||||
|
||||
/**
|
||||
* Resource classification (e.g. SQL or RPC)
|
||||
*/
|
||||
private int classification;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
private long passQps;
|
||||
|
||||
private long blockQps;
|
||||
|
||||
private long successQps;
|
||||
|
||||
private long exceptionQps;
|
||||
|
||||
private long rt;
|
||||
|
||||
private long occupiedPassQps;
|
||||
|
||||
private int concurrency;
|
||||
|
||||
private long version;
|
||||
|
||||
@Override
|
||||
public String getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClassification() {
|
||||
return classification;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPassQps() {
|
||||
return passQps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockQps() {
|
||||
return blockQps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSuccessQps() {
|
||||
return successQps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getExceptionQps() {
|
||||
return exceptionQps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRt() {
|
||||
return rt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOccupiedPassQps() {
|
||||
return occupiedPassQps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConcurrency() {
|
||||
return concurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the version to current Mbean.
|
||||
*
|
||||
* @param version current version.
|
||||
*/
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* reset the MBean value to the initialized value.
|
||||
*/
|
||||
public void reset() {
|
||||
this.blockQps = 0;
|
||||
this.passQps = 0;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.exceptionQps = 0;
|
||||
this.occupiedPassQps = 0;
|
||||
this.successQps = 0;
|
||||
this.rt = 0;
|
||||
this.concurrency = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the MetricBean's value which from MetricNode.
|
||||
*
|
||||
* @param metricNode metric Node for write file
|
||||
*/
|
||||
public void setValueFromNode(MetricNode metricNode) {
|
||||
if (metricNode == null) {
|
||||
return;
|
||||
}
|
||||
this.successQps = metricNode.getSuccessQps();
|
||||
this.blockQps = metricNode.getBlockQps();
|
||||
this.passQps = metricNode.getPassQps();
|
||||
this.occupiedPassQps = metricNode.getOccupiedPassQps();
|
||||
this.exceptionQps = metricNode.getExceptionQps();
|
||||
this.timestamp = metricNode.getTimestamp();
|
||||
this.classification = metricNode.getClassification();
|
||||
this.concurrency = metricNode.getConcurrency();
|
||||
this.resource = metricNode.getResource();
|
||||
this.rt = metricNode.getRt();
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter.jmx;
|
||||
|
||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
|
||||
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||
import com.alibaba.csp.sentinel.node.metric.MetricNode;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* the metric bean writer, it provides {@link MetricBeanWriter#write} method for register the
|
||||
* MetricBean in {@link MBeanRegistry} or update the value of MetricBean
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:02
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public class MetricBeanWriter {
|
||||
|
||||
private final MBeanRegistry mBeanRegistry = MBeanRegistry.getInstance();
|
||||
|
||||
private static final String DEFAULT_APP_NAME = "sentinel-application";
|
||||
|
||||
/**
|
||||
* write the MetricNode value to MetricBean
|
||||
* if the MetricBean is not registered into {@link MBeanRegistry},
|
||||
* it will be created and registered into {@link MBeanRegistry}.
|
||||
* else it will update the value of MetricBean.
|
||||
* Notes. if the MetricNode is null, then {@link MetricBean} will be reset.
|
||||
* @param map metricNode value group by resource
|
||||
* @throws Exception write failed exception
|
||||
*/
|
||||
public synchronized void write(Map<String, MetricNode> map) throws Exception {
|
||||
if (map == null || map.isEmpty()) {
|
||||
List<MetricBean> metricNodes = mBeanRegistry.listAllMBeans();
|
||||
if (metricNodes == null || metricNodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (MetricBean metricNode : metricNodes) {
|
||||
metricNode.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
String appName = SentinelConfig.getAppName();
|
||||
if (appName == null) {
|
||||
appName = DEFAULT_APP_NAME;
|
||||
}
|
||||
long version = System.currentTimeMillis();
|
||||
// set or update the new value
|
||||
for (MetricNode metricNode : map.values()) {
|
||||
final String mBeanName = "Sentinel:type=" + appName + ",name=\"" + metricNode.getResource()
|
||||
+"\",classification=\"" + metricNode.getClassification() +"\"";
|
||||
MetricBean metricBean = mBeanRegistry.findMBean(mBeanName);
|
||||
if (metricBean != null) {
|
||||
metricBean.setValueFromNode(metricNode);
|
||||
metricBean.setVersion(version);
|
||||
} else {
|
||||
metricBean = new MetricBean();
|
||||
metricBean.setValueFromNode(metricNode);
|
||||
metricBean.setVersion(version);
|
||||
mBeanRegistry.register(metricBean, mBeanName);
|
||||
RecordLog.info("[MetricBeanWriter] Registering with JMX as Metric MBean [{}]", mBeanName);
|
||||
}
|
||||
}
|
||||
// reset the old value
|
||||
List<MetricBean> metricBeans = mBeanRegistry.listAllMBeans();
|
||||
if (metricBeans == null || metricBeans.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (MetricBean metricBean : metricBeans) {
|
||||
if (!Objects.equals(metricBean.getVersion(), version)) {
|
||||
metricBean.reset();
|
||||
mBeanRegistry.unRegister(metricBean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 1999-2021 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.metric.exporter.jmx;
|
||||
|
||||
/**
|
||||
* the Metric JMX Bean interface.
|
||||
*
|
||||
* @author chenglu
|
||||
* @date 2021-07-01 20:02
|
||||
* @since 1.8.3
|
||||
*/
|
||||
public interface MetricMXBean {
|
||||
|
||||
long getTimestamp();
|
||||
|
||||
long getOccupiedPassQps();
|
||||
|
||||
long getSuccessQps();
|
||||
|
||||
long getPassQps();
|
||||
|
||||
long getExceptionQps();
|
||||
|
||||
long getBlockQps();
|
||||
|
||||
long getRt();
|
||||
|
||||
String getResource();
|
||||
|
||||
int getClassification();
|
||||
|
||||
int getConcurrency();
|
||||
|
||||
long getVersion();
|
||||
}
|
@@ -0,0 +1 @@
|
||||
com.alibaba.csp.sentinel.metric.MetricExporterInit
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user