@@ -22,6 +22,7 @@ | |||||
<module>sentinel-datasource-spring-cloud-config</module> | <module>sentinel-datasource-spring-cloud-config</module> | ||||
<module>sentinel-datasource-consul</module> | <module>sentinel-datasource-consul</module> | ||||
<module>sentinel-datasource-etcd</module> | <module>sentinel-datasource-etcd</module> | ||||
<module>sentinel-datasource-eureka</module> | |||||
<module>sentinel-annotation-cdi-interceptor</module> | <module>sentinel-annotation-cdi-interceptor</module> | ||||
</modules> | </modules> | ||||
@@ -0,0 +1,64 @@ | |||||
# 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", new Converter<String, List<FlowRule>>() { | |||||
@Override | |||||
public List<FlowRule> convert(String o) { | |||||
return JSON.parseObject(o, new TypeReference<List<FlowRule>>() { | |||||
}); | |||||
} | |||||
}); | |||||
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.0-SNAPSHOT</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,213 @@ | |||||
/* | |||||
* 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.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 | |||||
* @create: 2020-05-23 12:01 | |||||
*/ | |||||
public class EurekaDataSource<T> extends AutoRefreshDataSource<String, T> { | |||||
private static final long DEFAULT_REFRESH_MS = 10000; | |||||
/** | |||||
* connect timeout: 3s | |||||
*/ | |||||
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 3000; | |||||
/** | |||||
* read timeout: 30s | |||||
*/ | |||||
private static final int DEFAULT_READ_TIMEOUT_MS = 30000; | |||||
private int connectTimeoutMills; | |||||
private int readTimeoutMills; | |||||
/** | |||||
* eureka instance appid | |||||
*/ | |||||
private String appId; | |||||
/** | |||||
* eureka instance id | |||||
*/ | |||||
private String instanceId; | |||||
/** | |||||
* collect of eureka server urls | |||||
*/ | |||||
private List<String> serviceUrls; | |||||
/** | |||||
* metadata key of the rule source | |||||
*/ | |||||
private 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/ |