// Copyright (C) 2011 - Will Glozer. All rights reserved. package com.lambdaworks.redis.pubsub; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; import static org.hamcrest.CoreMatchers.hasItem; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.lambdaworks.Wait; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.async.RedisAsyncCommands; import com.lambdaworks.redis.internal.LettuceFactories; import com.lambdaworks.redis.pubsub.api.async.RedisPubSubAsyncCommands; public class PubSubCommandTest extends AbstractRedisClientTest implements RedisPubSubListener<String, String> { private RedisPubSubAsyncCommands<String, String> pubsub; private BlockingQueue<String> channels; private BlockingQueue<String> patterns; private BlockingQueue<String> messages; private BlockingQueue<Long> counts; private String channel = "channel0"; private String pattern = "channel*"; private String message = "msg!"; @Before public void openPubSubConnection() throws Exception { pubsub = client.connectPubSub().async(); pubsub.addListener(this); channels = LettuceFactories.newBlockingQueue(); patterns = LettuceFactories.newBlockingQueue(); messages = LettuceFactories.newBlockingQueue(); counts = LettuceFactories.newBlockingQueue(); } @After public void closePubSubConnection() throws Exception { pubsub.close(); } @Test public void auth() throws Exception { new WithPasswordRequired() { @Override protected void run(RedisClient client) throws Exception { RedisPubSubAsyncCommands<String, String> connection = client.connectPubSub().async(); connection.addListener(PubSubCommandTest.this); connection.auth(passwd); connection.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); } }; } @Test public void authWithReconnect() throws Exception { new WithPasswordRequired() { @Override protected void run(RedisClient client) throws Exception { RedisPubSubAsyncCommands<String, String> connection = client.connectPubSub().async(); connection.addListener(PubSubCommandTest.this); connection.auth(passwd); connection.quit(); Wait.untilTrue(() -> { return !connection.isOpen(); }).waitOrTimeout(); connection.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); } }; } @Test(timeout = 2000) public void message() throws Exception { pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); } @Test(timeout = 2000) public void pipelinedMessage() throws Exception { pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); RedisAsyncCommands<String, String> connection = client.connectAsync(); connection.setAutoFlushCommands(false); connection.publish(channel, message); Thread.sleep(100); assertThat(channels).isEmpty(); connection.flushCommands(); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); connection.close(); } @Test(timeout = 2000) public void pmessage() throws Exception { pubsub.psubscribe(pattern).await(1, TimeUnit.MINUTES); assertThat(patterns.take()).isEqualTo(pattern); redis.publish(channel, message); assertThat(patterns.take()).isEqualTo(pattern); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); redis.publish("channel2", "msg 2!"); assertThat(patterns.take()).isEqualTo(pattern); assertThat(channels.take()).isEqualTo("channel2"); assertThat(messages.take()).isEqualTo("msg 2!"); } @Test(timeout = 2000) public void pipelinedSubscribe() throws Exception { pubsub.setAutoFlushCommands(false); pubsub.subscribe(channel); Thread.sleep(100); assertThat(channels).isEmpty(); pubsub.flushCommands(); assertThat(channels.take()).isEqualTo(channel); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); } @Test(timeout = 2000) public void psubscribe() throws Exception { RedisFuture<Void> psubscribe = pubsub.psubscribe(pattern); assertThat(psubscribe.get()).isNull(); assertThat(psubscribe.getError()).isNull(); assertThat(psubscribe.isCancelled()).isFalse(); assertThat(psubscribe.isDone()).isTrue(); assertThat(patterns.take()).isEqualTo(pattern); assertThat((long) counts.take()).isEqualTo(1); } @Test(timeout = 2000) public void psubscribeWithListener() throws Exception { RedisFuture<Void> psubscribe = pubsub.psubscribe(pattern); final List<Object> listener = new ArrayList<>(); psubscribe.thenAccept(aVoid -> listener.add("done")); psubscribe.await(1, TimeUnit.MINUTES); assertThat(patterns.take()).isEqualTo(pattern); assertThat((long) counts.take()).isEqualTo(1); assertThat(listener).hasSize(1); } @Test(expected = IllegalArgumentException.class) public void pubsubEmptyChannels() throws Exception { pubsub.subscribe(); fail("Missing IllegalArgumentException: channels must not be empty"); } @Test public void pubsubChannels() throws Exception { RedisFuture<Void> future = pubsub.subscribe(channel); future.get(1, TimeUnit.MINUTES); List<String> result = redis.pubsubChannels(); assertThat(result).contains(channel); } @Test public void pubsubMultipleChannels() throws Exception { RedisFuture<Void> future = pubsub.subscribe(channel, "channel1", "channel3"); future.get(); List<String> result = redis.pubsubChannels(); assertThat(result).contains(channel, "channel1", "channel3"); } @Test public void pubsubChannelsWithArg() throws Exception { pubsub.subscribe(channel).get(); List<String> result = redis.pubsubChannels(pattern); assertThat(result, hasItem(channel)); } @Test public void pubsubNumsub() throws Exception { pubsub.subscribe(channel); Thread.sleep(100); Map<String, Long> result = redis.pubsubNumsub(channel); assertThat(result.size()).isGreaterThan(0); assertThat(result.get(channel)).isGreaterThan(0); // Redis sometimes keeps old references } @Test public void pubsubNumpat() throws Exception { pubsub.psubscribe(pattern).get(); Long result = redis.pubsubNumpat(); assertThat(result.longValue()).isGreaterThan(0); // Redis sometimes keeps old references } @Test public void punsubscribe() throws Exception { pubsub.punsubscribe(pattern).get(); assertThat(patterns.take()).isEqualTo(pattern); assertThat((long) counts.take()).isEqualTo(0); } @Test(timeout = 2000) public void subscribe() throws Exception { pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); assertThat((long) counts.take()).isEqualTo(1); } @Test(timeout = 2000) public void unsubscribe() throws Exception { pubsub.unsubscribe(channel).get(); assertThat(channels.take()).isEqualTo(channel); assertThat((long) counts.take()).isEqualTo(0); RedisFuture<Void> future = pubsub.unsubscribe(); assertThat(future.get()).isNull(); assertThat(future.getError()).isNull(); assertThat(channels).isEmpty(); assertThat(patterns).isEmpty(); } @Test public void pubsubCloseOnClientShutdown() throws Exception { RedisClient redisClient = RedisClient.create(RedisURI.Builder.redis(host, port).build()); RedisPubSubAsyncCommands<String, String> connection = redisClient.connectPubSub().async(); FastShutdown.shutdown(redisClient); assertThat(connection.isOpen()).isFalse(); } @Test(timeout = 2000) public void utf8Channel() throws Exception { String channel = "channelλ"; String message = "αβγ"; pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); } @Test(timeout = 10000) public void resubscribeChannelsOnReconnect() throws Exception { pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); assertThat((long) counts.take()).isEqualTo(1); pubsub.quit(); assertThat(channels.take()).isEqualTo(channel); assertThat((long) counts.take()).isEqualTo(1); Wait.untilTrue(pubsub::isOpen).waitOrTimeout(); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); } @Test(timeout = 10000) public void resubscribePatternsOnReconnect() throws Exception { pubsub.psubscribe(pattern); assertThat(patterns.take()).isEqualTo(pattern); assertThat((long) counts.take()).isEqualTo(1); pubsub.quit(); assertThat(patterns.take()).isEqualTo(pattern); assertThat((long) counts.take()).isEqualTo(1); Wait.untilTrue(pubsub::isOpen).waitOrTimeout(); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); } @Test(timeout = 2000) public void adapter() throws Exception { final BlockingQueue<Long> localCounts = LettuceFactories.newBlockingQueue(); RedisPubSubAdapter<String, String> adapter = new RedisPubSubAdapter<String, String>() { @Override public void subscribed(String channel, long count) { super.subscribed(channel, count); localCounts.add(count); } @Override public void unsubscribed(String channel, long count) { super.unsubscribed(channel, count); localCounts.add(count); } }; pubsub.addListener(adapter); pubsub.subscribe(channel); pubsub.psubscribe(pattern); assertThat((long) localCounts.take()).isEqualTo(1L); redis.publish(channel, message); pubsub.punsubscribe(pattern); pubsub.unsubscribe(channel); assertThat((long) localCounts.take()).isEqualTo(0L); } @Test(timeout = 2000) public void removeListener() throws Exception { pubsub.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); redis.publish(channel, message); assertThat(channels.take()).isEqualTo(channel); assertThat(messages.take()).isEqualTo(message); pubsub.removeListener(this); redis.publish(channel, message); assertThat(channels.poll(10, TimeUnit.MILLISECONDS)).isNull(); assertThat(messages.poll(10, TimeUnit.MILLISECONDS)).isNull(); } // RedisPubSubListener implementation @Override public void message(String channel, String message) { channels.add(channel); messages.add(message); } @Override public void message(String pattern, String channel, String message) { patterns.add(pattern); channels.add(channel); messages.add(message); } @Override public void subscribed(String channel, long count) { channels.add(channel); counts.add(count); } @Override public void psubscribed(String pattern, long count) { patterns.add(pattern); counts.add(count); } @Override public void unsubscribed(String channel, long count) { channels.add(channel); counts.add(count); } @Override public void punsubscribed(String pattern, long count) { patterns.add(pattern); counts.add(count); } }