Perform cleanup operations before overwriting an existing account record

This commit is contained in:
Jon Chambers
2023-12-05 12:18:09 -05:00
committed by GitHub
parent 331bbdd4e6
commit 5f0726af8a
11 changed files with 128 additions and 44 deletions

View File

@@ -67,6 +67,7 @@ public class AccountCreationIntegrationTest {
private static final Clock CLOCK = Clock.fixed(Instant.now(), ZoneId.systemDefault());
private ExecutorService accountLockExecutor;
private ExecutorService clientPresenceExecutor;
private AccountsManager accountsManager;
private KeysManager keysManager;
@@ -99,6 +100,7 @@ public class AccountCreationIntegrationTest {
DynamoDbExtensionSchema.Tables.DELETED_ACCOUNTS.tableName());
accountLockExecutor = Executors.newSingleThreadExecutor();
clientPresenceExecutor = Executors.newSingleThreadExecutor();
final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
DynamoDbExtensionSchema.Tables.DELETED_ACCOUNTS_LOCK.tableName());
@@ -139,15 +141,20 @@ public class AccountCreationIntegrationTest {
mock(ExperimentEnrollmentManager.class),
registrationRecoveryPasswordsManager,
accountLockExecutor,
clientPresenceExecutor,
CLOCK);
}
@AfterEach
void tearDown() throws InterruptedException {
accountLockExecutor.shutdown();
clientPresenceExecutor.shutdown();
//noinspection ResultOfMethodCallIgnored
accountLockExecutor.awaitTermination(1, TimeUnit.SECONDS);
//noinspection ResultOfMethodCallIgnored
clientPresenceExecutor.awaitTermination(1, TimeUnit.SECONDS);
}
@CartesianTest

View File

@@ -64,6 +64,7 @@ class AccountsManagerChangeNumberIntegrationTest {
private ClientPresenceManager clientPresenceManager;
private ExecutorService accountLockExecutor;
private ExecutorService clientPresenceExecutor;
private AccountsManager accountsManager;
@@ -95,6 +96,7 @@ class AccountsManagerChangeNumberIntegrationTest {
Tables.DELETED_ACCOUNTS.tableName());
accountLockExecutor = Executors.newSingleThreadExecutor();
clientPresenceExecutor = Executors.newSingleThreadExecutor();
final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
Tables.DELETED_ACCOUNTS_LOCK.tableName());
@@ -136,6 +138,7 @@ class AccountsManagerChangeNumberIntegrationTest {
mock(ExperimentEnrollmentManager.class),
registrationRecoveryPasswordsManager,
accountLockExecutor,
clientPresenceExecutor,
mock(Clock.class));
}
}
@@ -143,9 +146,13 @@ class AccountsManagerChangeNumberIntegrationTest {
@AfterEach
void tearDown() throws InterruptedException {
accountLockExecutor.shutdown();
clientPresenceExecutor.shutdown();
//noinspection ResultOfMethodCallIgnored
accountLockExecutor.awaitTermination(1, TimeUnit.SECONDS);
//noinspection ResultOfMethodCallIgnored
clientPresenceExecutor.awaitTermination(1, TimeUnit.SECONDS);
}
@Test

View File

@@ -141,6 +141,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
mock(ExperimentEnrollmentManager.class),
mock(RegistrationRecoveryPasswordsManager.class),
mock(Executor.class),
mock(Executor.class),
mock(Clock.class)
);
}

View File

