Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add redis implement of datasource #102

Merged
merged 7 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentinel-extension/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<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>
</modules>

Expand Down
37 changes: 37 additions & 0 deletions sentinel-extension/sentinel-datasource-redis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Sentinel DataSource Redis
sczyh30 marked this conversation as resolved.
Show resolved Hide resolved

Sentinel DataSource Redis provides integration with Redis so that Redis
can be the dynamic rule data source of Sentinel. The data source uses push model (listener) with redis pub/sub feature.

this RedisDataSource implement only Redis Standalone. if you want to use redis cluster. please read this [Redis Cluster PUB/SUB](https://github.com/lettuce-io/lettuce-core/wiki/Pub-Sub),
then you can implement by yourself.

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

DataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<List<FlowRule>>(client, ruleKey, channel, flowConfigParser);
FlowRuleManager.register2Property(redisDataSource.getProperty());
```

*client* : we use [lettuce](https://lettuce.io/) as redis-cli client. you can build client this way [how client build](https://github.com/lettuce-io/lettuce-core/wiki/Basic-usage) .

*ruleKey* : when the json rule data publish. it also should save to the key for init read.

*channel* : the channel to listen.

you can also create multi data source listen for different rule type.

you can run test case for usage.
55 changes: 55 additions & 0 deletions sentinel-extension/sentinel-datasource-redis/pom.xml
Original file line number Diff line number Diff line change
@@ -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>0.2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sentinel-datasource-redis</artifactId>
<packaging>jar</packaging>

<properties>
<zookeeper.version>3.4.13</zookeeper.version>
tigerMoon marked this conversation as resolved.
Show resolved Hide resolved
<curator.version>4.0.1</curator.version>
<curator.test.version>2.12.0</curator.test.version>
<lettuce.version>5.0.1.RELEASE</lettuce.version>
<redis.mock.version>0.1.6</redis.mock.version>
</properties>

<dependencies>

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>${lettuce.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.alibaba.csp.sentinel.datasource.redis;
tigerMoon marked this conversation as resolved.
Show resolved Hide resolved

import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
import com.alibaba.csp.sentinel.datasource.ConfigParser;
import com.alibaba.csp.sentinel.log.RecordLog;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.RedisPubSubAdapter;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;


/**
* Redis data resource. when data publish in redis,this will update rule config in time
*
* @author moon tiger
*/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can reformat your code with Alibaba P3C Plugin and obey the rules of Alibaba Coding Guidelines. (e.g. remove redundant lines)

public class RedisDataSource<T> extends AbstractDataSource<String, T> {

private RedisClient redisClient = null;

private String ruleKey;

public RedisDataSource(RedisClient client, String ruleKey, String channel, ConfigParser<String, T> parser) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the constructor should not directly accept a RedisClient as users don't have to know the internal client here. Users can provide host and port, then we create the client internal. So even if we changed the Redis client library, users don't have to care about that.

The constructor can be like this:

public RedisDataSource(String host, int port, String ruleKey, String channel, ConfigParser<String, T> parser)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. it should be decoupled. but when build RedisClient with lettuce. it has different mode to build like this:

Standalone mode build:

RedisURI masterUri = RedisURI.Builder.redis("master-host", 6379).build();

Sentinel mode build:

RedisURI sentinelUri = RedisURI.Builder.sentinel("sentinel-host", 26379, "master-name").build();

case i build a own constructor to compatible to all:

MyRedisURI{host,port,password,RedisConnectionType,ClientResources}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's okay, you can implement a universal RedisConfig class including these attributes, then users can pass a RedisConfig to the constructor.

super(parser);
this.redisClient = client;
this.ruleKey = ruleKey;
tigerMoon marked this conversation as resolved.
Show resolved Hide resolved
initConfig();
subscribeFromChannel(channel);
}

private void subscribeFromChannel(String channel) {
StatefulRedisPubSubConnection<String, String> pubSubConnection = redisClient.connectPubSub();
RedisPubSubAdapter<String, String> adapterListener = new DelegatingRedisPubSubListener();
pubSubConnection.addListener(adapterListener);
RedisPubSubCommands<String, String> sync = pubSubConnection.sync();
sync.subscribe(channel);
}


private void initConfig() {
try {
loadConfig();
tigerMoon marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e) {
RecordLog.info("[RedisDataSource] Error when loading initial config", e);
}
}

@Override
public String readSource() throws Exception {
if (this.redisClient == null) {
throw new IllegalStateException("redis client has not been initialized or error occurred");
}
RedisCommands<String, String> stringRedisCommands = redisClient.connect().sync();
String value = stringRedisCommands.get(ruleKey);
if (value != null) {
return value;
}
RecordLog.warn("rules is empty in redis. key:" + ruleKey);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here just return the retrieved value is okay. Users should check whether the value is empty when invoking this method.

return null;
}

@Override
public void close() throws Exception {
redisClient.shutdown();
}


private class DelegatingRedisPubSubListener extends RedisPubSubAdapter<String, String> {

DelegatingRedisPubSubListener() {
}

@Override
public void message(String channel, String message) {
getProperty().updateValue(parser.parse(message));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add a log here like this:

RecordLog.info(String.format("[RedisDataSource] New property value received for (%s:%d, %s): %s", host, port, channel, message));

}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.alibaba.csp.sentinel.datasource.redis;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add license header here.


import ai.grakn.redismock.RedisServer;
import com.alibaba.csp.sentinel.datasource.ConfigParser;
import com.alibaba.csp.sentinel.datasource.DataSource;
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 org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.List;

public class RedisDataSourceTest {

private static RedisServer server = null;

private RedisClient client;

private String ruleKey = "sentinel.flow.rulekey";

private String channel = "sentinel.flow.channel";

@Before
public void buildResource() {
try {
//bind to a random port
server = RedisServer.newRedisServer();
server.start();
} catch (IOException e) {
e.printStackTrace();
}
ConfigParser<String, List<FlowRule>> flowConfigParser = buildFlowConfigParser();
client = RedisClient.create(RedisURI.create(server.getHost(), server.getBindPort()));
initRedisRuleData();
DataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<List<FlowRule>>(client, ruleKey, channel, flowConfigParser);
FlowRuleManager.register2Property(redisDataSource.getProperty());
}

@Test
public void pub_msg_and_receive_success() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method name should follow camel-case naming style like: testPubMessageAndReceiveSuccess.

List<FlowRule> rules = FlowRuleManager.getRules();
Assert.assertTrue(rules.size() == 0);
tigerMoon marked this conversation as resolved.
Show resolved Hide resolved
int maxQueueingTimeMs = 480;
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}]";
connection.sync().publish(channel, flowRules);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
rules = FlowRuleManager.getRules();
Assert.assertTrue(rules.get(0).getMaxQueueingTimeMs() == maxQueueingTimeMs);
}


@Test
public void init_and_parse_flow_rule_success() {
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
String value = stringRedisCommands.get(ruleKey);
List<FlowRule> flowRules = buildFlowConfigParser().parse(value);
Assert.assertTrue(flowRules.size() == 1);
stringRedisCommands.del(ruleKey);
}

@Test
public void read_resource_fail() {
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
stringRedisCommands.del(ruleKey);
String value = stringRedisCommands.get(ruleKey);
Assert.assertTrue(value == null);
}


@After
public void clearResource() {
RedisCommands<String, String> stringRedisCommands = client.connect().sync();
stringRedisCommands.del(ruleKey);
client.shutdown();
server.stop();
server = null;
}

private ConfigParser<String, List<FlowRule>> buildFlowConfigParser() {
return new ConfigParser<String, List<FlowRule>>() {
@Override
public List<FlowRule> parse(String source) {
return 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.assertTrue(ok.equals("OK"));
}
}