Refactor account locks/deleted account manager

This commit is contained in:
Jon Chambers 2023-06-05 12:30:44 -04:00 committed by GitHub
parent e6917d8427
commit 085c7a67c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 253 additions and 322 deletions

View File

@ -169,11 +169,11 @@ import org.whispersystems.textsecuregcm.storage.AccountCleaner;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.Keys;
@ -506,10 +506,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
messageDeletionAsyncExecutor);
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
deletedAccountsLockDynamoDbClient, config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, keys, messagesManager, profilesManager,
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client,
clientPresenceManager,
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
@ -744,7 +743,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, reportSpamTokenProvider),
new PaymentsController(currencyManager, paymentsCredentialsGenerator),

View File

@ -93,7 +93,7 @@ import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
@ -117,7 +117,7 @@ public class MessageController {
private final MessageSender messageSender;
private final ReceiptSender receiptSender;
private final AccountsManager accountsManager;
private final DeletedAccountsManager deletedAccountsManager;
private final DeletedAccounts deletedAccounts;
private final MessagesManager messagesManager;
private final PushNotificationManager pushNotificationManager;
private final ReportMessageManager reportMessageManager;
@ -150,7 +150,7 @@ public class MessageController {
MessageSender messageSender,
ReceiptSender receiptSender,
AccountsManager accountsManager,
DeletedAccountsManager deletedAccountsManager,
DeletedAccounts deletedAccounts,
MessagesManager messagesManager,
PushNotificationManager pushNotificationManager,
ReportMessageManager reportMessageManager,
@ -161,7 +161,7 @@ public class MessageController {
this.messageSender = messageSender;
this.receiptSender = receiptSender;
this.accountsManager = accountsManager;
this.deletedAccountsManager = deletedAccountsManager;
this.deletedAccounts = deletedAccounts;
this.messagesManager = messagesManager;
this.pushNotificationManager = pushNotificationManager;
this.reportMessageManager = reportMessageManager;
@ -621,7 +621,7 @@ public class MessageController {
sourceAci = maybeAccount.map(Account::getUuid);
sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier);
} else {
sourceAci = deletedAccountsManager.findDeletedAccountAci(source);
sourceAci = deletedAccounts.findUuid(source);
sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source));
}
} else {
@ -631,7 +631,7 @@ public class MessageController {
if (sourceAccount.isEmpty()) {
logger.warn("Could not find source: {}", sourceAci.get());
sourceNumber = deletedAccountsManager.findDeletedAccountE164(sourceAci.get());
sourceNumber = deletedAccounts.findE164(sourceAci.get());
sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier);
} else {
sourceNumber = sourceAccount.map(Account::getNumber);

View File

@ -0,0 +1,66 @@
package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.AcquireLockOptions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions;
import com.amazonaws.services.dynamodbv2.LockItem;
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class AccountLockManager {
private final AmazonDynamoDBLockClient lockClient;
public AccountLockManager(final AmazonDynamoDB lockDynamoDb, final String lockTableName) {
this(new AmazonDynamoDBLockClient(
AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName)
.withPartitionKeyName(DeletedAccounts.KEY_ACCOUNT_E164)
.withLeaseDuration(15L)
.withHeartbeatPeriod(2L)
.withTimeUnit(TimeUnit.SECONDS)
.withCreateHeartbeatBackgroundThread(true)
.build()));
}
@VisibleForTesting
AccountLockManager(final AmazonDynamoDBLockClient lockClient) {
this.lockClient = lockClient;
}
/**
* Acquires a distributed, pessimistic lock for the accounts identified by the given phone numbers. By design, the
* accounts need not actually exist in order to acquire a lock; this allows lock acquisition for operations that span
* account lifecycle changes (like deleting an account or changing a phone number). The given task runs once locks for
* all given phone numbers have been acquired, and the locks are released as soon as the task completes by any means.
*
* @param e164s the phone numbers for which to acquire a distributed, pessimistic lock
* @param task the task to execute once locks have been acquired
*
* @throws InterruptedException if interrupted while acquiring a lock
*/
public void withLock(final List<String> e164s, final Runnable task) throws InterruptedException {
if (e164s.isEmpty()) {
throw new IllegalArgumentException("List of e164s to lock must not be empty");
}
final List<LockItem> lockItems = new ArrayList<>(e164s.size());
try {
for (final String e164 : e164s) {
lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164)
.withAcquireReleasedLocksConsistently(true)
.build()));
}
task.run();
} finally {
lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem)
.withBestEffort(true)
.build()));
}
}
}