@@ -48,6 +48,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -134,6 +135,15 @@ class AccountsManagerTest {
profilesManager = mock(ProfilesManager.class);
clientPresenceManager = mock(ClientPresenceManager.class);
final Executor clientPresenceExecutor = mock(Executor.class);
doAnswer(invocation -> {
final Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(clientPresenceExecutor).execute(any());
//noinspection unchecked
commands = mock(RedisAdvancedClusterCommands.class);
@@ -224,6 +234,7 @@ class AccountsManagerTest {
enrollmentManager,
registrationRecoveryPasswordsManager,
mock(Executor.class),
clientPresenceExecutor,
mock(Clock.class));
}
@@ -856,7 +867,7 @@ class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.getByAccountIdentifier(uuid)).thenReturn(Optional.empty())
.thenReturn(Optional.of(account));
when(accounts.create(any(), any())).thenThrow(ContestedOptimisticLockException.class);
when(accounts.create(any(), any(), any())).thenThrow(ContestedOptimisticLockException.class);
accountsManager.update(account, a -> {
});
@@ -974,14 +985,14 @@ class AccountsManagerTest {
@Test
void testCreateFreshAccount() throws InterruptedException {
when(accounts.create(any(), any())).thenReturn(true);
when(accounts.create(any(), any(), any())).thenReturn(true);
final String e164 = "+18005550123";
final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null, true, null);
createAccount(e164, attributes);
verify(accounts).create(argThat(account -> e164.equals(account.getNumber())), any());
verify(accounts).create(argThat(account -> e164.equals(account.getNumber())), any(), any());
verifyNoInteractions(messagesManager);
verifyNoInteractions(profilesManager);
@@ -991,25 +1002,31 @@ class AccountsManagerTest {
void testReregisterAccount() throws InterruptedException {
final UUID existingUuid = UUID.randomUUID();
when(accounts.create(any(), any())).thenAnswer(invocation -> {
invocation.getArgument(0, Account.class).setUuid(existingUuid);
return false;
});
final String e164 = "+18005550123";
final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null, true, null);
when(accounts.create(any(), any(), any())).thenAnswer(invocation -> {
invocation.getArgument(0, Account.class).setUuid(existingUuid);
final BiFunction<UUID, UUID, CompletableFuture<Void>> cleanupOperation = invocation.getArgument(2);
cleanupOperation.apply(existingUuid, phoneNumberIdentifiersByE164.get(e164));
return false;
});
createAccount(e164, attributes);
assertTrue(phoneNumberIdentifiersByE164.containsKey(e164));
verify(accounts)
.create(argThat(account -> e164.equals(account.getNumber()) && existingUuid.equals(account.getUuid())), any());
.create(argThat(account -> e164.equals(account.getNumber()) && existingUuid.equals(account.getUuid())), any(), any());
verify(keysManager).delete(existingUuid);
verify(keysManager).delete(phoneNumberIdentifiersByE164.get(e164));
verify(keysManager).delete(existingUuid, true);
verify(keysManager).delete(phoneNumberIdentifiersByE164.get(e164), true);
verify(messagesManager).clear(existingUuid);
verify(profilesManager).deleteAll(existingUuid);
verify(messagesManager, times(2)).clear(existingUuid);
verify(profilesManager, times(2)).deleteAll(existingUuid);
verify(clientPresenceManager).disconnectAllPresencesForUuid(existingUuid);
}
@@ -1018,7 +1035,7 @@ class AccountsManagerTest {
final UUID recentlyDeletedUuid = UUID.randomUUID();
when(accounts.findRecentlyDeletedAccountIdentifier(anyString())).thenReturn(Optional.of(recentlyDeletedUuid));
when(accounts.create(any(), any())).thenReturn(true);
when(accounts.create(any(), any(), any())).thenReturn(true);
final String e164 = "+18005550123";
final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null, true, null);
@@ -1027,6 +1044,7 @@ class AccountsManagerTest {
verify(accounts).create(
argThat(account -> e164.equals(account.getNumber()) && recentlyDeletedUuid.equals(account.getUuid())),
any(),
any());
verifyNoInteractions(keysManager);

View File

@@ -149,6 +149,7 @@ class AccountsManagerUsernameIntegrationTest {
experimentEnrollmentManager,
mock(RegistrationRecoveryPasswordsManager.class),
mock(Executor.class),
mock(Executor.class),
mock(Clock.class));
}
@@ -312,7 +313,8 @@ class AccountsManagerUsernameIntegrationTest {
final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
account.setUsernameHash(TestRandomUtil.nextBytes(16));
accounts.create(account, ignored -> Collections.emptyList());
accounts.create(account, ignored -> Collections.emptyList(),
(ignoredAci, ignoredPni) -> CompletableFuture.completedFuture(null));
final UUID linkHandle = UUID.randomUUID();
final byte[] encryptedUsername = TestRandomUtil.nextBytes(32);

View File

@@ -209,6 +209,34 @@ class AccountsTest {
assertThat(accounts.findRecentlyDeletedAccountIdentifier(account.getNumber())).isEmpty();
}
@Test
void testStoreCleanupFailure() {
final Account existingAccount = nextRandomAccount();
createAccount(existingAccount);
verifyStoredState(existingAccount.getNumber(),
existingAccount.getUuid(),
existingAccount.getPhoneNumberIdentifier(),
existingAccount.getUsernameHash().orElse(null),
existingAccount,
true);
final CompletionException completionException = assertThrows(CompletionException.class,
() -> accounts.create(generateAccount(existingAccount.getNumber(), UUID.randomUUID(), UUID.randomUUID()),
ignored -> Collections.emptyList(),
(aci, pni) -> CompletableFuture.failedFuture(new RuntimeException("OH NO"))));
assertTrue(completionException.getCause() instanceof RuntimeException);
// If the existing account cleanup task failed, we should not overwrite the existing account record
verifyStoredState(existingAccount.getNumber(),
existingAccount.getUuid(),
existingAccount.getPhoneNumberIdentifier(),
existingAccount.getUsernameHash().orElse(null),
existingAccount,
true);
}
@Test
void testStoreMulti() {
final List<Device> devices = List.of(generateDevice(DEVICE_ID_1), generateDevice(DEVICE_ID_2));
@@ -1113,7 +1141,8 @@ class AccountsTest {
}
private boolean createAccount(final Account account) {
return accounts.create(account, ignored -> Collections.emptyList());
return accounts.create(account, ignored -> Collections.emptyList(),
(ignoredAci, ignoredPni) -> CompletableFuture.completedFuture(null));
}
private static Account nextRandomAccount() {