+ * When data source init,reads form redis with string k-v,value is string format rule config data. + * This data source subscribe from specific channel and then data published in redis with this channel,data source + * will be notified and then update rule config in time. + *
+ * + * @author tiger + */ + +public class RedisDataSourceruleKey`s value to {@literal alibaba/Sentinel} rule type
+ */
+ public RedisDataSource(RedisConnectionConfig connectionConfig, String ruleKey, String channel, Converter parser) {
+ super(parser);
+ AssertUtil.notNull(connectionConfig, "redis connection config can not be null");
+ AssertUtil.notEmpty(ruleKey, "redis subscribe ruleKey can not be empty");
+ AssertUtil.notEmpty(channel, "redis subscribe channel can not be empty");
+ this.redisClient = getRedisClient(connectionConfig);
+ this.ruleKey = ruleKey;
+ loadInitialConfig();
+ subscribeFromChannel(channel);
+ }
+
+ /**
+ * build redis client form {@code RedisConnectionConfig} with io.lettuce.
+ *
+ * @return a new {@link RedisClient}
+ */
+ private RedisClient getRedisClient(RedisConnectionConfig connectionConfig) {
+ if (connectionConfig.getRedisSentinels().size() == 0) {
+ RecordLog.info("start standLone mode to connect to redis");
+ return getRedisStandLoneClient(connectionConfig);
+ } else {
+ RecordLog.info("start redis sentinel mode to connect to redis");
+ return getRedisSentinelClient(connectionConfig);
+ }
+ }
+
+ private RedisClient getRedisStandLoneClient(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(connectionConfig.getTimeout(), TimeUnit.MILLISECONDS);
+ 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) {
+ StatefulRedisPubSubConnection pubSubConnection = redisClient.connectPubSub();
+ RedisPubSubAdapter adapterListener = new DelegatingRedisPubSubListener();
+ pubSubConnection.addListener(adapterListener);
+ RedisPubSubCommands 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() throws Exception {
+ if (this.redisClient == null) {
+ throw new IllegalStateException("redis client has not been initialized or error occurred");
+ }
+ RedisCommands stringRedisCommands = redisClient.connect().sync();
+ return stringRedisCommands.get(ruleKey);
+ }
+
+ @Override
+ public void close() throws Exception {
+ redisClient.shutdown();
+ }
+
+ private class DelegatingRedisPubSubListener extends RedisPubSubAdapter {
+
+ DelegatingRedisPubSubListener() {
+ }
+
+ @Override
+ public void message(String channel, String message) {
+ RecordLog.info(String.format("[RedisDataSource] New property value received for channel %s: %s", channel, message));
+ getProperty().updateValue(parser.convert(message));
+ }
+ }
+
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisConnectionConfig.java b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisConnectionConfig.java
new file mode 100644
index 0000000000..6fa23b00d1
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisConnectionConfig.java
@@ -0,0 +1,583 @@
+/*
+ * 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.datasource.redis.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 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 redisSentinels = new ArrayList();
+
+ /**
+ * 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 getRedisSentinels() {
+ return redisSentinels;
+ }
+
+
+ @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);
+ }
+
+ 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;
+ }
+ 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);
+ 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 redisSentinels = new ArrayList();
+
+ 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;
+ }
+
+ /**
+ * 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() && StringUtil.isEmpty(host)) {
+ throw new IllegalStateException(
+ "Cannot build a RedisConnectionConfig. One of the following must be provided Host, Socket 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));
+ }
+
+ redisURI.setTimeout(timeout);
+
+ return redisURI;
+ }
+ }
+
+ /**
+ * Return true for valid port numbers.
+ */
+ private static boolean isValidPort(int port) {
+ return port >= 0 && port <= 65535;
+ }
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisHostAndPort.java b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisHostAndPort.java
new file mode 100644
index 0000000000..4a199c1883
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/config/RedisHostAndPort.java
@@ -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.datasource.redis.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();
+ }
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/util/AssertUtil.java b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/util/AssertUtil.java
new file mode 100644
index 0000000000..38e9d47052
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/main/java/com/alibaba/csp/sentinel/datasource/redis/util/AssertUtil.java
@@ -0,0 +1,81 @@
+/*
+ * 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.util;
+
+import com.alibaba.csp.sentinel.util.StringUtil;
+
+/**
+ * A util class for filed check
+ *
+ * @author tiger
+ */
+public class AssertUtil {
+
+ private AssertUtil(){}
+ /**
+ * Assert that a string is not empty, it must not be {@code null} and it must not be empty.
+ *
+ * @param string the object to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object is {@code null} or the underlying string is empty
+ */
+ public static void notEmpty(String string, String message) {
+ if (StringUtil.isEmpty(string)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that an object is not {@code null} .
+ *
+ * @param object the object to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object is {@code null}
+ */
+ public static void notNull(Object object, String message) {
+ if (object == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that {@code value} is {@literal true}.
+ *
+ * @param value the value to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object array contains a {@code null} element
+ */
+ public static void isTrue(boolean value, String message) {
+ if (!value) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not involving any parameters to the
+ * calling method.
+ *
+ * @param condition a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void assertState(boolean condition, String message) {
+ if (!condition) {
+ throw new IllegalStateException(message);
+ }
+ }
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/RedisConnectionConfigTest.java b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/RedisConnectionConfigTest.java
new file mode 100644
index 0000000000..89850f8e52
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/RedisConnectionConfigTest.java
@@ -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.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.assertEquals(null, 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.assertEquals(null, 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.assertEquals(null, redisConnectionConfig.getHost());
+ Assert.assertEquals(3, redisConnectionConfig.getRedisSentinels().size());
+ }
+
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/SentinelModeRedisDataSourceTest.java b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/SentinelModeRedisDataSourceTest.java
new file mode 100644
index 0000000000..c291940d70
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/SentinelModeRedisDataSourceTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.junit.*;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Redis redisSentinel mode test cases for {@link RedisDataSource}.
+ *
+ * @author tiger
+ */
+@Ignore(value = "before run this test. you should build your own redisSentinel config in local")
+public class SentinelModeRedisDataSourceTest {
+
+ private String host = "localhost";
+
+ private int redisSentinelPort = 5000;
+
+ private String redisSentinelMasterId = "mymaster";
+
+ private String ruleKey = "redis.redisSentinel.flow.rulekey";
+
+ private String channel = "redis.redisSentinel.flow.channel";
+
+ private final RedisClient client = RedisClient.create(RedisURI.Builder.sentinel(host, redisSentinelPort)
+ .withSentinelMasterId(redisSentinelMasterId).build());
+
+ @Before
+ public void initData() {
+ Converter> flowConfigParser = buildFlowConfigParser();
+ RedisConnectionConfig config = RedisConnectionConfig.builder()
+ .withRedisSentinel(host, redisSentinelPort)
+ .withRedisSentinel(host, redisSentinelPort)
+ .withSentinelMasterId(redisSentinelMasterId).build();
+ initRedisRuleData();
+ ReadableDataSource> 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 subCommands = client.connect().sync();
+ subCommands.multi();
+ subCommands.set(ruleKey, flowRulesJson);
+ subCommands.publish(channel, flowRulesJson);
+ subCommands.exec();
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ List rules = FlowRuleManager.getRules();
+ Assert.assertEquals(1, rules.size());
+ rules = FlowRuleManager.getRules();
+ Assert.assertEquals(rules.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
+ String value = subCommands.get(ruleKey);
+ List flowRulesValuesInRedis = buildFlowConfigParser().convert(value);
+ Assert.assertEquals(flowRulesValuesInRedis.size(), 1);
+ Assert.assertEquals(flowRulesValuesInRedis.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
+ }
+
+ @After
+ public void clearResource() {
+ RedisCommands stringRedisCommands = client.connect().sync();
+ stringRedisCommands.del(ruleKey);
+ client.shutdown();
+ }
+
+ private Converter> buildFlowConfigParser() {
+ return new Converter>() {
+ @Override
+ public List convert(String source) {
+ return JSON.parseObject(source, new TypeReference>() {
+ });
+ }
+ };
+ }
+
+ 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 stringRedisCommands = client.connect().sync();
+ String ok = stringRedisCommands.set(ruleKey, flowRulesJson);
+ Assert.assertTrue(ok.equals("OK"));
+ }
+}
diff --git a/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/StandLoneRedisDataSourceTest.java b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/StandLoneRedisDataSourceTest.java
new file mode 100644
index 0000000000..bc6c693544
--- /dev/null
+++ b/sentinel-extension/sentinel-datasource-redis/src/test/java/com/alibaba/csp/sentinel/datasource/redis/StandLoneRedisDataSourceTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.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;
+
+/**
+ * Redis standLone mode test cases for {@link RedisDataSource}.
+ *
+ * @author tiger
+ */
+public class StandLoneRedisDataSourceTest {
+
+ private static RedisServer server = null;
+
+ private RedisClient client;
+
+ private String ruleKey = "redisSentinel.flow.rulekey";
+
+ private String channel = "redisSentinel.flow.channel";
+
+ @Before
+ public void buildResource() {
+ try {
+ //bind to a random port
+ server = RedisServer.newRedisServer();
+ server.start();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Converter> flowConfigParser = buildFlowConfigParser();
+ client = RedisClient.create(RedisURI.create(server.getHost(), server.getBindPort()));
+ RedisConnectionConfig config = RedisConnectionConfig.builder()
+ .withHost(server.getHost())
+ .withPort(server.getBindPort())
+ .build();
+ initRedisRuleData();
+ ReadableDataSource> redisDataSource = new RedisDataSource>(config, ruleKey, channel, flowConfigParser);
+ FlowRuleManager.register2Property(redisDataSource.getProperty());
+ }
+
+ @Test
+ public void testPubMsgAndReceiveSuccess() {
+ List rules = FlowRuleManager.getRules();
+ Assert.assertEquals(1, rules.size());
+ int maxQueueingTimeMs = new Random().nextInt();
+ StatefulRedisPubSubConnection 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 subCommands = connection.sync();
+ subCommands.multi();
+ subCommands.set(ruleKey, flowRules);
+ subCommands.publish(channel, flowRules);
+ subCommands.exec();
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ rules = FlowRuleManager.getRules();
+ Assert.assertEquals(rules.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
+ String value = subCommands.get(ruleKey);
+ List flowRulesValuesInRedis = buildFlowConfigParser().convert(value);
+ Assert.assertEquals(flowRulesValuesInRedis.size(), 1);
+ Assert.assertEquals(flowRulesValuesInRedis.get(0).getMaxQueueingTimeMs(), maxQueueingTimeMs);
+ }
+
+ @Test
+ public void testInitAndParseFlowRuleSuccess() {
+ RedisCommands stringRedisCommands = client.connect().sync();
+ String value = stringRedisCommands.get(ruleKey);
+ List flowRules = buildFlowConfigParser().convert(value);
+ Assert.assertEquals(flowRules.size(), 1);
+ stringRedisCommands.del(ruleKey);
+ }
+
+ @Test
+ public void testReadResourceFail() {
+ RedisCommands stringRedisCommands = client.connect().sync();
+ stringRedisCommands.del(ruleKey);
+ String value = stringRedisCommands.get(ruleKey);
+ Assert.assertEquals(value, null);
+ }
+
+ @After
+ public void clearResource() {
+ RedisCommands stringRedisCommands = client.connect().sync();
+ stringRedisCommands.del(ruleKey);
+ client.shutdown();
+ server.stop();
+ server = null;
+ }
+
+ private Converter> buildFlowConfigParser() {
+ return new Converter>() {
+ @Override
+ public List convert(String source) {
+ return JSON.parseObject(source, new TypeReference>() {
+ });
+ }
+ };
+ }
+
+ 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 stringRedisCommands = client.connect().sync();
+ String ok = stringRedisCommands.set(ruleKey, flowRulesJson);
+ Assert.assertEquals(ok, "OK");
+ }
+}