From 41f61c66a36260988650122e62078ba84ee12371 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Tue, 11 Jul 2023 23:10:44 -0400 Subject: [PATCH] Add public methods for fetching accounts asynchronously --- .../storage/AccountsManager.java | 123 ++++++++- .../storage/AccountsManagerTest.java | 252 +++++++++++++++++- 2 files changed, 366 insertions(+), 9 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index 3ad4affa1..d948cceae 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -20,6 +20,7 @@ import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tags; import java.io.IOException; +import java.io.UncheckedIOException; import java.time.Clock; import java.time.Duration; import java.util.Arrays; @@ -39,6 +40,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.signal.libsignal.protocol.IdentityKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +60,6 @@ import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator; import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.Util; -import reactor.core.publisher.Flux; import reactor.core.publisher.ParallelFlux; import reactor.core.scheduler.Scheduler; @@ -89,7 +90,7 @@ public class AccountsManager { @VisibleForTesting public static final String USERNAME_EXPERIMENT_NAME = "usernames"; - private final Logger logger = LoggerFactory.getLogger(AccountsManager.class); + private static final Logger logger = LoggerFactory.getLogger(AccountsManager.class); private final Accounts accounts; private final PhoneNumberIdentifiers phoneNumberIdentifiers; @@ -686,6 +687,14 @@ public class AccountsManager { ); } + public CompletableFuture> getByE164Async(final String number) { + return checkRedisThenAccountsAsync( + getByNumberTimer, + () -> redisGetBySecondaryKeyAsync(getAccountMapKey(number), redisNumberGetTimer), + () -> accounts.getByE164Async(number) + ); + } + public Optional getByPhoneNumberIdentifier(final UUID pni) { return checkRedisThenAccounts( getByNumberTimer, @@ -694,6 +703,14 @@ public class AccountsManager { ); } + public CompletableFuture> getByPhoneNumberIdentifierAsync(final UUID pni) { + return checkRedisThenAccountsAsync( + getByNumberTimer, + () -> redisGetBySecondaryKeyAsync(getAccountMapKey(pni.toString()), redisPniGetTimer), + () -> accounts.getByPhoneNumberIdentifierAsync(pni) + ); + } + public Optional getByUsernameLinkHandle(final UUID usernameLinkHandle) { return checkRedisThenAccounts( getByUsernameLinkHandleTimer, @@ -718,6 +735,14 @@ public class AccountsManager { ); } + public CompletableFuture> getByAccountIdentifierAsync(final UUID uuid) { + return checkRedisThenAccountsAsync( + getByUuidTimer, + () -> redisGetByAccountIdentifierAsync(uuid), + () -> accounts.getByAccountIdentifierAsync(uuid) + ); + } + public UUID getPhoneNumberIdentifier(String e164) { return phoneNumberIdentifiers.getPhoneNumberIdentifier(e164); } @@ -815,6 +840,36 @@ public class AccountsManager { } } + private CompletableFuture redisSetAsync(final Account account) { + final String accountJson; + + try { + accountJson = mapper.writeValueAsString(account); + } catch (final JsonProcessingException e) { + throw new UncheckedIOException(e); + } + + return cacheCluster.withCluster(connection -> CompletableFuture.allOf( + connection.async().setex( + getAccountMapKey(account.getPhoneNumberIdentifier().toString()), CACHE_TTL_SECONDS, + account.getUuid().toString()) + .toCompletableFuture(), + + connection.async() + .setex(getAccountMapKey(account.getNumber()), CACHE_TTL_SECONDS, account.getUuid().toString()) + .toCompletableFuture(), + + connection.async().setex(getAccountEntityKey(account.getUuid()), CACHE_TTL_SECONDS, accountJson) + .toCompletableFuture(), + + account.getUsernameHash() + .map(usernameHash -> connection.async() + .setex(getUsernameHashAccountMapKey(usernameHash), CACHE_TTL_SECONDS, account.getUuid().toString()) + .toCompletableFuture()) + .orElseGet(() -> CompletableFuture.completedFuture(null)) + )); + } + private Optional checkRedisThenAccounts( final Timer overallTimer, final Supplier> resolveFromRedis, @@ -829,6 +884,23 @@ public class AccountsManager { } } + private CompletableFuture> checkRedisThenAccountsAsync( + final Timer overallTimer, + final Supplier>> resolveFromRedis, + final Supplier>> resolveFromAccounts) { + + @SuppressWarnings("resource") final Timer.Context timerContext = overallTimer.time(); + + return resolveFromRedis.get() + .thenCompose(maybeAccountFromRedis -> maybeAccountFromRedis + .map(accountFromRedis -> CompletableFuture.completedFuture(maybeAccountFromRedis)) + .orElseGet(() -> resolveFromAccounts.get() + .thenCompose(maybeAccountFromAccounts -> maybeAccountFromAccounts + .map(account -> redisSetAsync(account).thenApply(ignored -> maybeAccountFromAccounts)) + .orElseGet(() -> CompletableFuture.completedFuture(maybeAccountFromAccounts))))) + .whenComplete((ignored, throwable) -> timerContext.close()); + } + private Optional redisGetBySecondaryKey(final String secondaryKey, final Timer timer) { try (final Timer.Context ignored = timer.time()) { return Optional.ofNullable(cacheCluster.withCluster(connection -> connection.sync().get(secondaryKey))) @@ -843,12 +915,50 @@ public class AccountsManager { } } + private CompletableFuture> redisGetBySecondaryKeyAsync(final String secondaryKey, final Timer timer) { + @SuppressWarnings("resource") final Timer.Context timerContext = timer.time(); + + return cacheCluster.withCluster(connection -> connection.async().get(secondaryKey)) + .thenCompose(nullableUuid -> { + if (nullableUuid != null) { + return getByAccountIdentifierAsync(UUID.fromString(nullableUuid)); + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } + }) + .exceptionally(throwable -> { + logger.warn("Failed to retrieve account from Redis", throwable); + return Optional.empty(); + }) + .whenComplete((ignored, throwable) -> timerContext.close()) + .toCompletableFuture(); + } + private Optional redisGetByAccountIdentifier(UUID uuid) { try (Timer.Context ignored = redisUuidGetTimer.time()) { final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid))); - if (json != null) { - Account account = mapper.readValue(json, Account.class); + return parseAccountJson(json, uuid); + } catch (final RedisException e) { + logger.warn("Redis failure", e); + return Optional.empty(); + } + } + + private CompletableFuture> redisGetByAccountIdentifierAsync(final UUID uuid) { + return cacheCluster.withCluster(connection -> connection.async().get(getAccountEntityKey(uuid))) + .thenApply(accountJson -> parseAccountJson(accountJson, uuid)) + .exceptionally(throwable -> { + logger.warn("Failed to retrieve account from Redis", throwable); + return Optional.empty(); + }) + .toCompletableFuture(); + } + + private static Optional parseAccountJson(@Nullable final String accountJson, final UUID uuid) { + try { + if (StringUtils.isNotBlank(accountJson)) { + Account account = mapper.readValue(accountJson, Account.class); account.setUuid(uuid); if (account.getPhoneNumberIdentifier() == null) { @@ -859,12 +969,9 @@ public class AccountsManager { } return Optional.empty(); - } catch (IOException e) { + } catch (final IOException e) { logger.warn("Deserialization error", e); return Optional.empty(); - } catch (RedisException e) { - logger.warn("Redis failure", e); - return Optional.empty(); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index f73fba492..2d84e140d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import java.time.Clock; import java.time.Duration; @@ -45,6 +46,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -67,8 +69,10 @@ import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.tests.util.MockRedisFuture; import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) class AccountsManagerTest { private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; @@ -86,6 +90,7 @@ class AccountsManagerTest { private Map phoneNumberIdentifiersByE164; private RedisAdvancedClusterCommands commands; + private RedisAdvancedClusterAsyncCommands asyncCommands; private AccountsManager accountsManager; private static final Answer ACCOUNT_UPDATE_ANSWER = (answer) -> { @@ -108,6 +113,9 @@ class AccountsManagerTest { //noinspection unchecked commands = mock(RedisAdvancedClusterCommands.class); + //noinspection unchecked + asyncCommands = mock(RedisAdvancedClusterAsyncCommands.class); + doAnswer((Answer) invocation -> { final Account account = invocation.getArgument(0, Account.class); final String number = invocation.getArgument(1, String.class); @@ -162,7 +170,10 @@ class AccountsManagerTest { accountsManager = new AccountsManager( accounts, phoneNumberIdentifiers, - RedisClusterHelper.builder().stringCommands(commands).build(), + RedisClusterHelper.builder() + .stringCommands(commands) + .stringAsyncCommands(asyncCommands) + .build(), accountLockManager, deletedAccounts, keysManager, @@ -199,6 +210,31 @@ class AccountsManagerTest { verifyNoInteractions(accounts); } + @Test + void testGetAccountByNumberAsyncInCache() { + UUID uuid = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))) + .thenReturn(MockRedisFuture.completedFuture(uuid.toString())); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + @Test void testGetAccountByUuidInCache() { UUID uuid = UUID.randomUUID(); @@ -219,6 +255,28 @@ class AccountsManagerTest { verifyNoInteractions(accounts); } + @Test + void testGetAccountByUuidInCacheAsync() { + UUID uuid = UUID.randomUUID(); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(account.get().getUuid(), uuid); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands, times(1)).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + @Test void testGetByPniInCache() { UUID uuid = UUID.randomUUID(); @@ -241,6 +299,32 @@ class AccountsManagerTest { verifyNoInteractions(accounts); } + @Test + void testGetByPniInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::" + pni))) + .thenReturn(MockRedisFuture.completedFuture(uuid.toString())); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + @Test void testGetByUsernameHashInCache() { UUID uuid = UUID.randomUUID(); @@ -287,6 +371,32 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByNumberNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByE164Async(eq("+14152222222"))) + .thenReturn(MockRedisFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByE164Async(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByUuidNotInCache() { UUID uuid = UUID.randomUUID(); @@ -311,6 +421,32 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByUuidNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByAccountIdentifierAsync(eq(uuid))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("Account3::" + uuid)); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByAccountIdentifierAsync(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByPniNotInCache() { UUID uuid = UUID.randomUUID(); @@ -336,6 +472,33 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByPniNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::" + pni))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByPhoneNumberIdentifierAsync(pni)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByPhoneNumberIdentifierAsync(pni); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByUsernameHashNotInCache() { UUID uuid = UUID.randomUUID(); @@ -386,6 +549,34 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByNumberBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("Connection lost!"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByE164Async(eq("+14152222222"))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByE164Async(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByUuidBrokenCache() { UUID uuid = UUID.randomUUID(); @@ -410,6 +601,35 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByUuidBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("Account3::" + uuid))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("Connection lost!"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByAccountIdentifierAsync(eq(uuid))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("Account3::" + uuid)); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByAccountIdentifierAsync(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByPniBrokenCache() { UUID uuid = UUID.randomUUID(); @@ -435,6 +655,36 @@ class AccountsManagerTest { verifyNoMoreInteractions(accounts); } + @Test + void testGetAccountByPniBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::" + pni))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("OH NO"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByPhoneNumberIdentifierAsync(pni)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByPhoneNumberIdentifierAsync(pni); + verifyNoMoreInteractions(accounts); + } + @Test void testGetAccountByUsernameBrokenCache() { UUID uuid = UUID.randomUUID();