diff --git a/pom.xml b/pom.xml index 8dd4fb692c..32ea5aa3ab 100644 --- a/pom.xml +++ b/pom.xml @@ -219,6 +219,12 @@ ${resilience4j.version} true + + org.powermock + powermock-module-junit4 + 2.0.2 + test + io.github.resilience4j resilience4j-circuitbreaker @@ -538,6 +544,7 @@ **/ClientTestUtil.java **/ReflectionTestUtil.java **/*CommandFlags*.java + src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java diff --git a/src/main/java/redis/clients/jedis/JedisSentineled.java b/src/main/java/redis/clients/jedis/JedisSentineled.java index efc4ff69c6..b2a041fb41 100644 --- a/src/main/java/redis/clients/jedis/JedisSentineled.java +++ b/src/main/java/redis/clients/jedis/JedisSentineled.java @@ -1,6 +1,5 @@ package redis.clients.jedis; -import java.util.Set; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.builders.SentinelClientBuilder; @@ -11,6 +10,8 @@ import redis.clients.jedis.providers.ConnectionProvider; import redis.clients.jedis.providers.SentineledConnectionProvider; +import java.util.Set; + public class JedisSentineled extends UnifiedJedis { public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, @@ -40,6 +41,21 @@ public JedisSentineled(String masterName, final JedisClientConfig masterClientCo masterClientConfig.getRedisProtocol()); } + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, poolConfig, sentinels, sentinelClientConfig, readFrom), + masterClientConfig.getRedisProtocol()); + } + + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom, + ReadOnlyPredicate readOnlyPredicate) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, poolConfig, sentinels, sentinelClientConfig, readFrom, readOnlyPredicate), + masterClientConfig.getRedisProtocol()); + } + @Experimental public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, diff --git a/src/main/java/redis/clients/jedis/ReadFrom.java b/src/main/java/redis/clients/jedis/ReadFrom.java new file mode 100644 index 0000000000..5ac339009b --- /dev/null +++ b/src/main/java/redis/clients/jedis/ReadFrom.java @@ -0,0 +1,12 @@ +package redis.clients.jedis; + +public enum ReadFrom { + // read from the upstream only. + UPSTREAM, + // read from the replica only. + REPLICA, + // read preferred from the upstream and fall back to a replica if the upstream is not available. + UPSTREAM_PREFERRED, + // read preferred from replica and fall back to upstream if no replica is not available. + REPLICA_PREFERRED +} diff --git a/src/main/java/redis/clients/jedis/ReadOnlyPredicate.java b/src/main/java/redis/clients/jedis/ReadOnlyPredicate.java new file mode 100644 index 0000000000..1c58929a3b --- /dev/null +++ b/src/main/java/redis/clients/jedis/ReadOnlyPredicate.java @@ -0,0 +1,11 @@ +package redis.clients.jedis; + +@FunctionalInterface +public interface ReadOnlyPredicate { + + /** + * @param command the input command. + * @return {@code true} if the input argument matches the predicate, otherwise {@code false} + */ + boolean isReadOnly(CommandArguments command); +} diff --git a/src/main/java/redis/clients/jedis/StaticReadOnlyPredicate.java b/src/main/java/redis/clients/jedis/StaticReadOnlyPredicate.java new file mode 100644 index 0000000000..13f98eaf5e --- /dev/null +++ b/src/main/java/redis/clients/jedis/StaticReadOnlyPredicate.java @@ -0,0 +1,16 @@ +package redis.clients.jedis; + +public class StaticReadOnlyPredicate implements ReadOnlyPredicate { + + private static final StaticReadOnlyPredicate REGISTRY = new StaticReadOnlyPredicate(); + + private StaticReadOnlyPredicate(){} + + public static StaticReadOnlyPredicate registry() { + return REGISTRY; + } + + public boolean isReadOnly(CommandArguments command) { + return StaticCommandFlagsRegistry.registry().getFlags(command).contains(CommandFlagsRegistry.CommandFlag.READONLY); + } +} diff --git a/src/main/java/redis/clients/jedis/builders/SentinelClientBuilder.java b/src/main/java/redis/clients/jedis/builders/SentinelClientBuilder.java index 24cae18eb3..8c1cb46cc1 100644 --- a/src/main/java/redis/clients/jedis/builders/SentinelClientBuilder.java +++ b/src/main/java/redis/clients/jedis/builders/SentinelClientBuilder.java @@ -1,10 +1,16 @@ package redis.clients.jedis.builders; -import java.util.Set; -import redis.clients.jedis.*; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.ReadFrom; +import redis.clients.jedis.ReadOnlyPredicate; +import redis.clients.jedis.StaticReadOnlyPredicate; import redis.clients.jedis.providers.ConnectionProvider; import redis.clients.jedis.providers.SentineledConnectionProvider; +import java.util.Set; + /** * Builder for creating JedisSentineled instances (Redis Sentinel connections). *

@@ -21,6 +27,10 @@ public abstract class SentinelClientBuilder private Set sentinels = null; private JedisClientConfig sentinelClientConfig = null; + private ReadFrom readFrom = ReadFrom.UPSTREAM; + + private ReadOnlyPredicate readOnlyPredicate = StaticReadOnlyPredicate.registry(); + /** * Sets the master name for the Redis Sentinel configuration. *

@@ -47,6 +57,33 @@ public SentinelClientBuilder sentinels(Set sentinels) { return this; } + /** + * Sets the readFrom. + *

+ * It is used to specify the policy preference of which nodes the client should read data from. It + * defines which type of node the client should prioritize reading data from when there are + * multiple Redis instances (such as master nodes and slave nodes) available in the Redis Sentinel + * environment. + * @param readFrom the read preferences + * @return this builder + */ + public SentinelClientBuilder readForm(ReadFrom readFrom) { + this.readFrom = readFrom; + return this; + } + + /** + * Sets the readOnlyPredicate. + *

+ * Check a Redis command is a read request. + * @param readOnlyPredicate + * @return this builder + */ + public SentinelClientBuilder readOnlyPredicate(ReadOnlyPredicate readOnlyPredicate) { + this.readOnlyPredicate = readOnlyPredicate; + return this; + } + /** * Sets the client configuration for Sentinel connections. *

@@ -68,7 +105,8 @@ protected SentinelClientBuilder self() { @Override protected ConnectionProvider createDefaultConnectionProvider() { return new SentineledConnectionProvider(this.masterName, this.clientConfig, this.cache, - this.poolConfig, this.sentinels, this.sentinelClientConfig); + this.poolConfig, this.sentinels, this.sentinelClientConfig, this.readFrom, + this.readOnlyPredicate); } @Override diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index c3b13c6016..16d247fab5 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -1,19 +1,8 @@ package redis.clients.jedis.providers; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import redis.clients.jedis.CommandArguments; import redis.clients.jedis.Connection; import redis.clients.jedis.ConnectionPool; @@ -21,6 +10,9 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.ReadFrom; +import redis.clients.jedis.ReadOnlyPredicate; +import redis.clients.jedis.StaticReadOnlyPredicate; import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.csc.Cache; import redis.clients.jedis.exceptions.JedisConnectionException; @@ -28,7 +20,26 @@ import redis.clients.jedis.util.IOUtils; import redis.clients.jedis.util.Pool; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + public class SentineledConnectionProvider implements ConnectionProvider { + class PoolInfo { + public String host; + public ConnectionPool pool; + + public PoolInfo(String host, ConnectionPool pool) { + this.host = host; + this.pool = pool; + } + } private static final Logger LOG = LoggerFactory.getLogger(SentineledConnectionProvider.class); @@ -52,8 +63,18 @@ public class SentineledConnectionProvider implements ConnectionProvider { private final long subscribeRetryWaitTimeMillis; + private final ReadFrom readFrom; + + private ReadOnlyPredicate readOnlyPredicate; + private final Lock initPoolLock = new ReentrantLock(true); + private final List slavePools = new ArrayList<>(); + + private final Lock slavePoolsLock = new ReentrantLock(true); + + private int poolIndex; + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { this(masterName, masterClientConfig, null, null, sentinels, sentinelClientConfig); @@ -72,26 +93,46 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); } + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom) { + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, readFrom, StaticReadOnlyPredicate.registry()); + } + + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom, + ReadOnlyPredicate readOnlyPredicate) { + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, readFrom, readOnlyPredicate); + } + + public SentineledConnectionProvider(String masterName, JedisClientConfig clientConfig, Cache cache, GenericObjectPoolConfig poolConfig, Set sentinels, JedisClientConfig sentinelClientConfig, ReadFrom readFrom, ReadOnlyPredicate readOnlyPredicate) { + this(masterName, clientConfig, cache, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, readFrom, readOnlyPredicate); + } + @Experimental public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { this(masterName, masterClientConfig, clientSideCache, poolConfig, sentinels, sentinelClientConfig, - DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, ReadFrom.UPSTREAM, StaticReadOnlyPredicate.registry()); } public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig, final long subscribeRetryWaitTimeMillis) { - this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, subscribeRetryWaitTimeMillis); + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, subscribeRetryWaitTimeMillis, ReadFrom.UPSTREAM, StaticReadOnlyPredicate.registry()); } @Experimental public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig, - final long subscribeRetryWaitTimeMillis) { + final long subscribeRetryWaitTimeMillis, ReadFrom readFrom, ReadOnlyPredicate readOnlyPredicate) { this.masterName = masterName; this.masterClientConfig = masterClientConfig; @@ -100,11 +141,49 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m this.sentinelClientConfig = sentinelClientConfig; this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis; + this.readFrom = readFrom; + this.readOnlyPredicate = readOnlyPredicate; HostAndPort master = initSentinels(sentinels); initMaster(master); } + private Connection getSlaveResource() { + int startIdx; + slavePoolsLock.lock(); + try { + poolIndex++; + if (poolIndex >= slavePools.size()) { + poolIndex = 0; + } + startIdx = poolIndex; + } finally { + slavePoolsLock.unlock(); + } + return _getSlaveResource(startIdx, 0); + } + + private Connection _getSlaveResource(int idx, int cnt) { + PoolInfo poolInfo; + slavePoolsLock.lock(); + try { + if (cnt >= slavePools.size()) { + return null; + } + poolInfo = slavePools.get(idx % slavePools.size()); + } finally { + slavePoolsLock.unlock(); + } + + try { + Connection jedis = poolInfo.pool.getResource(); + return jedis; + } catch (Exception e) { + LOG.error("get connection fail:", e); + return _getSlaveResource(idx + 1, cnt + 1); + } + } + @Override public Connection getConnection() { return pool.getResource(); @@ -112,7 +191,43 @@ public Connection getConnection() { @Override public Connection getConnection(CommandArguments args) { - return pool.getResource(); + boolean isReadCommand = readOnlyPredicate.isReadOnly(args); + if (!isReadCommand) { + return pool.getResource(); + } + + Connection conn; + switch (readFrom) { + case REPLICA: + conn = getSlaveResource(); + if (conn == null) { + throw new JedisException("all replica is invalid"); + } + return conn; + case UPSTREAM_PREFERRED: + try { + conn = pool.getResource(); + if (conn != null) { + return conn; + } + } catch (Exception e) { + LOG.error("get master connection error", e); + } + + conn = getSlaveResource(); + if (conn == null) { + throw new JedisException("all redis instance is invalid"); + } + return conn; + case REPLICA_PREFERRED: + conn = getSlaveResource(); + if (conn != null) { + return conn; + } + return pool.getResource(); + default: + return pool.getResource(); + } } @Override @@ -130,6 +245,10 @@ public void close() { sentinelListeners.forEach(SentinelListener::shutdown); pool.close(); + + for (PoolInfo slavePool : slavePools) { + slavePool.pool.close(); + } } public HostAndPort getCurrentMaster() { @@ -180,6 +299,88 @@ private ConnectionPool createNodePool(HostAndPort master) { } } + private void initSlaves(List slaves) { + List removedSlavePools = new ArrayList<>(); + slavePoolsLock.lock(); + try { + for (int i = slavePools.size()-1; i >= 0; i--) { + PoolInfo poolInfo = slavePools.get(i); + boolean found = false; + for (HostAndPort slave : slaves) { + String host = slave.toString(); + if (poolInfo.host.equals(host)) { + found = true; + break; + } + } + if (!found) { + removedSlavePools.add(slavePools.remove(i)); + } + } + + for (HostAndPort slave : slaves) { + addSlave(slave); + } + } finally { + slavePoolsLock.unlock(); + if (!removedSlavePools.isEmpty() && clientSideCache != null) { + clientSideCache.flush(); + } + + for (PoolInfo removedSlavePool : removedSlavePools) { + removedSlavePool.pool.destroy(); + } + } + } + + private static boolean isHealthy(String flags) { + for (String flag : flags.split(",")) { + switch (flag.trim()) { + case "s_down": + case "o_down": + case "disconnected": + return false; + } + } + return true; + } + + private void addSlave(HostAndPort slave) { + String newSlaveHost = slave.toString(); + slavePoolsLock.lock(); + try { + for (int i = 0; i < this.slavePools.size(); i++) { + PoolInfo poolInfo = this.slavePools.get(i); + if (poolInfo.host.equals(newSlaveHost)) { + return; + } + } + slavePools.add(new PoolInfo(newSlaveHost, createNodePool(slave))); + } finally { + slavePoolsLock.unlock(); + } + } + + private void removeSlave(HostAndPort slave) { + String newSlaveHost = slave.toString(); + PoolInfo removed = null; + slavePoolsLock.lock(); + try { + for (int i = 0; i < this.slavePools.size(); i++) { + PoolInfo poolInfo = this.slavePools.get(i); + if (poolInfo.host.equals(newSlaveHost)) { + removed = slavePools.remove(i); + break; + } + } + } finally { + slavePoolsLock.unlock(); + } + if (removed != null) { + removed.pool.destroy(); + } + } + private HostAndPort initSentinels(Set sentinels) { HostAndPort master = null; @@ -275,6 +476,24 @@ public void run() { sentinelJedis = new Jedis(node, sentinelClientConfig); + List> slaveInfos = sentinelJedis.sentinelSlaves(masterName); + + List slaves = new ArrayList<>(); + + for (int i = 0; i < slaveInfos.size(); i++) { + Map slaveInfo = slaveInfos.get(i); + String flags = slaveInfo.get("flags"); + if (flags == null || !isHealthy(flags)) { + continue; + } + String ip = slaveInfo.get("ip"); + int port = Integer.parseInt(slaveInfo.get("port")); + HostAndPort slave = new HostAndPort(ip, port); + slaves.add(slave); + } + + initSlaves(slaves); + // code for active refresh List masterAddr = sentinelJedis.sentinelGetMasterAddrByName(masterName); if (masterAddr == null || masterAddr.size() != 2) { @@ -286,26 +505,69 @@ public void run() { sentinelJedis.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { - LOG.debug("Sentinel {} published: {}.", node, message); - - String[] switchMasterMsg = message.split(" "); - - if (switchMasterMsg.length > 3) { - - if (masterName.equals(switchMasterMsg[0])) { - initMaster(toHostAndPort(switchMasterMsg[3], switchMasterMsg[4])); - } else { - LOG.debug( - "Ignoring message on +switch-master for master {}. Our master is {}.", - switchMasterMsg[0], masterName); - } - - } else { - LOG.error("Invalid message received on sentinel {} on channel +switch-master: {}.", - node, message); + LOG.debug("Sentinel {} with channel {} published: {}.", node, channel, message); + + String[] switchMsg = message.split(" "); + String slaveIp; + int slavePort; + switch (channel) { + case "+switch-master": + if (switchMsg.length > 3) { + if (masterName.equals(switchMsg[0])) { + initMaster(toHostAndPort(switchMsg[3], switchMsg[4])); + } else { + LOG.debug( + "Ignoring message on +switch-master for master {}. Our master is {}.", + switchMsg[0], masterName); + } + } else { + LOG.error("Invalid message received on sentinel {} on channel +switch-master: {}.", + node, message); + } + break; + case "+sdown": + if (switchMsg.length < 6) { + return; + } + if (switchMsg[0].equals("master")) { + return; + } + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + removeSlave(new HostAndPort(slaveIp, slavePort)); + break; + case "-sdown": + if (switchMsg.length < 6) { + return; + } + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + addSlave(new HostAndPort(slaveIp, slavePort)); + break; + case "+slave": + if (switchMsg.length < 8) { + return; + } + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + addSlave(new HostAndPort(slaveIp, slavePort)); + + String masterIp = switchMsg[6]; + int masterPort = Integer.parseInt(switchMsg[7]); + removeSlave(new HostAndPort(masterIp, masterPort)); + break; } } - }, "+switch-master"); + }, "+switch-master", "+sdown", "-sdown", "+slave"); } catch (JedisException e) { diff --git a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java index 68309b15b0..697ed29d94 100644 --- a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java +++ b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java @@ -1,5 +1,6 @@ package redis.clients.jedis; +import java.util.ArrayList; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -8,6 +9,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Timeout; import redis.clients.jedis.exceptions.JedisConnectionException; @@ -19,6 +24,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,6 +34,8 @@ * @see JedisSentinelPoolTest */ @Tag("integration") +@RunWith(PowerMockRunner.class) +@PrepareForTest({SentineledConnectionProvider.class}) public class SentineledConnectionProviderTest { private static final String MASTER_NAME = "mymaster"; @@ -39,6 +47,8 @@ public class SentineledConnectionProviderTest { protected Set sentinels = new HashSet<>(); + protected String password = "foobared"; + @BeforeEach public void setUp() throws Exception { sentinels.clear(); @@ -52,8 +62,8 @@ public void repeatedSentinelPoolInitialization() { for (int i = 0; i < 20; ++i) { try (SentineledConnectionProvider provider = new SentineledConnectionProvider(MASTER_NAME, - DefaultJedisClientConfig.builder().timeoutMillis(1000).password("foobared").database(2).build(), - sentinels, DefaultJedisClientConfig.builder().build())) { + DefaultJedisClientConfig.builder().timeoutMillis(1000).password("foobared").database(2).build(), + sentinels, DefaultJedisClientConfig.builder().build())) { provider.getConnection().close(); } @@ -129,7 +139,7 @@ public void initializeWithNotAvailableSentinelsShouldThrowException() { wrongSentinels.add(new HostAndPort("localhost", 65431)); assertThrows(JedisConnectionException.class, () -> { try (SentineledConnectionProvider provider = new SentineledConnectionProvider(MASTER_NAME, - DefaultJedisClientConfig.builder().build(), wrongSentinels, DefaultJedisClientConfig.builder().build())) { + DefaultJedisClientConfig.builder().build(), wrongSentinels, DefaultJedisClientConfig.builder().build())) { } }); } @@ -239,4 +249,94 @@ public void testResetValidPassword() { } } } + + @Test + public void testReadWriteSeparation() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build())) { + + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertEquals("bar", jedis.get("foo")); + } + } + + @Test + public void testReadFromREPLICAAndNoSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA)) { + + Thread.sleep(1000); + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertThrows(JedisException.class, () -> jedis.get("foo")); + } + } + + @Test + public void testFallbackTOMasterWhenNOSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA_PREFERRED)) { + + Thread.sleep(1000); + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertDoesNotThrow(() -> jedis.get("foo")); + } + } + + @Test + public void testAllWriteCommandsWhenNOSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA_PREFERRED, command -> false)) { + + Thread.sleep(1000); + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertDoesNotThrow(() -> jedis.get("foo")); + } + } + + @Test + public void testCreateJedisSentineledWithBuilder() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + JedisSentineled jedis = JedisSentineled.builder() + .masterName(MASTER_NAME) + .clientConfig(DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build()) + .readForm(ReadFrom.REPLICA_PREFERRED) + .sentinels(sentinels) + .build(); + + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertDoesNotThrow(() -> jedis.get("foo")); + + } } diff --git a/src/test/java/redis/clients/jedis/builders/JedisSentineledConstructorReflectionTest.java b/src/test/java/redis/clients/jedis/builders/JedisSentineledConstructorReflectionTest.java index 6e0346fb59..0f759683b8 100644 --- a/src/test/java/redis/clients/jedis/builders/JedisSentineledConstructorReflectionTest.java +++ b/src/test/java/redis/clients/jedis/builders/JedisSentineledConstructorReflectionTest.java @@ -21,6 +21,7 @@ import redis.clients.jedis.executors.CommandExecutor; import redis.clients.jedis.providers.ConnectionProvider; import redis.clients.jedis.providers.SentineledConnectionProvider; +import redis.clients.jedis.util.ReadOnlyCommands; /** * Reflection-based coverage test for JedisSentineled constructors against builder API. @@ -85,6 +86,12 @@ void testConstructorParameterCoverageReport() { } else if (t == CacheConfig.class) { paramCovered[i] = true; paramCoverageBy[i] = "JedisSentineled.builder().cacheConfig(...)"; + } else if (t == ReadFrom.class) { + paramCovered[i] = true; + paramCoverageBy[i] = "JedisSentineled.builder().readForm(...)"; + } else if (t == ReadOnlyCommands.ReadOnlyPredicate.class) { + paramCovered[i] = true; + paramCoverageBy[i] = "DefaultJedisClientConfig.builder().readOnlyPredicate(...)"; } else if (t == int.class || t == Integer.class) { String lname = name.toLowerCase(); if (lname.contains("db") || lname.contains("database")) { diff --git a/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java index fa4043799e..87e7940471 100644 --- a/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java +++ b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java @@ -145,7 +145,7 @@ public void immutableCacheEntriesTest() { } @Test - public void invalidationTest() { + public void invalidationTest() throws InterruptedException { try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { Cache cache = jedis.getCache(); jedis.set("{csc}1", "one"); @@ -161,6 +161,7 @@ public void invalidationTest() { assertEquals(0, cache.getStats().getInvalidationCount()); jedis.set("{csc}1", "new-one"); + Thread.sleep(1000); List reply2 = jedis.mget("{csc}1", "{csc}2", "{csc}3"); assertEquals(Arrays.asList("new-one", "two", "three"), reply2);