Add a cluster-backed message cache.

This commit is contained in:
Jon Chambers
2020-07-09 09:34:20 -04:00
committed by Jon Chambers
parent 639898ec07
commit 6fc1b4c6c0
15 changed files with 690 additions and 59 deletions

View File

@@ -36,7 +36,7 @@ public abstract class AbstractRedisClusterTest {
private FaultTolerantRedisCluster redisCluster;
@BeforeClass
public static void setUpBeforeClass() throws IOException, URISyntaxException, InterruptedException {
public static void setUpBeforeClass() throws Exception {
assumeFalse(System.getProperty("os.name").equalsIgnoreCase("windows"));
clusterNodes = new RedisServer[NODE_COUNT];
@@ -50,7 +50,7 @@ public abstract class AbstractRedisClusterTest {
}
@Before
public void setUp() {
public void setUp() throws Exception {
final List<String> urls = Arrays.stream(clusterNodes)
.map(node -> String.format("redis://127.0.0.1:%d", node.ports().get(0)))
.collect(Collectors.toList());
@@ -63,12 +63,12 @@ public abstract class AbstractRedisClusterTest {
}
@After
public void tearDown() {
public void tearDown() throws Exception {
redisCluster.stop();
}
@AfterClass
public static void tearDownAfterClass() {
public static void tearDownAfterClass() throws Exception {
for (final RedisServer node : clusterNodes) {
node.stop();
}

View File

@@ -0,0 +1,157 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.protobuf.ByteString;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(JUnitParamsRunner.class)
public abstract class AbstractMessagesCacheTest extends AbstractRedisClusterTest {
private static final String DESTINATION_ACCOUNT = "+18005551234";
private static final int DESTINATION_DEVICE_ID = 7;
private final Random random = new Random();
private long serialTimestamp = 0;
protected abstract UserMessagesCache getMessagesCache();
@Test
@Parameters({"true", "false"})
public void testInsert(final boolean sealedSender) {
final UUID messageGuid = UUID.randomUUID();
assertTrue(getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, generateRandomMessage(messageGuid, sealedSender)) > 0);
}
@Test
@Parameters({"true", "false"})
public void testRemoveById(final boolean sealedSender) {
final UUID messageGuid = UUID.randomUUID();
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
final long messageId = getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message);
final Optional<OutgoingMessageEntity> maybeRemovedMessage = getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageId);
assertTrue(maybeRemovedMessage.isPresent());
assertEquals(UserMessagesCache.constructEntityFromEnvelope(messageId, message), maybeRemovedMessage.get());
assertEquals(Optional.empty(), getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageId));
}
@Test
public void testRemoveBySender() {
final UUID messageGuid = UUID.randomUUID();
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, false);
getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message);
final Optional<OutgoingMessageEntity> maybeRemovedMessage = getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message.getSource(), message.getTimestamp());
assertTrue(maybeRemovedMessage.isPresent());
assertEquals(UserMessagesCache.constructEntityFromEnvelope(0, message), maybeRemovedMessage.get());
assertEquals(Optional.empty(), getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message.getSource(), message.getTimestamp()));
}
@Test
@Parameters({"true", "false"})
public void testRemoveByUUID(final boolean sealedSender) {
final UUID messageGuid = UUID.randomUUID();
assertEquals(Optional.empty(), getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageGuid));
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message);
final Optional<OutgoingMessageEntity> maybeRemovedMessage = getMessagesCache().remove(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageGuid);
assertTrue(maybeRemovedMessage.isPresent());
assertEquals(UserMessagesCache.constructEntityFromEnvelope(0, message), maybeRemovedMessage.get());
}
@Test
@Parameters({"true", "false"})
public void testGetMessages(final boolean sealedSender) {
final int messageCount = 100;
final List<OutgoingMessageEntity> expectedMessages = new ArrayList<>(messageCount);
for (int i = 0; i < messageCount; i++) {
final UUID messageGuid = UUID.randomUUID();
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
final long messageId = getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, message);
expectedMessages.add(UserMessagesCache.constructEntityFromEnvelope(messageId, message));
}
assertEquals(expectedMessages, getMessagesCache().get(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageCount));
}
@Test
@Parameters({"true", "false"})
public void testClearQueueForDevice(final boolean sealedSender) {
final int messageCount = 100;
for (final int deviceId : new int[] { DESTINATION_DEVICE_ID, DESTINATION_DEVICE_ID + 1 }) {
for (int i = 0; i < messageCount; i++) {
final UUID messageGuid = UUID.randomUUID();
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, deviceId, message);
}
}
getMessagesCache().clear(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID);
assertEquals(Collections.emptyList(), getMessagesCache().get(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageCount));
assertEquals(messageCount, getMessagesCache().get(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID + 1, messageCount).size());
}
@Test
@Parameters({"true", "false"})
public void testClearQueueForAccount(final boolean sealedSender) {
final int messageCount = 100;
for (final int deviceId : new int[] { DESTINATION_DEVICE_ID, DESTINATION_DEVICE_ID + 1 }) {
for (int i = 0; i < messageCount; i++) {
final UUID messageGuid = UUID.randomUUID();
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
getMessagesCache().insert(messageGuid, DESTINATION_ACCOUNT, deviceId, message);
}
}
getMessagesCache().clear(DESTINATION_ACCOUNT);
assertEquals(Collections.emptyList(), getMessagesCache().get(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, messageCount));
assertEquals(Collections.emptyList(), getMessagesCache().get(DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID + 1, messageCount));
}
protected MessageProtos.Envelope generateRandomMessage(final UUID messageGuid, final boolean sealedSender) {
final MessageProtos.Envelope.Builder envelopeBuilder = MessageProtos.Envelope.newBuilder()
.setTimestamp(serialTimestamp++)
.setServerTimestamp(serialTimestamp++)
.setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256)))
.setType(MessageProtos.Envelope.Type.CIPHERTEXT)
.setServerGuid(messageGuid.toString());
if (!sealedSender) {
envelopeBuilder.setSourceDevice(random.nextInt(256))
.setSource("+1" + RandomStringUtils.randomNumeric(10));
}
return envelopeBuilder.build();
}
}

