package com.lambdaworks.redis.reliability; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assume.assumeTrue; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import io.netty.handler.codec.EncoderException; import io.netty.util.Version; import org.junit.Before; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import com.lambdaworks.Wait; import com.lambdaworks.redis.AbstractRedisClientTest; import com.lambdaworks.redis.ClientOptions; import com.lambdaworks.redis.RedisChannelHandler; import com.lambdaworks.redis.RedisChannelWriter; import com.lambdaworks.redis.RedisConnection; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.RedisFuture; import com.lambdaworks.redis.api.sync.RedisCommands; import com.lambdaworks.redis.codec.Utf8StringCodec; import com.lambdaworks.redis.output.IntegerOutput; import com.lambdaworks.redis.output.StatusOutput; import com.lambdaworks.redis.protocol.AsyncCommand; import com.lambdaworks.redis.protocol.Command; import com.lambdaworks.redis.protocol.CommandArgs; import com.lambdaworks.redis.protocol.CommandType; import com.lambdaworks.redis.protocol.ConnectionWatchdog; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; /** * @author Mark Paluch */ @SuppressWarnings("rawtypes") public class AtMostOnceTest extends AbstractRedisClientTest { protected final Utf8StringCodec CODEC = new Utf8StringCodec(); protected String key = "key"; @Before public void before() throws Exception { client.setOptions(ClientOptions.builder().autoReconnect(false).build()); // needs to be increased on slow systems...perhaps... client.setDefaultTimeout(3, TimeUnit.SECONDS); RedisCommands<String, String> connection = client.connect().sync(); connection.flushall(); connection.flushdb(); connection.close(); } @Test public void connectionIsConnectedAfterConnect() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); assertThat(getConnectionState(getRedisChannelHandler(connection))); connection.close(); } @Test public void noReconnectHandler() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); assertThat(getHandler(RedisChannelWriter.class, getRedisChannelHandler(connection))).isNotNull(); assertThat(getHandler(ConnectionWatchdog.class, getRedisChannelHandler(connection))).isNull(); connection.close(); } @Test public void basicOperations() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); connection.set(key, "1"); assertThat(connection.get("key")).isEqualTo("1"); connection.close(); } @Test public void noBufferedCommandsAfterExecute() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); connection.set(key, "1"); assertThat(getQueue(getRedisChannelHandler(connection))).isEmpty(); assertThat(getCommandBuffer(getRedisChannelHandler(connection))).isEmpty(); connection.close(); } @Test public void commandIsExecutedOnce() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); connection.set(key, "1"); connection.incr(key); assertThat(connection.get(key)).isEqualTo("2"); connection.incr(key); assertThat(connection.get(key)).isEqualTo("3"); connection.incr(key); assertThat(connection.get(key)).isEqualTo("4"); connection.close(); } @Test public void commandNotExecutedFailsOnEncode() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); RedisChannelWriter<String, String> channelWriter = getRedisChannelHandler(connection).getChannelWriter(); connection.set(key, "1"); AsyncCommand<String, String, String> working = new AsyncCommand<>(new Command<String, String, String>(CommandType.INCR, new IntegerOutput(CODEC), new CommandArgs<String, String>(CODEC).addKey(key))); channelWriter.write(working); assertThat(working.await(2, TimeUnit.SECONDS)).isTrue(); assertThat(connection.get(key)).isEqualTo("2"); AsyncCommand<String, String, Object> command = new AsyncCommand<String, String, Object>( new Command<String, String, Object>(CommandType.INCR, new IntegerOutput(CODEC), new CommandArgs<String, String>(CODEC).addKey(key))) { @Override public void encode(ByteBuf buf) { throw new IllegalStateException("I want to break free"); } }; channelWriter.write(command); assertThat(command.await(2, TimeUnit.SECONDS)).isTrue(); assertThat(command.isCancelled()).isFalse(); assertThat(getException(command)).isInstanceOf(EncoderException.class); assertThat(getQueue(getRedisChannelHandler(connection))).isNotEmpty(); getQueue(getRedisChannelHandler(connection)).clear(); assertThat(connection.get(key)).isEqualTo("2"); assertThat(getQueue(getRedisChannelHandler(connection))).isEmpty(); assertThat(getCommandBuffer(getRedisChannelHandler(connection))).isEmpty(); connection.close(); } @Test public void commandNotExecutedChannelClosesWhileFlush() throws Exception { assumeTrue(Version.identify().get("netty-transport").artifactVersion().startsWith("4.0.2")); RedisCommands<String, String> connection = client.connect().sync(); RedisCommands<String, String> verificationConnection = client.connect().sync(); RedisChannelWriter<String, String> channelWriter = getRedisChannelHandler(connection).getChannelWriter(); connection.set(key, "1"); assertThat(verificationConnection.get(key)).isEqualTo("1"); final CountDownLatch block = new CountDownLatch(1); AsyncCommand<String, String, Object> command = new AsyncCommand<String, String, Object>(new Command<>(CommandType.INCR, new IntegerOutput(CODEC), new CommandArgs<>(CODEC).addKey(key))) { @Override public void encode(ByteBuf buf) { try { block.await(); } catch (InterruptedException e) { } super.encode(buf); } }; channelWriter.write(command); Channel channel = getChannel(getRedisChannelHandler(connection)); channel.unsafe().disconnect(channel.newPromise()); assertThat(channel.isOpen()).isFalse(); assertThat(command.isCancelled()).isFalse(); assertThat(command.isDone()).isFalse(); block.countDown(); assertThat(command.await(2, TimeUnit.SECONDS)).isTrue(); assertThat(command.isCancelled()).isFalse(); assertThat(command.isDone()).isTrue(); assertThat(verificationConnection.get(key)).isEqualTo("1"); assertThat(getQueue(getRedisChannelHandler(connection))).isEmpty(); assertThat(getCommandBuffer(getRedisChannelHandler(connection))).isEmpty(); connection.close(); } @Test public void commandFailsDuringDecode() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); RedisChannelWriter<String, String> channelWriter = getRedisChannelHandler(connection).getChannelWriter(); RedisCommands<String, String> verificationConnection = client.connect().sync(); connection.set(key, "1"); AsyncCommand<String, String, String> command = new AsyncCommand<>(new Command<>(CommandType.INCR, new StatusOutput<>( CODEC), new CommandArgs<>(CODEC).addKey(key))); channelWriter.write(command); assertThat(command.await(2, TimeUnit.SECONDS)).isTrue(); assertThat(command.isCancelled()).isFalse(); assertThat(getException(command)).isInstanceOf(IllegalStateException.class); assertThat(verificationConnection.get(key)).isEqualTo("2"); assertThat(connection.get(key)).isEqualTo("2"); connection.close(); } @Test public void noCommandsExecutedAfterConnectionIsDisconnected() throws Exception { RedisCommands<String, String> connection = client.connect().sync(); connection.quit(); Wait.untilTrue(() -> !connection.isOpen()).waitOrTimeout(); try { connection.incr(key); } catch (RedisException e) { assertThat(e).isInstanceOf(RedisException.class); } connection.close(); RedisCommands<String, String> connection2 = client.connect().sync(); connection2.quit(); try { Wait.untilTrue(() -> !connection.isOpen()).waitOrTimeout(); connection2.incr(key); } catch (Exception e) { assertThat(e).isExactlyInstanceOf(RedisException.class).hasMessageContaining("not connected"); } connection2.close(); } private Throwable getException(RedisFuture<?> command) { try { command.get(); } catch (InterruptedException e) { return e; } catch (ExecutionException e) { return e.getCause(); } return null; } private <K, V> RedisChannelHandler<K, V> getRedisChannelHandler(RedisConnection<K, V> sync) { InvocationHandler invocationHandler = Proxy.getInvocationHandler(sync); return (RedisChannelHandler<K, V>) ReflectionTestUtils.getField(invocationHandler, "connection"); } private <T> T getHandler(Class<T> handlerType, RedisChannelHandler<?, ?> channelHandler) { Channel channel = getChannel(channelHandler); return (T) channel.pipeline().get((Class) handlerType); } private Channel getChannel(RedisChannelHandler<?, ?> channelHandler) { return (Channel) ReflectionTestUtils.getField(channelHandler.getChannelWriter(), "channel"); } private Queue<?> getQueue(RedisChannelHandler<?, ?> channelHandler) { return (Queue<?>) ReflectionTestUtils.getField(channelHandler.getChannelWriter(), "queue"); } private Queue<?> getCommandBuffer(RedisChannelHandler<?, ?> channelHandler) { return (Queue<?>) ReflectionTestUtils.getField(channelHandler.getChannelWriter(), "commandBuffer"); } private String getConnectionState(RedisChannelHandler<?, ?> channelHandler) { return ReflectionTestUtils.getField(channelHandler.getChannelWriter(), "lifecycleState").toString(); } }