some accounts classes refactorings

This commit is contained in:
Sergey Skrobotov 2022-12-02 11:09:00 -08:00
parent d0e7579f13
commit 9cf2635528
13 changed files with 430 additions and 352 deletions

View File

@ -337,7 +337,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d")).maxThreads(16)
.workQueue(messageDeletionQueue).build();
Accounts accounts = new Accounts(dynamicConfigurationManager,
Accounts accounts = new Accounts(
dynamoDbClient,
dynamoDbAsyncClient,
config.getDynamoDbTables().getAccounts().getTableName(),

View File

@ -16,7 +16,6 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@ -25,20 +24,26 @@ import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
import javax.annotation.Nonnull;
public abstract class AbstractDynamoDbStore {
private final DynamoDbClient dynamoDbClient;
private static final int MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE = 25; // This was arbitrarily chosen and may be entirely too high.
private final Timer batchWriteItemsFirstPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "true");
private final Timer batchWriteItemsRetryPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "false");
private final Counter batchWriteItemsUnprocessed = counter(name(getClass(), "batchWriteItemsUnprocessed"));
public static final int DYNAMO_DB_MAX_BATCH_SIZE = 25; // This limit comes from Amazon Dynamo DB itself. It will reject batch writes larger than this.
public static final int RESULT_SET_CHUNK_SIZE = 100;
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final int MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE = 25; // This was arbitrarily chosen and may be entirely too high.
public static final int DYNAMO_DB_MAX_BATCH_SIZE = 25; // This limit comes from Amazon Dynamo DB itself. It will reject batch writes larger than this.
public static final int RESULT_SET_CHUNK_SIZE = 100;
private final Timer batchWriteItemsFirstPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "true");
private final Timer batchWriteItemsRetryPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "false");
private final Counter batchWriteItemsUnprocessed = counter(name(getClass(), "batchWriteItemsUnprocessed"));
private final DynamoDbClient dynamoDbClient;
public AbstractDynamoDbStore(final DynamoDbClient dynamoDbClient) {
this.dynamoDbClient = dynamoDbClient;
@ -49,18 +54,15 @@ public abstract class AbstractDynamoDbStore {
}
protected void executeTableWriteItemsUntilComplete(final Map<String, List<WriteRequest>> items) {
AtomicReference<BatchWriteItemResponse> outcome = new AtomicReference<>();
batchWriteItemsFirstPass.record(
() -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder().requestItems(items).build())));
final AtomicReference<BatchWriteItemResponse> outcome = new AtomicReference<>();
writeAndStoreOutcome(items, batchWriteItemsFirstPass, outcome);
int attemptCount = 0;
while (!outcome.get().unprocessedItems().isEmpty() && attemptCount < MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE) {
batchWriteItemsRetryPass.record(() -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder()
.requestItems(outcome.get().unprocessedItems())
.build())));
writeAndStoreOutcome(outcome.get().unprocessedItems(), batchWriteItemsRetryPass, outcome);
++attemptCount;
}
if (!outcome.get().unprocessedItems().isEmpty()) {
int totalItems = outcome.get().unprocessedItems().values().stream().mapToInt(List::size).sum();
final int totalItems = outcome.get().unprocessedItems().values().stream().mapToInt(List::size).sum();
logger.error(
"Attempt count ({}) reached max ({}}) before applying all batch writes to dynamo. {} unprocessed items remain.",
attemptCount, MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE, totalItems);
@ -68,19 +70,28 @@ public abstract class AbstractDynamoDbStore {
}
}
protected List<Map<String, AttributeValue>> scan(ScanRequest scanRequest, int max) {
@Nonnull
protected List<Map<String, AttributeValue>> scan(final ScanRequest scanRequest, final int max) {
return db().scanPaginator(scanRequest)
.items()
.stream()
.limit(max)
.collect(Collectors.toList());
.toList();
}
private void writeAndStoreOutcome(
final Map<String, List<WriteRequest>> items,
final Timer timer,
final AtomicReference<BatchWriteItemResponse> outcome) {
timer.record(
() -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder().requestItems(items).build()))
);
}
static <T> void writeInBatches(final Iterable<T> items, final Consumer<List<T>> action) {
final List<T> batch = new ArrayList<>(DYNAMO_DB_MAX_BATCH_SIZE);
for (T item : items) {
for (final T item : items) {
batch.add(item);
if (batch.size() == DYNAMO_DB_MAX_BATCH_SIZE) {

View File

@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting;
@ -18,7 +19,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -29,12 +29,14 @@ import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
@ -56,8 +58,31 @@ import software.amazon.awssdk.services.dynamodb.model.Update;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.utils.CompletableFutureUtils;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class Accounts extends AbstractDynamoDbStore {
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
private static final byte RESERVED_USERNAME_HASH_VERSION = 1;
private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
private static final Timer RESERVE_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "reserveUsername"));
private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_BY_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
private static final String CONDITIONAL_CHECK_FAILED = "ConditionalCheckFailed";
private static final String TRANSACTION_CONFLICT = "TransactionConflict";
// uuid, primary key
static final String KEY_ACCOUNT_UUID = "U";
// uuid, attribute on account table, primary key for PNI table
@ -78,45 +103,32 @@ public class Accounts extends AbstractDynamoDbStore {
static final String ATTR_TTL = "TTL";
private final Clock clock;
private final DynamoDbClient client;
private final DynamoDbAsyncClient asyncClient;
private final String phoneNumberConstraintTableName;
private final String phoneNumberIdentifierConstraintTableName;
private final String usernamesConstraintTableName;
private final String accountsTableName;
private final int scanPageSize;
private static final byte RESERVED_USERNAME_HASH_VERSION = 1;
private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
private static final Timer RESERVE_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "reserveUsername"));
private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_BY_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
@VisibleForTesting
public Accounts(
final Clock clock,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
DynamoDbClient client, DynamoDbAsyncClient asyncClient,
String accountsTableName, String phoneNumberConstraintTableName,
String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
final DynamoDbClient client,
final DynamoDbAsyncClient asyncClient,
final String accountsTableName,
final String phoneNumberConstraintTableName,
final String phoneNumberIdentifierConstraintTableName,
final String usernamesConstraintTableName,
final int scanPageSize) {
super(client);
this.clock = clock;
this.client = client;
this.asyncClient = asyncClient;
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
@ -125,105 +137,61 @@ public class Accounts extends AbstractDynamoDbStore {
this.scanPageSize = scanPageSize;
}
public Accounts(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
DynamoDbClient client, DynamoDbAsyncClient asyncClient,
String accountsTableName, String phoneNumberConstraintTableName,
String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
public Accounts(
final DynamoDbClient client,
final DynamoDbAsyncClient asyncClient,
final String accountsTableName,
final String phoneNumberConstraintTableName,
final String phoneNumberIdentifierConstraintTableName,
final String usernamesConstraintTableName,
final int scanPageSize) {
this(Clock.systemUTC(), dynamicConfigurationManager, client, asyncClient, accountsTableName,
this(Clock.systemUTC(), client, asyncClient, accountsTableName,
phoneNumberConstraintTableName, phoneNumberIdentifierConstraintTableName, usernamesConstraintTableName,
scanPageSize);
}
public boolean create(Account account) {
public boolean create(final Account account) {
return CREATE_TIMER.record(() -> {
try {
TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumberConstraintTableName)
.item(Map.of(
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid());
final AttributeValue numberAttr = AttributeValues.fromString(account.getNumber());
final AttributeValue pniUuidAttr = AttributeValues.fromUUID(account.getPhoneNumberIdentifier());
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#pni", ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final TransactWriteItem phoneNumberConstraintPut = buildConstraintTablePutIfAbsent(
phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr);
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
final TransactWriteItem phoneNumberIdentifierConstraintPut = buildConstraintTablePutIfAbsent(
phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniUuidAttr);
// Add the UAK if it's in the account
account.getUnidentifiedAccessKey()
.map(AttributeValues::fromByteArray)
.ifPresent(uak -> item.put(ATTR_UAK, uak));
TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(accountsTableName)
.item(item)
.build())
.build();
final TransactWriteItem accountPut = buildAccountPut(account, uuidAttr, numberAttr, pniUuidAttr);
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
.build();
try {
client.transactWriteItems(request);
} catch (TransactionCanceledException e) {
db().transactWriteItems(request);
} catch (final TransactionCanceledException e) {
final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
if (conditionalCheckFailed(accountCancellationReason)) {
throw new IllegalArgumentException("account identifier present with different phone number");
}
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
if (conditionalCheckFailed(phoneNumberConstraintCancellationReason)
|| conditionalCheckFailed(phoneNumberIdentifierConstraintCancellationReason)) {
// In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
// we'll be cautious here and make sure we're choosing a condition check that really failed.
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
final CancellationReason reason = conditionalCheckFailed(phoneNumberConstraintCancellationReason)
? phoneNumberConstraintCancellationReason
: phoneNumberIdentifierConstraintCancellationReason;
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
final ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
@ -235,7 +203,7 @@ public class Accounts extends AbstractDynamoDbStore {
return false;
}
if ("TransactionConflict".equals(accountCancellationReason.code())) {
if (TRANSACTION_CONFLICT.equals(accountCancellationReason.code())) {
// this should only happen if two clients manage to make concurrent create() calls
throw new ContestedOptimisticLockException();
}
@ -243,7 +211,7 @@ public class Accounts extends AbstractDynamoDbStore {
// this shouldn't happen
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
}
} catch (JsonProcessingException e) {
} catch (final JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
@ -275,62 +243,34 @@ public class Accounts extends AbstractDynamoDbStore {
try {
final List<TransactWriteItem> writeItems = new ArrayList<>();
final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid());
final AttributeValue numberAttr = AttributeValues.fromString(number);
final AttributeValue pniAttr = AttributeValues.fromUUID(phoneNumberIdentifier);
writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumberConstraintTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.conditionExpression("attribute_not_exists(#number)")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(originalPni)))
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.conditionExpression("attribute_not_exists(#pni)")
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build());
writeItems.add(buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, originalNumber));
writeItems.add(buildConstraintTablePut(phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr));
writeItems.add(buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, originalPni));
writeItems.add(buildConstraintTablePut(phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniAttr));
writeItems.add(
TransactWriteItem.builder()
.update(Update.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
.key(Map.of(KEY_ACCOUNT_UUID, uuidAttr))
.updateExpression(
"SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
.conditionExpression(
"attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of(
"#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#pni", ATTR_PNI_UUID,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":number", numberAttr,
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":number", AttributeValues.fromString(number),
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":pni", pniAttr,
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.build())
@ -340,7 +280,7 @@ public class Accounts extends AbstractDynamoDbStore {
.transactItems(writeItems)
.build();
client.transactWriteItems(request);
db().transactWriteItems(request);
account.setVersion(account.getVersion() + 1);
succeeded = true;
@ -354,21 +294,6 @@ public class Accounts extends AbstractDynamoDbStore {
});
}
public static byte[] reservedUsernameHash(final UUID accountId, final String reservedUsername) {
final MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
final ByteBuffer byteBuffer = ByteBuffer.allocate(32 + 1);
sha256.update(reservedUsername.getBytes(StandardCharsets.UTF_8));
sha256.update(UUIDUtil.toBytes(accountId));
byteBuffer.put(RESERVED_USERNAME_HASH_VERSION);
byteBuffer.put(sha256.digest());
return byteBuffer.array();
}
/**
* Reserve a username under a token
*
@ -386,7 +311,7 @@ public class Accounts extends AbstractDynamoDbStore {
boolean succeeded = false;
long expirationTime = clock.instant().plus(ttl).getEpochSecond();
final long expirationTime = clock.instant().plus(ttl).getEpochSecond();
final UUID reservationToken = UUID.randomUUID();
try {
@ -425,14 +350,14 @@ public class Accounts extends AbstractDynamoDbStore {
.transactItems(writeItems)
.build();
client.transactWriteItems(request);
db().transactWriteItems(request);
account.setVersion(account.getVersion() + 1);
succeeded = true;
} catch (final JsonProcessingException e) {
throw new IllegalArgumentException(e);
} catch (final TransactionCanceledException e) {
if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch("ConditionalCheckFailed"::equals)) {
if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
throw new ContestedOptimisticLockException();
}
throw e;
@ -520,25 +445,21 @@ public class Accounts extends AbstractDynamoDbStore {
.build())
.build());
maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(usernamesConstraintTableName)
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(originalUsername)))
.build())
.build()));
maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(
buildDelete(usernamesConstraintTableName, ATTR_USERNAME, originalUsername)));
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(writeItems)
.build();
client.transactWriteItems(request);
db().transactWriteItems(request);
account.setVersion(account.getVersion() + 1);
succeeded = true;
} catch (final JsonProcessingException e) {
throw new IllegalArgumentException(e);
} catch (final TransactionCanceledException e) {
if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch("ConditionalCheckFailed"::equals)) {
if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
throw new ContestedOptimisticLockException();
}
throw e;
@ -551,7 +472,7 @@ public class Accounts extends AbstractDynamoDbStore {
}
}
public void clearUsername(Account account) {
public void clearUsername(final Account account) {
account.getUsername().ifPresent(username -> {
CLEAR_USERNAME_TIMER.record(() -> {
account.setUsername(null);
@ -578,25 +499,20 @@ public class Accounts extends AbstractDynamoDbStore {
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(usernamesConstraintTableName)
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
.build())
.build());
writeItems.add(buildDelete(usernamesConstraintTableName, ATTR_USERNAME, username));
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(writeItems)
.build();
client.transactWriteItems(request);
db().transactWriteItems(request);
account.setVersion(account.getVersion() + 1);
succeeded = true;
} catch (final JsonProcessingException e) {
throw new IllegalArgumentException(e);
} catch (final TransactionCanceledException e) {
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
if (conditionalCheckFailed(e.cancellationReasons().get(0))) {
throw new ContestedOptimisticLockException();
}
@ -610,27 +526,18 @@ public class Accounts extends AbstractDynamoDbStore {
});
}
/**
* Extract the cause from a CompletionException
*/
private static Throwable unwrap(Throwable throwable) {
while (throwable instanceof CompletionException e && throwable.getCause() != null) {
throwable = e.getCause();
}
return throwable;
}
public CompletionStage<Void> updateAsync(Account account) {
@Nonnull
public CompletionStage<Void> updateAsync(final Account account) {
return record(UPDATE_TIMER, () -> {
final UpdateItemRequest updateItemRequest;
try {
// username, e164, and pni cannot be modified through this method
Map<String, String> attrNames = new HashMap<>(Map.of(
final Map<String, String> attrNames = new HashMap<>(Map.of(
"#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION));
Map<String, AttributeValue> attrValues = new HashMap<>(Map.of(
final Map<String, AttributeValue> attrValues = new HashMap<>(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
@ -654,7 +561,7 @@ public class Accounts extends AbstractDynamoDbStore {
.expressionAttributeNames(attrNames)
.expressionAttributeValues(attrValues)
.build();
} catch (JsonProcessingException e) {
} catch (final JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
@ -664,7 +571,7 @@ public class Accounts extends AbstractDynamoDbStore {
return (Void) null;
})
.exceptionally(throwable -> {
final Throwable unwrapped = unwrap(throwable);
final Throwable unwrapped = ExceptionUtils.unwrap(throwable);
if (unwrapped instanceof TransactionConflictException) {
throw new ContestedOptimisticLockException();
} else if (unwrapped instanceof ConditionalCheckFailedException e) {
@ -679,12 +586,12 @@ public class Accounts extends AbstractDynamoDbStore {
});
}
public void update(Account account) throws ContestedOptimisticLockException {
public void update(final Account account) throws ContestedOptimisticLockException {
try {
this.updateAsync(account).toCompletableFuture().join();
} catch (CompletionException e) {
updateAsync(account).toCompletableFuture().join();
} catch (final CompletionException e) {
// unwrap CompletionExceptions, throw as long is it's unchecked
Throwables.throwIfUnchecked(unwrap(e));
Throwables.throwIfUnchecked(ExceptionUtils.unwrap(e));
// if we otherwise somehow got a wrapped checked exception,
// rethrow the checked exception wrapped by the original CompletionException
@ -698,15 +605,14 @@ public class Accounts extends AbstractDynamoDbStore {
}
public boolean usernameAvailable(final Optional<UUID> reservationToken, final String username) {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(usernamesConstraintTableName)
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
.build());
if (!response.hasItem()) {
final Optional<Map<String, AttributeValue>> usernameItem = itemByKey(
usernamesConstraintTableName, ATTR_USERNAME, AttributeValues.fromString(username));
if (usernameItem.isEmpty()) {
// username is free
return true;
}
final Map<String, AttributeValue> item = response.item();
final Map<String, AttributeValue> item = usernameItem.get();
if (AttributeValues.getLong(item, ATTR_TTL, Long.MAX_VALUE) < clock.instant().getEpochSecond()) {
// username was reserved, but has expired
@ -719,112 +625,56 @@ public class Accounts extends AbstractDynamoDbStore {
.orElse(false);
}
public Optional<Account> getByE164(String number) {
return GET_BY_NUMBER_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(this::accountByUuid)
.map(Accounts::fromItem);
});
}
public Optional<Account> getByUsername(final String username) {
return GET_BY_USERNAME_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(usernamesConstraintTableName)
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
.build());
return Optional.ofNullable(response.item())
// ignore items with a ttl (reservations)
.filter(item -> !item.containsKey(ATTR_TTL))
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(this::accountByUuid)
.map(Accounts::fromItem);
});
@Nonnull
public Optional<Account> getByE164(final String number) {
return getByIndirectLookup(
GET_BY_NUMBER_TIMER, phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, AttributeValues.fromString(number));
}
@Nonnull
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
return GET_BY_PNI_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(this::accountByUuid)
.map(Accounts::fromItem);
});
return getByIndirectLookup(
GET_BY_PNI_TIMER, phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier));
}
private Map<String, AttributeValue> accountByUuid(AttributeValue uuid) {
GetItemResponse r = client.getItem(GetItemRequest.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, uuid))
.consistentRead(true)
.build());
return r.item().isEmpty() ? null : r.item();
@Nonnull
public Optional<Account> getByUsername(final String username) {
return getByIndirectLookup(
GET_BY_USERNAME_TIMER,
usernamesConstraintTableName,
ATTR_USERNAME,
AttributeValues.fromString(username),
item -> !item.containsKey(ATTR_TTL) // ignore items with a ttl (reservations)
);
}
public Optional<Account> getByAccountIdentifier(UUID uuid) {
return GET_BY_UUID_TIMER.record(() ->
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
.map(Accounts::fromItem));
@Nonnull
public Optional<Account> getByAccountIdentifier(final UUID uuid) {
return requireNonNull(GET_BY_UUID_TIMER.record(() ->
itemByKey(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))
.map(Accounts::fromItem)));
}
public void delete(UUID uuid) {
DELETE_TIMER.record(() -> {
public void delete(final UUID uuid) {
DELETE_TIMER.record(() -> getByAccountIdentifier(uuid).ifPresent(account -> {
getByAccountIdentifier(uuid).ifPresent(account -> {
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(
buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, account.getNumber()),
buildDelete(accountsTableName, KEY_ACCOUNT_UUID, uuid),
buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, account.getPhoneNumberIdentifier())
));
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build())
.build();
account.getUsername().ifPresent(username -> transactWriteItems.add(
buildDelete(usernamesConstraintTableName, ATTR_USERNAME, username)));
TransactWriteItem accountDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.build())
.build();
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(transactWriteItems).build();
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
transactWriteItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier())))
.build())
.build());
account.getUsername().ifPresent(username -> transactWriteItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(usernamesConstraintTableName)
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
.build())
.build()));
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(transactWriteItems).build();
client.transactWriteItems(request);
});
});
db().transactWriteItems(request);
}));
}
@Nonnull
public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(scanPageSize)
@ -833,6 +683,7 @@ public class Accounts extends AbstractDynamoDbStore {
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER);
}
@Nonnull
public AccountCrawlChunk getAllFromStart(final int maxCount) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(scanPageSize);
@ -840,34 +691,185 @@ public class Accounts extends AbstractDynamoDbStore {
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER);
}
private static <T> CompletionStage<T> record(final Timer timer, Supplier<CompletionStage<T>> toRecord) {
final Instant start = Instant.now();
return toRecord.get().whenComplete((ignoreT, ignoreE) -> timer.record(Duration.between(start, Instant.now())));
@Nonnull
private Optional<Account> getByIndirectLookup(
final Timer timer,
final String tableName,
final String keyName,
final AttributeValue keyValue) {
return getByIndirectLookup(timer, tableName, keyName, keyValue, i -> true);
}
@Nonnull
private Optional<Account> getByIndirectLookup(
final Timer timer,
final String tableName,
final String keyName,
final AttributeValue keyValue,
final Predicate<? super Map<String, AttributeValue>> predicate) {
return requireNonNull(timer.record(() -> itemByKey(tableName, keyName, keyValue)
.filter(predicate)
.map(item -> item.get(KEY_ACCOUNT_UUID))
.flatMap(uuid -> itemByKey(accountsTableName, KEY_ACCOUNT_UUID, uuid))
.map(Accounts::fromItem)));
}
@Nonnull
private Optional<Map<String, AttributeValue>> itemByKey(final String table, final String keyName, final AttributeValue keyValue) {
final GetItemResponse response = db().getItem(GetItemRequest.builder()
.tableName(table)
.key(Map.of(keyName, keyValue))
.consistentRead(true)
.build());
return Optional.ofNullable(response.item()).filter(m -> !m.isEmpty());
}
@Nonnull
private TransactWriteItem buildAccountPut(
final Account account,
final AttributeValue uuidAttr,
final AttributeValue numberAttr,
final AttributeValue pniUuidAttr) throws JsonProcessingException {
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
KEY_ACCOUNT_UUID, uuidAttr,
ATTR_ACCOUNT_E164, numberAttr,
ATTR_PNI_UUID, pniUuidAttr,
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
// Add the UAK if it's in the account
account.getUnidentifiedAccessKey()
.map(AttributeValues::fromByteArray)
.ifPresent(uak -> item.put(ATTR_UAK, uak));
return TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", numberAttr))
.tableName(accountsTableName)
.item(item)
.build())
.build();
}
@Nonnull
private static TransactWriteItem buildConstraintTablePutIfAbsent(
final String tableName,
final AttributeValue uuidAttr,
final String keyName,
final AttributeValue keyValue
) {
return TransactWriteItem.builder()
.put(Put.builder()
.tableName(tableName)
.item(Map.of(
keyName, keyValue,
KEY_ACCOUNT_UUID, uuidAttr))
.conditionExpression(
"attribute_not_exists(#key) OR #uuid = :uuid")
.expressionAttributeNames(Map.of(
"#key", keyName,
"#uuid", KEY_ACCOUNT_UUID))
.expressionAttributeValues(Map.of(
":uuid", uuidAttr))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
}
@Nonnull
private static TransactWriteItem buildConstraintTablePut(
final String tableName,
final AttributeValue uuidAttr,
final String keyName,
final AttributeValue keyValue) {
return TransactWriteItem.builder()
.put(Put.builder()
.tableName(tableName)
.item(Map.of(
keyName, keyValue,
KEY_ACCOUNT_UUID, uuidAttr))
.conditionExpression(
"attribute_not_exists(#key)")
.expressionAttributeNames(Map.of(
"#key", keyName))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
}
@Nonnull
private static TransactWriteItem buildDelete(final String tableName, final String keyName, final String keyValue) {
return buildDelete(tableName, keyName, AttributeValues.fromString(keyValue));
}
@Nonnull
private static TransactWriteItem buildDelete(final String tableName, final String keyName, final UUID keyValue) {
return buildDelete(tableName, keyName, AttributeValues.fromUUID(keyValue));
}
@Nonnull
private static TransactWriteItem buildDelete(final String tableName, final String keyName, final AttributeValue keyValue) {
return TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(tableName)
.key(Map.of(keyName, keyValue))
.build())
.build();
}
@Nonnull
private static <T> CompletionStage<T> record(final Timer timer, final Supplier<CompletionStage<T>> toRecord) {
final Timer.Sample sample = Timer.start();
return toRecord.get().whenComplete((ignoreT, ignoreE) -> sample.stop(timer));
}
@Nonnull
private AccountCrawlChunk scanForChunk(final ScanRequest.Builder scanRequestBuilder, final int maxCount, final Timer timer) {
scanRequestBuilder.tableName(accountsTableName);
final List<Map<String, AttributeValue>> items = timer.record(() -> scan(scanRequestBuilder.build(), maxCount));
final List<Map<String, AttributeValue>> items = requireNonNull(timer.record(() -> scan(scanRequestBuilder.build(), maxCount)));
final List<Account> accounts = items.stream().map(Accounts::fromItem).toList();
return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null);
}
@Nonnull
private static String extractCancellationReasonCodes(final TransactionCanceledException exception) {
return exception.cancellationReasons().stream()
.map(CancellationReason::code)
.collect(Collectors.joining(", "));
}
@Nonnull
public static byte[] reservedUsernameHash(final UUID accountId, final String reservedUsername) {
final MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
final ByteBuffer byteBuffer = ByteBuffer.allocate(32 + 1);
sha256.update(reservedUsername.getBytes(StandardCharsets.UTF_8));
sha256.update(UUIDUtil.toBytes(accountId));
byteBuffer.put(RESERVED_USERNAME_HASH_VERSION);
byteBuffer.put(sha256.digest());
return byteBuffer.array();
}
@VisibleForTesting
static Account fromItem(Map<String, AttributeValue> item) {
if (!item.containsKey(ATTR_ACCOUNT_DATA) ||
!item.containsKey(ATTR_ACCOUNT_E164) ||
// TODO: eventually require ATTR_CANONICALLY_DISCOVERABLE
!item.containsKey(KEY_ACCOUNT_UUID)) {
@Nonnull
static Account fromItem(final Map<String, AttributeValue> item) {
// TODO: eventually require ATTR_CANONICALLY_DISCOVERABLE
if (!item.containsKey(ATTR_ACCOUNT_DATA)
|| !item.containsKey(ATTR_ACCOUNT_E164)
|| !item.containsKey(KEY_ACCOUNT_UUID)) {
throw new RuntimeException("item missing values");
}
try {
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
final Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
final UUID accountIdentifier = UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer());
final UUID phoneNumberIdentifierFromAttribute = AttributeValues.getUUID(item, ATTR_PNI_UUID, null);
@ -883,12 +885,18 @@ public class Accounts extends AbstractDynamoDbStore {
account.setUuid(accountIdentifier);
account.setUsername(AttributeValues.getString(item, ATTR_USERNAME, null));
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE))
.map(AttributeValue::bool)
.orElse(false));
return account;
} catch (IOException e) {
} catch (final IOException e) {
throw new RuntimeException("Could not read stored account data", e);
}
}
private static boolean conditionalCheckFailed(final CancellationReason reason) {
return CONDITIONAL_CHECK_FAILED.equals(reason.code());
}
}

View File

@ -6,5 +6,6 @@
package org.whispersystems.textsecuregcm.storage;
public interface PubSubAddress {
public String serialize();
String serialize();
}

View File

@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.util;
import java.util.concurrent.CompletionException;
public final class ExceptionUtils {
private ExceptionUtils() {
// utility class
}
/**
* Extracts the cause of a {@link CompletionException}. If the given {@code throwable} is a
* {@code CompletionException}, this method will recursively iterate through its causal chain until it finds the first
* cause that is not a {@code CompletionException}. If the last {@code CompletionException} in the causal chain has a
* {@code null} cause, then this method returns the last {@code CompletionException} in the chain. If the given
* {@code throwable} is not a {@code CompletionException}, then this method returns the original {@code throwable}.
*
* @param throwable the throwable to "unwrap"
*
* @return the first entity in the given {@code throwable}'s causal chain that is not a {@code CompletionException}
*/
public static Throwable unwrap(Throwable throwable) {
while (throwable instanceof CompletionException e && throwable.getCause() != null) {
throwable = e.getCause();
}
return throwable;
}
/**
* Wraps the given {@code throwable} in a {@link CompletionException} unless the given {@code throwable} is alreadt
* a {@code CompletionException}, in which case this method returns the original throwable.
*
* @param throwable the throwable to wrap in a {@code CompletionException}
*/
public static CompletionException wrap(final Throwable throwable) {
return throwable instanceof CompletionException completionException
? completionException
: new CompletionException(throwable);
}
}

View File

@ -10,20 +10,25 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.annotation.Nonnull;
public class SystemMapper {
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectMapper MAPPER = build();
static {
@Nonnull
public static ObjectMapper getMapper() {
return MAPPER;
}
@Nonnull
private static ObjectMapper build() {
final ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public static ObjectMapper getMapper() {
return mapper;
}
}

View File

@ -141,7 +141,7 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
configuration.getDynamoDbTables().getPendingAccounts().getTableName());
Accounts accounts = new Accounts(dynamicConfigurationManager,
Accounts accounts = new Accounts(
dynamoDbClient,
dynamoDbAsyncClient,
configuration.getDynamoDbTables().getAccounts().getTableName(),

View File

@ -143,7 +143,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
configuration.getDynamoDbTables().getPendingAccounts().getTableName());
Accounts accounts = new Accounts(dynamicConfigurationManager,
Accounts accounts = new Accounts(
dynamoDbClient,
dynamoDbAsyncClient,
configuration.getDynamoDbTables().getAccounts().getTableName(),

View File

@ -146,7 +146,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
configuration.getDynamoDbTables().getPendingAccounts().getTableName());
Accounts accounts = new Accounts(dynamicConfigurationManager,
Accounts accounts = new Accounts(
dynamoDbClient,
dynamoDbAsyncClient,
configuration.getDynamoDbTables().getAccounts().getTableName(),

View File

@ -157,7 +157,6 @@ class AccountsManagerChangeNumberIntegrationTest {
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
final Accounts accounts = new Accounts(
dynamicConfigurationManager,
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(),
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbAsyncClient(),
ACCOUNTS_DYNAMO_EXTENSION.getTableName(),

View File

@ -127,7 +127,6 @@ class AccountsManagerConcurrentModificationIntegrationTest {
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
accounts = new Accounts(
dynamicConfigurationManager,
dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getDynamoDbAsyncClient(),
dynamoDbExtension.getTableName(),

View File

@ -5,6 +5,27 @@
package org.whispersystems.textsecuregcm.storage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
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;
@ -22,17 +43,14 @@ import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
import software.amazon.awssdk.services.dynamodb.model.KeyType;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
class AccountsManagerUsernameIntegrationTest {
@ -126,7 +144,6 @@ class AccountsManagerUsernameIntegrationTest {
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
accounts = Mockito.spy(new Accounts(
dynamicConfigurationManager,
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(),
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbAsyncClient(),
ACCOUNTS_DYNAMO_EXTENSION.getTableName(),

View File

@ -17,7 +17,6 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.uuid.UUIDComparator;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@ -138,7 +137,6 @@ class AccountsTest {
this.accounts = new Accounts(
clock,
mockDynamicConfigManager,
dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getDynamoDbAsyncClient(),
dynamoDbExtension.getTableName(),
@ -373,7 +371,7 @@ class AccountsTest {
void testUpdateWithMockTransactionConflictException(boolean wrapException) {
final DynamoDbAsyncClient dynamoDbAsyncClient = mock(DynamoDbAsyncClient.class);
accounts = new Accounts(mockDynamicConfigManager, mock(DynamoDbClient.class),
accounts = new Accounts(mock(DynamoDbClient.class),
dynamoDbAsyncClient, dynamoDbExtension.getTableName(),
NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, USERNAME_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);