package com.lambdaworks.redis.protocol;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.*;
import com.lambdaworks.redis.*;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.lambdaworks.Connections;
import com.lambdaworks.Wait;
import com.lambdaworks.redis.api.async.RedisAsyncCommands;
import com.lambdaworks.redis.server.RandomResponseServer;
/**
* @author Mark Paluch
*/
public class ConnectionFailureTest extends AbstractRedisClientTest {
private RedisURI defaultRedisUri = RedisURI.Builder.redis(TestSettings.host(), TestSettings.port()).build();
/**
* Expect to run into Invalid first byte exception instead of timeout.
*
* @throws Exception
*/
@Test(timeout = 10000)
public void pingBeforeConnectFails() throws Exception {
client.setOptions(ClientOptions.builder().pingBeforeActivateConnection(true).build());
RandomResponseServer ts = getRandomResponseServer();
RedisURI redisUri = RedisURI.Builder.redis(TestSettings.host(), TestSettings.nonexistentPort())
.withTimeout(10, TimeUnit.MINUTES).build();
try {
client.connect(redisUri);
} catch (Exception e) {
assertThat(e).isExactlyInstanceOf(RedisConnectionException.class);
assertThat(e.getCause()).hasMessageContaining("Invalid first byte:");
} finally {
ts.shutdown();
}
}
/**
* Simulates a failure on reconnect by changing the port to a invalid server and triggering a reconnect. Meanwhile a command
* is fired to the connection and the watchdog is triggered afterwards to reconnect.
*
* Expectation: Command after failed reconnect contains the reconnect exception.
*
* @throws Exception
*/
@Test(timeout = 120000)
public void pingBeforeConnectFailOnReconnect() throws Exception {
ClientOptions clientOptions = ClientOptions.builder().pingBeforeActivateConnection(true)
.suspendReconnectOnProtocolFailure(true).build();
client.setOptions(clientOptions);
RandomResponseServer ts = getRandomResponseServer();
RedisURI redisUri = RedisURI.Builder.redis(TestSettings.host(), TestSettings.port()).build();
redisUri.setTimeout(5);
redisUri.setUnit(TimeUnit.SECONDS);
try {
RedisAsyncCommands<String, String> connection = client.connectAsync(redisUri);
ConnectionWatchdog connectionWatchdog = Connections.getConnectionWatchdog(connection.getStatefulConnection());
assertThat(connectionWatchdog.isListenOnChannelInactive()).isTrue();
assertThat(connectionWatchdog.isReconnectSuspended()).isFalse();
assertThat(clientOptions.isSuspendReconnectOnProtocolFailure()).isTrue();
assertThat(connectionWatchdog.getReconnectionHandler().getClientOptions()).isSameAs(clientOptions);
redisUri.setPort(TestSettings.nonexistentPort());
connection.quit();
Wait.untilTrue(() -> connectionWatchdog.isReconnectSuspended()).waitOrTimeout();
assertThat(connectionWatchdog.isListenOnChannelInactive()).isTrue();
try {
connection.info().get(1, TimeUnit.MINUTES);
} catch (ExecutionException e) {
assertThat(e).hasRootCauseExactlyInstanceOf(RedisException.class);
assertThat(e.getCause()).hasMessageStartingWith("Invalid first byte");
}
connection.close();
} finally {
ts.shutdown();
}
}
/**
* Simulates a failure on reconnect by changing the port to a invalid server and triggering a reconnect.
*
* Expectation: {@link com.lambdaworks.redis.ConnectionEvents.Reconnect} events are sent.
*
* @throws Exception
*/
@Test(timeout = 120000)
public void pingBeforeConnectFailOnReconnectShouldSendEvents() throws Exception {
client.setOptions(ClientOptions.builder().pingBeforeActivateConnection(true)
.suspendReconnectOnProtocolFailure(false).build());
RandomResponseServer ts = getRandomResponseServer();
RedisURI redisUri = RedisURI.create(defaultRedisUri.toURI());
redisUri.setTimeout(5);
redisUri.setUnit(TimeUnit.SECONDS);
try {
final BlockingQueue<ConnectionEvents.Reconnect> events = new LinkedBlockingDeque<>();
RedisAsyncCommands<String, String> connection = client.connectAsync(redisUri);
ConnectionWatchdog connectionWatchdog = Connections.getConnectionWatchdog(connection.getStatefulConnection());
ReconnectionListener reconnectionListener = new ReconnectionListener() {
@Override
public void onReconnect(ConnectionEvents.Reconnect reconnect) {
events.add(reconnect);
}
};
ReflectionTestUtils.setField(connectionWatchdog, "reconnectionListener", reconnectionListener);
redisUri.setPort(TestSettings.nonexistentPort());
connection.quit();
Wait.untilTrue(() -> events.size() > 1).waitOrTimeout();
connection.close();
ConnectionEvents.Reconnect event1 = events.take();
assertThat(event1.getAttempt()).isEqualTo(1);
ConnectionEvents.Reconnect event2 = events.take();
assertThat(event2.getAttempt()).isEqualTo(2);
} finally {
ts.shutdown();
}
}
/**
* Simulates a failure on reconnect by changing the port to a invalid server and triggering a reconnect. Meanwhile a command
* is fired to the connection and the watchdog is triggered afterwards to reconnect.
*
* Expectation: Queued commands are canceled (reset), subsequent commands contain the connection exception.
*
* @throws Exception
*/
@Test(timeout = 10000)
public void cancelCommandsOnReconnectFailure() throws Exception {
client.setOptions(
ClientOptions.builder().pingBeforeActivateConnection(true).cancelCommandsOnReconnectFailure(true).build());
RandomResponseServer ts = getRandomResponseServer();
RedisURI redisUri = RedisURI.create(defaultRedisUri.toURI());
try {
RedisAsyncCommandsImpl<String, String> connection = (RedisAsyncCommandsImpl<String, String>) client
.connectAsync(redisUri);
ConnectionWatchdog connectionWatchdog = Connections.getConnectionWatchdog(connection.getStatefulConnection());
assertThat(connectionWatchdog.isListenOnChannelInactive()).isTrue();
connectionWatchdog.setReconnectSuspended(true);
redisUri.setPort(TestSettings.nonexistentPort());
connection.quit();
Wait.untilTrue(() -> !connection.isOpen()).waitOrTimeout();
RedisFuture<String> set1 = connection.set(key, value);
RedisFuture<String> set2 = connection.set(key, value);
assertThat(set1.isDone()).isFalse();
assertThat(set1.isCancelled()).isFalse();
assertThat(connection.isOpen()).isFalse();
connectionWatchdog.setReconnectSuspended(false);
connectionWatchdog.run(null);
Thread.sleep(500);
assertThat(connection.isOpen()).isFalse();
try {
set1.get();
} catch (CancellationException e) {
assertThat(e).hasNoCause();
}
try {
set2.get();
} catch (CancellationException e) {
assertThat(e).hasNoCause();
}
try {
connection.info().get();
} catch (ExecutionException e) {
assertThat(e).hasRootCauseExactlyInstanceOf(RedisException.class);
assertThat(e.getCause()).hasMessageStartingWith("Invalid first byte");
}
connection.close();
} finally {
ts.shutdown();
}
}
/**
* Expect to disable {@link ConnectionWatchdog} when closing a broken connection.
*
* @throws Exception
*/
@Test
public void closingDisconnectedConnectionShouldDisableConnectionWatchdog() throws Exception {
client.setOptions(ClientOptions.create());
RedisURI redisUri = RedisURI.Builder.redis(TestSettings.host(), TestSettings.port())
.withTimeout(10, TimeUnit.MINUTES).build();
StatefulRedisConnection<String, String> connection = client.connect(redisUri);
ConnectionWatchdog connectionWatchdog = Connections.getConnectionWatchdog(connection);
assertThat(connectionWatchdog.isReconnectSuspended()).isFalse();
assertThat(connectionWatchdog.isListenOnChannelInactive()).isTrue();
connection.sync().ping();
redisUri.setPort(TestSettings.nonexistentPort() + 5);
connection.async().quit();
Wait.untilTrue(() -> !connection.isOpen()).waitOrTimeout();
connection.close();
assertThat(connectionWatchdog.isReconnectSuspended()).isTrue();
assertThat(connectionWatchdog.isListenOnChannelInactive()).isFalse();
}
protected RandomResponseServer getRandomResponseServer() throws InterruptedException {
RandomResponseServer ts = new RandomResponseServer();
ts.initialize(TestSettings.nonexistentPort());
return ts;
}
}