View File

@ -87,7 +87,8 @@ public class AccountsManager {
private final Accounts accounts;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
private final FaultTolerantRedisCluster cacheCluster;
private final DeletedAccountsManager deletedAccountsManager;
private final AccountLockManager accountLockManager;
private final DeletedAccounts deletedAccounts;
private final Keys keys;
private final MessagesManager messagesManager;
private final ProfilesManager profilesManager;
@ -130,7 +131,8 @@ public class AccountsManager {
public AccountsManager(final Accounts accounts,
final PhoneNumberIdentifiers phoneNumberIdentifiers,
final FaultTolerantRedisCluster cacheCluster,
final DeletedAccountsManager deletedAccountsManager,
final AccountLockManager accountLockManager,
final DeletedAccounts deletedAccounts,
final Keys keys,
final MessagesManager messagesManager,
final ProfilesManager profilesManager,
@ -145,7 +147,8 @@ public class AccountsManager {
this.accounts = accounts;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.cacheCluster = cacheCluster;
this.deletedAccountsManager = deletedAccountsManager;
this.accountLockManager = accountLockManager;
this.deletedAccounts = deletedAccounts;
this.keys = keys;
this.messagesManager = messagesManager;
this.profilesManager = profilesManager;
@ -168,7 +171,7 @@ public class AccountsManager {
try (Timer.Context ignored = createTimer.time()) {
final Account account = new Account();
deletedAccountsManager.lockAndTake(number, maybeRecentlyDeletedUuid -> {
accountLockManager.withLock(List.of(number), () -> {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
@ -182,7 +185,10 @@ public class AccountsManager {
device.setUserAgent(signalAgent);
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
// Reuse the ACI from any recently-deleted account with this number to cover cases where somebody is
// re-registering.
account.setUuid(deletedAccounts.findUuid(number).orElseGet(UUID::randomUUID));
account.addDevice(device);
account.setRegistrationLockFromAttributes(accountAttributes);
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
@ -236,6 +242,10 @@ public class AccountsManager {
accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword));
// Clear any "recently deleted account" record for this number since, if it existed, we've used its old ACI for
// the newly-created account.
deletedAccounts.remove(number);
});
return account;
@ -243,7 +253,7 @@ public class AccountsManager {
}
public Account changeNumber(final Account account,
final String number,
final String targetNumber,
@Nullable final byte[] pniIdentityKey,
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
@ -252,7 +262,7 @@ public class AccountsManager {
final String originalNumber = account.getNumber();
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
if (originalNumber.equals(number)) {
if (originalNumber.equals(targetNumber)) {
if (pniIdentityKey != null) {
throw new IllegalArgumentException("change number must supply a changed phone number; otherwise use updatePniKeys");
}
@ -263,28 +273,42 @@ public class AccountsManager {
final AtomicReference<Account> updatedAccount = new AtomicReference<>();
deletedAccountsManager.lockAndPut(account.getNumber(), number, (originalAci, deletedAci) -> {
accountLockManager.withLock(List.of(account.getNumber(), targetNumber), () -> {
redisDelete(account);
final Optional<Account> maybeExistingAccount = getByE164(number);
final Optional<UUID> displacedUuid;
// There are three possible states for accounts associated with the target phone number:
//
// 1. An account exists with the target number; the caller has proved ownership of the number, so delete the
// account with the target number. This will leave a "deleted account" record for the deleted account mapping
// the UUID of the deleted account to the target phone number. We'll then overwrite that so it points to the
// original number to facilitate switching back and forth between numbers.
// 2. No account with the target number exists, but one has recently been deleted. In that case, add a "deleted
// account" record that maps the ACI of the recently-deleted account to the now-abandoned original phone number
// of the account changing its number (which facilitates ACI consistency in cases that a party is switching
// back and forth between numbers).
// 3. No account with the target number exists at all, in which case no additional action is needed.
final Optional<UUID> recentlyDeletedAci = deletedAccounts.findUuid(targetNumber);
final Optional<Account> maybeExistingAccount = getByE164(targetNumber);
final Optional<UUID> maybeDisplacedUuid;
if (maybeExistingAccount.isPresent()) {
delete(maybeExistingAccount.get());
displacedUuid = maybeExistingAccount.map(Account::getUuid);
maybeDisplacedUuid = maybeExistingAccount.map(Account::getUuid);
} else {
displacedUuid = deletedAci;
maybeDisplacedUuid = recentlyDeletedAci;
}
maybeDisplacedUuid.ifPresent(displacedUuid -> deletedAccounts.put(displacedUuid, originalNumber));
final UUID uuid = account.getUuid();
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(targetNumber);
final Account numberChangedAccount;
numberChangedAccount = updateWithRetries(
account,
a -> { setPniKeys(account, pniIdentityKey, pniSignedPreKeys, pniRegistrationIds); return true; },
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
a -> accounts.changeNumber(a, targetNumber, phoneNumberIdentifier),
() -> accounts.getByAccountIdentifier(uuid).orElseThrow(),
AccountChangeValidator.NUMBER_CHANGE_VALIDATOR);
@ -301,8 +325,6 @@ public class AccountsManager {
Function.identity(),
pniPqLastResortPreKeys::get)));
}
return displacedUuid;
});
return updatedAccount.get();
@ -675,10 +697,13 @@ public class AccountsManager {
public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException {
try (final Timer.Context ignored = deleteTimer.time()) {
deletedAccountsManager.lockAndPut(account.getNumber(), () -> {
accountLockManager.withLock(List.of(account.getNumber()), () -> {
final UUID accountIdentifier = account.getUuid();
final String e164 = account.getNumber();
delete(account);
return account.getUuid();
deletedAccounts.put(accountIdentifier, e164);
});
} catch (final RuntimeException | InterruptedException e) {
logger.warn("Failed to delete account", e);

View File

@ -18,7 +18,7 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
public class DeletedAccounts extends AbstractDynamoDbStore {
public class DeletedAccounts {
// e164, primary key
static final String KEY_ACCOUNT_E164 = "P";
@ -29,19 +29,16 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
// Note that this limit is imposed by DynamoDB itself; going above 100 will result in errors
static final int GET_BATCH_SIZE = 100;
private final DynamoDbClient dynamoDbClient;
private final String tableName;
public DeletedAccounts(final DynamoDbClient dynamoDb, final String tableName) {
super(dynamoDb);
public DeletedAccounts(final DynamoDbClient dynamoDbClient, final String tableName) {
this.dynamoDbClient = dynamoDbClient;
this.tableName = tableName;
}
void put(UUID uuid, String e164) {
db().putItem(PutItemRequest.builder()
public void put(UUID uuid, String e164) {
dynamoDbClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.item(Map.of(
KEY_ACCOUNT_E164, AttributeValues.fromString(e164),
@ -50,8 +47,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
.build());
}
Optional<UUID> findUuid(final String e164) {
final GetItemResponse response = db().getItem(GetItemRequest.builder()
public Optional<UUID> findUuid(final String e164) {
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.consistentRead(true)
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
@ -60,8 +57,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
return Optional.ofNullable(AttributeValues.getUUID(response.item(), ATTR_ACCOUNT_UUID, null));
}
Optional<String> findE164(final UUID uuid) {
final QueryResponse response = db().query(QueryRequest.builder()
public Optional<String> findE164(final UUID uuid) {
final QueryResponse response = dynamoDbClient.query(QueryRequest.builder()
.tableName(tableName)
.indexName(UUID_TO_E164_INDEX_NAME)
.keyConditionExpression("#uuid = :uuid")
@ -75,15 +72,14 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
}
if (response.count() > 1) {
throw new RuntimeException(
"Impossible result: more than one phone number returned for UUID: " + uuid);
throw new RuntimeException("Impossible result: more than one phone number returned for UUID: " + uuid);
}
return Optional.ofNullable(response.items().get(0).get(KEY_ACCOUNT_E164).s());
}
void remove(final String e164) {
db().deleteItem(DeleteItemRequest.builder()
public void remove(final String e164) {
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
.build());

View File

@ -1,145 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.AcquireLockOptions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions;
import com.amazonaws.services.dynamodbv2.LockItem;
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DeletedAccountsManager {
private final DeletedAccounts deletedAccounts;
private final AmazonDynamoDBLockClient lockClient;
private static final Logger log = LoggerFactory.getLogger(DeletedAccountsManager.class);
public DeletedAccountsManager(final DeletedAccounts deletedAccounts, final AmazonDynamoDB lockDynamoDb, final String lockTableName) {
this.deletedAccounts = deletedAccounts;
lockClient = new AmazonDynamoDBLockClient(
AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName)
.withPartitionKeyName(DeletedAccounts.KEY_ACCOUNT_E164)
.withLeaseDuration(15L)
.withHeartbeatPeriod(2L)
.withTimeUnit(TimeUnit.SECONDS)
.withCreateHeartbeatBackgroundThread(true)
.build());
}
/**
* Acquires a pessimistic lock for the given phone number and performs the given action, passing the UUID of the
* recently-deleted account (if any) that previously held the given number.
*
* @param e164 the phone number to lock and with which to perform an action
* @param consumer the action to take; accepts the UUID of the account that previously held the given e164, if any,
* as an argument
*
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone number
*/
public void lockAndTake(final String e164, final Consumer<Optional<UUID>> consumer) throws InterruptedException {
withLock(List.of(e164), acis -> {
try {
consumer.accept(acis.get(0));
deletedAccounts.remove(e164);
} catch (final Exception e) {
log.warn("Consumer threw an exception while holding lock on a deleted account record", e);
throw new RuntimeException(e);
}
});
}
/**
* Acquires a pessimistic lock for the given phone number and performs an action that deletes an account, returning
* the UUID of the deleted account. The UUID of the deleted account will be stored in the list of recently-deleted
* e164-to-UUID mappings.
*
* @param e164 the phone number to lock and with which to perform an action
* @param supplier the deletion action to take on the account associated with the given number; must return the UUID
* of the deleted account
*
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone number
*/
public void lockAndPut(final String e164, final Supplier<UUID> supplier) throws InterruptedException {
withLock(List.of(e164), ignored -> {
try {
deletedAccounts.put(supplier.get(), e164);
} catch (final Exception e) {
log.warn("Supplier threw an exception while holding lock on a deleted account record", e);
throw new RuntimeException(e);
}
});
}
/**
* Acquires a pessimistic lock for the given phone numbers and performs an action that may or may not delete an
* account associated with the target number. The UUID of the deleted account (if any) will be stored in the list of
* recently-deleted e164-to-UUID mappings.
*
* @param original the phone number of an existing account to lock and with which to perform an action
* @param target the phone number of an account that may or may not exist with which to perform an action
* @param function the action to take on the given phone numbers and ACIs, if known; the action may delete the account
* identified by the target number, in which case it must return the ACI of that account
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone numbers
*/
public void lockAndPut(final String original, final String target,
final BiFunction<Optional<UUID>, Optional<UUID>, Optional<UUID>> function)
throws InterruptedException {
withLock(List.of(original, target), acis -> {
try {
function.apply(acis.get(0), acis.get(1)).ifPresent(aci -> deletedAccounts.put(aci, original));
} catch (final Exception e) {
log.warn("Supplier threw an exception while holding lock on a deleted account record", e);
throw new RuntimeException(e);
}
});
}
private void withLock(final List<String> e164s, final Consumer<List<Optional<UUID>>> task)
throws InterruptedException {
final List<LockItem> lockItems = new ArrayList<>(e164s.size());
try {
final List<Optional<UUID>> previouslyDeletedUuids = new ArrayList<>(e164s.size());
for (final String e164 : e164s) {
lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164)
.withAcquireReleasedLocksConsistently(true)
.build()));
previouslyDeletedUuids.add(deletedAccounts.findUuid(e164));
}
task.accept(previouslyDeletedUuids);
} finally {
for (final LockItem lockItem : lockItems) {
lockClient.releaseLock(ReleaseLockOptions.builder(lockItem)
.withBestEffort(true)
.build());
}
}
}
public Optional<UUID> findDeletedAccountAci(final String e164) {
return deletedAccounts.findUuid(e164);
}
public Optional<String> findDeletedAccountE164(final UUID uuid) {
return deletedAccounts.findE164(uuid);
}
}

View File

@ -37,10 +37,10 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
@ -205,12 +205,10 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
configuration.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
reportMessageManager, messageDeletionExecutor);
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
deletedAccountsLockDynamoDbClient,
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, keys, messagesManager, profilesManager,
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
experimentEnrollmentManager, registrationRecoveryPasswordsManager, Clock.systemUTC());

View File

@ -31,10 +31,10 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
@ -64,7 +64,6 @@ record CommandDependencies(
ReportMessageManager reportMessageManager,
MessagesCache messagesCache,
MessagesManager messagesManager,
DeletedAccountsManager deletedAccountsManager,
StoredVerificationCodeManager pendingAccountsManager,
ClientPresenceManager clientPresenceManager,
Keys keys,
@ -190,12 +189,10 @@ record CommandDependencies(
configuration.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
reportMessageManager, messageDeletionExecutor);
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
deletedAccountsLockDynamoDbClient,
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, keys, messagesManager, profilesManager,
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
@ -205,7 +202,6 @@ record CommandDependencies(
reportMessageManager,
messagesCache,
messagesManager,
deletedAccountsManager,
pendingAccountsManager,
clientPresenceManager,
keys,

View File

@ -97,7 +97,7 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
@ -141,7 +141,7 @@ class MessageControllerTest {
private static final MessageSender messageSender = mock(MessageSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
private static final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
private static final MessagesManager messagesManager = mock(MessagesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
@ -159,7 +159,7 @@ class MessageControllerTest {
.addProvider(MultiRecipientMessageProvider.class)
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, ReportSpamTokenProvider.noop()))
.build();
@ -214,7 +214,7 @@ class MessageControllerTest {
messageSender,
receiptSender,
accountsManager,
deletedAccountsManager,
deletedAccounts,
messagesManager,
rateLimiters,
rateLimiter,
@ -634,7 +634,7 @@ class MessageControllerTest {
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountAci(senderNumber)).thenReturn(Optional.of(senderAci));
when(deletedAccounts.findUuid(senderNumber)).thenReturn(Optional.of(senderAci));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Response response =
@ -649,7 +649,7 @@ class MessageControllerTest {
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
verify(deletedAccounts, never()).findE164(any(UUID.class));
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty());
@ -684,7 +684,7 @@ class MessageControllerTest {
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Response response =
@ -699,7 +699,7 @@ class MessageControllerTest {
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
verify(deletedAccounts, never()).findE164(any(UUID.class));
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
@ -734,7 +734,7 @@ class MessageControllerTest {
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Entity<SpamReport> entity = Entity.entity(new SpamReport(new byte[3]), "application/json");
@ -753,7 +753,7 @@ class MessageControllerTest {
eq(AuthHelper.VALID_UUID),
argThat(maybeBytes -> maybeBytes.map(bytes -> Arrays.equals(bytes, new byte[3])).orElse(false)),
any());
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
verify(deletedAccounts, never()).findE164(any(UUID.class));
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
@ -792,7 +792,7 @@ class MessageControllerTest {
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Response response =

View File

@ -0,0 +1,62 @@
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class AccountLockManagerTest {
private AmazonDynamoDBLockClient lockClient;
private AccountLockManager accountLockManager;
private static final String FIRST_NUMBER = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164);
private static final String SECOND_NUMBER = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("JP"), PhoneNumberUtil.PhoneNumberFormat.E164);
@BeforeEach
void setUp() {
lockClient = mock(AmazonDynamoDBLockClient.class);
accountLockManager = new AccountLockManager(lockClient);
}
@Test
void withLock() throws InterruptedException {
accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {});
verify(lockClient, times(2)).acquireLock(any());
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
}
@Test
void withLockTaskThrowsException() throws InterruptedException {
assertThrows(RuntimeException.class, () -> accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {
throw new RuntimeException();
}));
verify(lockClient, times(2)).acquireLock(any());
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
}
@Test
void withLockEmptyList() {
final Runnable task = mock(Runnable.class);
assertThrows(IllegalArgumentException.class, () -> accountLockManager.withLock(Collections.emptyList(), () -> {}));
verify(task, never()).run();
}
}

View File

@ -80,8 +80,7 @@ class AccountsManagerChangeNumberIntegrationTest {
deletedAccounts = new DeletedAccounts(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
Tables.DELETED_ACCOUNTS.tableName());
final DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
Tables.DELETED_ACCOUNTS_LOCK.tableName());
final SecureStorageClient secureStorageClient = mock(SecureStorageClient.class);
@ -102,8 +101,7 @@ class AccountsManagerChangeNumberIntegrationTest {
accounts,
phoneNumberIdentifiers,
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
deletedAccountsManager,
mock(Keys.class),
accountLockManager, deletedAccounts, mock(Keys.class),
mock(MessagesManager.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),

View File

@ -92,13 +92,17 @@ class AccountsManagerConcurrentModificationIntegrationTest {
//noinspection unchecked
commands = mock(RedisAdvancedClusterCommands.class);
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
doAnswer(invocation -> {
//noinspection unchecked
invocation.getArgument(1, Consumer.class).accept(Optional.empty());
final Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
}).when(accountLockManager).withLock(any(), any());
final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
when(deletedAccounts.findUuid(any())).thenReturn(Optional.empty());
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
@ -108,8 +112,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
accounts,
phoneNumberIdentifiers,
RedisClusterHelper.builder().stringCommands(commands).build(),
deletedAccountsManager,
mock(Keys.class),
accountLockManager, deletedAccounts, mock(Keys.class),
mock(MessagesManager.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),

View File

@ -36,7 +36,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -71,7 +70,7 @@ class AccountsManagerTest {
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
private Accounts accounts;
private DeletedAccountsManager deletedAccountsManager;
private DeletedAccounts deletedAccounts;
private Keys keys;
private MessagesManager messagesManager;
private ProfilesManager profilesManager;
@ -94,7 +93,7 @@ class AccountsManagerTest {
@BeforeEach
void setup() throws InterruptedException {
accounts = mock(Accounts.class);
deletedAccountsManager = mock(DeletedAccountsManager.class);
deletedAccounts = mock(DeletedAccounts.class);
keys = mock(Keys.class);
messagesManager = mock(MessagesManager.class);
profilesManager = mock(ProfilesManager.class);
@ -113,11 +112,7 @@ class AccountsManagerTest {
return null;
}).when(accounts).changeNumber(any(), anyString(), any());
doAnswer(invocation -> {
//noinspection unchecked
invocation.getArgument(1, Consumer.class).accept(Optional.empty());
return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
when(deletedAccounts.findUuid(anyString())).thenReturn(Optional.empty());
final SecureStorageClient storageClient = mock(SecureStorageClient.class);
when(storageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
@ -147,11 +142,21 @@ class AccountsManagerTest {
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
when(accounts.usernameHashAvailable(any())).thenReturn(true);
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
doAnswer(invocation -> {
final Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(accountLockManager).withLock(any(), any());
accountsManager = new AccountsManager(
accounts,
phoneNumberIdentifiers,
RedisClusterHelper.builder().stringCommands(commands).build(),
deletedAccountsManager,
accountLockManager,
deletedAccounts,
keys,
messagesManager,
profilesManager,
@ -571,12 +576,7 @@ class AccountsManagerTest {
void testCreateAccountRecentlyDeleted() throws InterruptedException {
final UUID recentlyDeletedUuid = UUID.randomUUID();
doAnswer(invocation -> {
//noinspection unchecked
invocation.getArgument(1, Consumer.class).accept(Optional.of(recentlyDeletedUuid));
return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
when(deletedAccounts.findUuid(anyString())).thenReturn(Optional.of(recentlyDeletedUuid));
when(accounts.create(any())).thenReturn(true);
final String e164 = "+18005550123";
@ -634,9 +634,6 @@ class AccountsManagerTest {
@Test
void testChangePhoneNumber() throws InterruptedException, MismatchedDevicesException {
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
final String originalNumber = "+14152222222";
final String targetNumber = "+14153333333";
final UUID uuid = UUID.randomUUID();
@ -661,7 +658,7 @@ class AccountsManagerTest {
account = accountsManager.changeNumber(account, number, null, null, null, null);
assertEquals(number, account.getNumber());
verify(deletedAccountsManager, never()).lockAndPut(anyString(), anyString(), any());
verify(deletedAccounts, never()).put(any(), any());
verify(keys, never()).delete(any());
}
@ -676,15 +673,12 @@ class AccountsManagerTest {
"AccountsManager should not allow use of changeNumber with new PNI keys but without changing number");
verify(accounts, never()).update(any());
verifyNoInteractions(deletedAccountsManager);
verifyNoInteractions(deletedAccounts);
verifyNoInteractions(keys);
}
@Test
void testChangePhoneNumberExistingAccount() throws InterruptedException, MismatchedDevicesException {
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
final String originalNumber = "+14152222222";
final String targetNumber = "+14153333333";
final UUID existingAccountUuid = UUID.randomUUID();
@ -712,9 +706,6 @@ class AccountsManagerTest {
@Test
void testChangePhoneNumberWithPqKeysExistingAccount() throws InterruptedException, MismatchedDevicesException {
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
final String originalNumber = "+14152222222";
final String targetNumber = "+14153333333";
final UUID existingAccountUuid = UUID.randomUUID();
@ -799,7 +790,7 @@ class AccountsManagerTest {
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
verify(accounts).update(any());
verifyNoInteractions(deletedAccountsManager);
verifyNoInteractions(deletedAccounts);
verify(keys).delete(oldPni);
}
@ -846,7 +837,7 @@ class AccountsManagerTest {
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
verify(accounts).update(any());
verifyNoInteractions(deletedAccountsManager);
verifyNoInteractions(deletedAccounts);
verify(keys).delete(oldPni);

View File

@ -30,12 +30,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
@ -94,13 +92,17 @@ class AccountsManagerUsernameIntegrationTest {
Tables.USERNAMES.tableName(),
SCAN_PAGE_SIZE));
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
doAnswer((final InvocationOnMock invocationOnMock) -> {
@SuppressWarnings("unchecked")
Consumer<Optional<UUID>> consumer = invocationOnMock.getArgument(1, Consumer.class);
consumer.accept(Optional.empty());
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
doAnswer(invocation -> {
final Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(deletedAccountsManager).lockAndTake(any(), any());
}).when(accountLockManager).withLock(any(), any());
final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
when(deletedAccounts.findUuid(any())).thenReturn(Optional.empty());
final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName());
@ -112,7 +114,8 @@ class AccountsManagerUsernameIntegrationTest {
accounts,
phoneNumberIdentifiers,
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
deletedAccountsManager,
accountLockManager,
deletedAccounts,
mock(Keys.class),
mock(MessagesManager.class),
mock(ProfilesManager.class),

View File

@ -1,61 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
class DeletedAccountsManagerTest {
@RegisterExtension
static final DynamoDbExtension DYNAMO_DB_EXTENSION =
new DynamoDbExtension(Tables.DELETED_ACCOUNTS, Tables.DELETED_ACCOUNTS_LOCK);
private DeletedAccounts deletedAccounts;
private DeletedAccountsManager deletedAccountsManager;
@BeforeEach
void setUp() {
deletedAccounts = new DeletedAccounts(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
Tables.DELETED_ACCOUNTS.tableName());
deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
Tables.DELETED_ACCOUNTS_LOCK.tableName());
}
@Test
void testLockAndTake() throws InterruptedException {
final UUID uuid = UUID.randomUUID();
final String e164 = "+18005551234";
deletedAccounts.put(uuid, e164);
deletedAccountsManager.lockAndTake(e164, maybeUuid -> assertEquals(Optional.of(uuid), maybeUuid));
assertEquals(Optional.empty(), deletedAccounts.findUuid(e164));
}
@Test
void testLockAndTakeWithException() {
final UUID uuid = UUID.randomUUID();
final String e164 = "+18005551234";
deletedAccounts.put(uuid, e164);
assertThrows(RuntimeException.class, () -> deletedAccountsManager.lockAndTake(e164, maybeUuid -> {
assertEquals(Optional.of(uuid), maybeUuid);
throw new RuntimeException("OH NO");
}));
assertEquals(Optional.of(uuid), deletedAccounts.findUuid(e164));
}
}