View File

@@ -0,0 +1,41 @@
package org.whispersystems.textsecuregcm.storage;
import org.junit.After;
import org.junit.Before;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import redis.embedded.RedisServer;
import java.util.List;
import static org.mockito.Mockito.mock;
public class MessagesCacheTest extends AbstractMessagesCacheTest {
private RedisServer redisServer;
private MessagesCache messagesCache;
@Before
public void setUp() throws Exception {
redisServer = new RedisServer(AbstractRedisClusterTest.getNextRedisClusterPort());
redisServer.start();
final String redisUrl = String.format("redis://127.0.0.1:%d", redisServer.ports().get(0));
final RedisClientFactory clientFactory = new RedisClientFactory("message-cache-test", redisUrl, List.of(redisUrl), new CircuitBreakerConfiguration());
final ReplicatedJedisPool jedisPool = clientFactory.getRedisClientPool();
messagesCache = new MessagesCache(jedisPool, mock(Messages.class), mock(AccountsManager.class), 60, mock(RedisClusterMessagesCache.class));
}
@After
public void tearDown() {
redisServer.stop();
}
@Override
protected UserMessagesCache getMessagesCache() {
return messagesCache;
}
}

View File

@@ -0,0 +1,48 @@
package org.whispersystems.textsecuregcm.storage;
import junitparams.Parameters;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
public class RedisClusterMessagesCacheTest extends AbstractMessagesCacheTest {
private static final String DESTINATION_ACCOUNT = "+18005551234";
private static final int DESTINATION_DEVICE_ID = 7;
private RedisClusterMessagesCache messagesCache;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
try {
messagesCache = new RedisClusterMessagesCache(getRedisCluster());
} catch (final IOException e) {
throw new RuntimeException(e);
}
getRedisCluster().useWriteCluster(connection -> connection.sync().flushall());
}
@Override
protected UserMessagesCache getMessagesCache() {
return messagesCache;
}
@Test
@Parameters({"true", "false"})
public void testInsertWithPrescribedId(final boolean sealedSender) {
final UUID firstMessageGuid = UUID.randomUUID();
final UUID secondMessageGuid = UUID.randomUUID();
final long messageId = 74;
assertEquals(messageId, messagesCache.insert(firstMessageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, generateRandomMessage(firstMessageGuid, sealedSender), messageId));
assertEquals(messageId + 1, messagesCache.insert(secondMessageGuid, DESTINATION_ACCOUNT, DESTINATION_DEVICE_ID, generateRandomMessage(secondMessageGuid, sealedSender)));
}
}