下面列出了怎么用com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException的API类实例代码及写法,或者点击链接到github查看源代码。
@Override
public void create(Entry entry) {
readWriteLock.writeLock().lock();
try {
Map<String, AttributeValue> keys = createKey(entry);
Map<String, AttributeValueUpdate> attributes = createAttributes(entry);
Map<String, ExpectedAttributeValue> expected = expectNotExists();
try {
executeUpdate(keys, attributes, expected);
} catch (ConditionalCheckFailedException e) {
throw new AlreadyExistsException("DynamoDB store entry already exists:" + keys.toString());
}
} finally {
readWriteLock.writeLock().unlock();
}
}
@Override
public void update(Entry entry, Entry existingEntry) {
readWriteLock.writeLock().lock();
try {
Map<String, AttributeValue> keys = createKey(entry);
Map<String, AttributeValueUpdate> attributes = createAttributes(entry);
Map<String, ExpectedAttributeValue> expected = expectExists(existingEntry);
try {
executeUpdate(keys, attributes, expected);
} catch (ConditionalCheckFailedException e) {
throw new DoesNotExistException("Precondition to update entry in DynamoDB failed:" + keys.toString());
}
} finally {
readWriteLock.writeLock().unlock();
}
}
@Test
public void testCreateEntryAlreadyExists() throws Exception {
RawSecretEntry rawSecretEntry = constructRawEntry(SECRET_NAME);
UpdateItemRequest expectedUpdateRequest = constructUpdateItemRequest(rawSecretEntry, false, Optional.empty());
// Already exists will cause a check failed exception.
when(mockDynamoDBClient.updateItem(expectedUpdateRequest)).thenThrow(
new ConditionalCheckFailedException(""));
boolean exceptionThrown = false;
try {
dynamoDB.create(rawSecretEntry);
} catch (AlreadyExistsException e) {
assertEquals(e.getMessage(), "DynamoDB store entry already exists:{1={S: secret1,}, 2={N: 1,}}");
exceptionThrown = true;
}
assertTrue(exceptionThrown);
verify(mockDynamoDBClient, times(1)).updateItem(expectedUpdateRequest);
}
@Test
public void testUpdateEntryDoesNotExist() throws Exception {
RawSecretEntry rawSecretEntry = constructRawEntry(SECRET_NAME);
RawSecretEntry alternativeRawSecretEntry = constructAlternativeRawSecretEntry(SECRET_NAME);
UpdateItemRequest expectedUpdateRequest = constructUpdateItemRequest(rawSecretEntry, true, Optional.of(alternativeRawSecretEntry));
when(mockDynamoDBClient.updateItem(expectedUpdateRequest)).thenThrow(
new ConditionalCheckFailedException(""));
boolean exceptionThrown = false;
try {
dynamoDB.update(rawSecretEntry, alternativeRawSecretEntry);
} catch (DoesNotExistException e) {
assertEquals(e.getMessage(), "Precondition to update entry in DynamoDB failed:{1={S: secret1,}, 2={N: 1,}}");
exceptionThrown = true;
}
assertTrue(exceptionThrown);
// Check all the expected calls to AWS were made.
verify(mockDynamoDBClient, times(1)).updateItem(expectedUpdateRequest);
}
@Test
public void createVersionedItemWhenItemAlreadyExists() {
ExampleVersionedHashKeyItem item1 = newVersionedItem();
Transaction t1 = manager.newTransaction();
t1.save(item1);
t1.commit();
assertItemNotLocked(INTEG_HASH_TABLE_NAME, item1.getKey(), item1.getExpectedValues(), true);
ExampleVersionedHashKeyItem item2 = new ExampleVersionedHashKeyItem();
item2.setId(item1.getId());
Transaction t2 = manager.newTransaction();
try {
t2.save(item2);
fail();
} catch (ConditionalCheckFailedException e) {
t2.rollback();
}
assertItemNotLocked(INTEG_HASH_TABLE_NAME, item1.getKey(), item1.getExpectedValues(), true);
t1.delete(Long.MAX_VALUE);
t2.delete(Long.MAX_VALUE);
}
/**
* Deletes the transaction item from the database.
*
* Does not throw if the item is gone, even if the conditional check to delete the item fails, and this method doesn't know what state
* it was in when deleted. The caller is responsible for guaranteeing that it was actually in "currentState" immediately before calling
* this method.
*/
protected void complete(final State expectedCurrentState) {
try {
txItem.complete(expectedCurrentState);
} catch (ConditionalCheckFailedException e) {
// Re-read state to ensure it was already completed
try {
txItem = new TransactionItem(txId, txManager, false);
if(! txItem.isCompleted()) {
throw new TransactionAssertionException(txId, "Expected the transaction to be completed (no item), but there was one.");
}
} catch (TransactionNotFoundException tnfe) {
// expected - transaction record no longer exists
}
}
}
/**
* Inserts a new transaction item into the table. Assumes txKey is already initialized.
* @return the txItem
* @throws TransactionException if the transaction already exists
*/
private Map<String, AttributeValue> insert() {
Map<String, AttributeValue> item = new HashMap<String, AttributeValue>();
item.put(AttributeName.STATE.toString(), new AttributeValue(STATE_PENDING));
item.put(AttributeName.VERSION.toString(), new AttributeValue().withN(Integer.toString(1)));
item.put(AttributeName.DATE.toString(), txManager.getCurrentTimeAttribute());
item.putAll(txKey);
Map<String, ExpectedAttributeValue> expectNotExists = new HashMap<String, ExpectedAttributeValue>(2);
expectNotExists.put(AttributeName.TXID.toString(), new ExpectedAttributeValue(false));
expectNotExists.put(AttributeName.STATE.toString(), new ExpectedAttributeValue(false));
PutItemRequest request = new PutItemRequest()
.withTableName(txManager.getTransactionTableName())
.withItem(item)
.withExpected(expectNotExists);
try {
txManager.getClient().putItem(request);
return item;
} catch (ConditionalCheckFailedException e) {
throw new TransactionException("Failed to create new transaction with id " + txId, e);
}
}
/**
* Checks a map of expected values against a map of actual values in a way
* that's compatible with how the DynamoDB service interprets the Expected
* parameter of PutItem, UpdateItem and DeleteItem.
*
* @param expectedValues
* A description of the expected values.
* @param item
* The actual values.
* @throws ConditionalCheckFailedException
* Thrown if the values do not match the expected values.
*/
public static void checkExpectedValues(Map<String, ExpectedAttributeValue> expectedValues, Map<String, AttributeValue> item) {
for (Map.Entry<String, ExpectedAttributeValue> entry : expectedValues.entrySet()) {
// if the attribute is expected to exist (null for isExists means
// true)
if ((entry.getValue().isExists() == null || entry.getValue().isExists() == true)
// but the item doesn't
&& (item == null
// or the attribute doesn't
|| item.get(entry.getKey()) == null
// or it doesn't have the expected value
|| !expectedValueMatches(entry.getValue().getValue(), item.get(entry.getKey())))) {
throw new ConditionalCheckFailedException(
"expected attribute(s) " + expectedValues
+ " but found " + item);
} else if (entry.getValue().isExists() != null
&& !entry.getValue().isExists()
&& item != null && item.get(entry.getKey()) != null) {
// the attribute isn't expected to exist, but the item exists
// and the attribute does too
throw new ConditionalCheckFailedException(
"expected attribute(s) " + expectedValues
+ " but found " + item);
}
}
}
private static void updateExistingAttributeConditionally() {
try {
HashMap<String, AttributeValue> key = new HashMap<String, AttributeValue>();
key.put("Id", new AttributeValue().withN("120"));
// Specify the desired price (25.00) and also the condition (price =
// 20.00)
Map<String, AttributeValue> expressionAttributeValues = new HashMap<String, AttributeValue>();
expressionAttributeValues.put(":val1", new AttributeValue().withN("25.00"));
expressionAttributeValues.put(":val2", new AttributeValue().withN("20.00"));
ReturnValue returnValues = ReturnValue.ALL_NEW;
UpdateItemRequest updateItemRequest = new UpdateItemRequest().withTableName(tableName).withKey(key)
.withUpdateExpression("set Price = :val1").withConditionExpression("Price = :val2")
.withExpressionAttributeValues(expressionAttributeValues).withReturnValues(returnValues);
UpdateItemResult result = client.updateItem(updateItemRequest);
// Check the response.
System.out.println("Printing item after conditional update to new attribute...");
printItem(result.getAttributes());
}
catch (ConditionalCheckFailedException cse) {
// Reload object and retry code.
System.err.println("Conditional check failed in " + tableName);
}
catch (AmazonServiceException ase) {
System.err.println("Error updating item in " + tableName);
}
}
@Override
@NonNull
public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {
String nowIso = toIsoString(now());
String lockUntilIso = toIsoString(lockConfiguration.getLockAtMostUntil());
UpdateItemSpec request = new UpdateItemSpec()
.withPrimaryKey(ID, lockConfiguration.getName())
.withUpdateExpression(OBTAIN_LOCK_QUERY)
.withConditionExpression(OBTAIN_LOCK_CONDITION)
.withValueMap(new ValueMap()
.withString(":lockUntil", lockUntilIso)
.withString(":lockedAt", nowIso)
.withString(":lockedBy", hostname)
)
.withReturnValues(ReturnValue.UPDATED_NEW);
try {
// There are three possible situations:
// 1. The lock document does not exist yet - it is inserted - we have the lock
// 2. The lock document exists and lockUtil <= now - it is updated - we have the lock
// 3. The lock document exists and lockUtil > now - ConditionalCheckFailedException is thrown
table.updateItem(request);
return Optional.of(new DynamoDBLock(table, lockConfiguration));
} catch (ConditionalCheckFailedException e) {
// Condition failed. This means there was a lock with lockUntil > now.
return Optional.empty();
}
}
private BackendException processDynamoDbApiException(final Throwable e, final String apiName, final String tableName) {
Preconditions.checkArgument(apiName != null);
Preconditions.checkArgument(!apiName.isEmpty());
final String prefix;
if (tableName == null) {
prefix = apiName;
} else {
prefix = String.format("%s_%s", apiName, tableName);
}
final String message = String.format("%s %s", prefix, e.getMessage());
if (e instanceof ResourceNotFoundException) {
return new BackendNotFoundException(String.format("%s; table not found", message), e);
} else if (e instanceof ConditionalCheckFailedException) {
return new PermanentLockingException(message, e);
} else if (e instanceof AmazonServiceException) {
if (e.getMessage() != null
&& (e.getMessage().contains(HASH_RANGE_KEY_SIZE_LIMIT) || e.getMessage().contains(UPDATE_ITEM_SIZE_LIMIT))) {
return new PermanentBackendException(message, e);
} else {
return new TemporaryBackendException(message, e);
}
} else if (e instanceof AmazonClientException) { //all client exceptions are retriable by default
return new TemporaryBackendException(message, e);
} else if (e instanceof SocketException) { //sometimes this doesn't get caught by SDK
return new TemporaryBackendException(message, e);
}
// unknown exception type
return new PermanentBackendException(message, e);
}
/**
* This API retrieves the intermediate keys from the source region and replicates it in the target region.
*
* @param materialName material name of the encryption material.
* @param version version of the encryption material.
* @param targetMetaStore target MetaStore where the encryption material to be stored.
*/
public void replicate(final String materialName, final long version, final MetaStore targetMetaStore) {
try {
Map<String, AttributeValue> item = getMaterialItem(materialName, version);
final Map<String, AttributeValue> plainText = getPlainText(item);
final Map<String, AttributeValue> encryptedText = targetMetaStore.getEncryptedText(plainText);
final PutItemRequest put = new PutItemRequest().withTableName(targetMetaStore.tableName).withItem(encryptedText)
.withExpected(doesNotExist);
targetMetaStore.ddb.putItem(put);
} catch (ConditionalCheckFailedException e) {
//Item already present.
}
}
private Map<String, AttributeValue> conditionalPut(final Map<String, AttributeValue> item) {
try {
final PutItemRequest put = new PutItemRequest().withTableName(tableName).withItem(item)
.withExpected(doesNotExist);
ddb.putItem(put);
return item;
} catch (final ConditionalCheckFailedException ex) {
final Map<String, AttributeValue> ddbKey = new HashMap<>();
ddbKey.put(DEFAULT_HASH_KEY, item.get(DEFAULT_HASH_KEY));
ddbKey.put(DEFAULT_RANGE_KEY, item.get(DEFAULT_RANGE_KEY));
return ddbGet(ddbKey);
}
}
@Override
public <T extends Item> T update(final T item,
final PersistenceExceptionHandler<?>... persistenceExceptionHandlers) {
final ItemConfiguration itemConfiguration = getItemConfiguration(item.getClass());
if (item.getVersion() == null) {
return create(item);
}
final Expected expectedCondition = new Expected(VERSION_ATTRIBUTE).eq(item.getVersion());
final Long newVersion = item.getVersion() + 1l;
item.setVersion(newVersion);
final String tableName = databaseSchemaHolder.schemaName() + "." + itemConfiguration.tableName();
final String itemJson = itemToString(item);
final PrimaryKey primaryKey = new PrimaryKey();
final ItemId itemId = itemConfiguration.getItemId(item);
final PrimaryKeyDefinition primaryKeyDefinition = itemConfiguration.primaryKeyDefinition();
primaryKey.addComponent(primaryKeyDefinition.propertyName(), itemId.value());
if (primaryKeyDefinition instanceof CompoundPrimaryKeyDefinition) {
primaryKey.addComponent(((CompoundPrimaryKeyDefinition) primaryKeyDefinition).supportingPropertyName(),
itemId.supportingValue());
}
final Table table = dynamoDBClient.getTable(tableName);
final com.amazonaws.services.dynamodbv2.document.Item previousAwsItem = table.getItem(primaryKey);
final String previousItemJson = previousAwsItem.toJSON();
final String mergedJson = mergeJSONObjects(itemJson, previousItemJson);
final com.amazonaws.services.dynamodbv2.document.Item awsItem = com.amazonaws.services.dynamodbv2.document.Item
.fromJSON(mergedJson);
final PutItemSpec putItemSpec = new PutItemSpec().withItem(awsItem).withExpected(expectedCondition);
try {
table.putItem(putItemSpec);
} catch (final ConditionalCheckFailedException e) {
throw new OptimisticLockException("Conflicting write detected while updating item");
}
return item;
}
@Test
public void shouldNotUpdate_withPutItemException() {
// Given
final ItemId itemId = new ItemId(randomId());
final StubItem stubItem = generateRandomStubItem(itemId);
final StubItem previousStubItem = generateRandomStubItem(itemId);
final ItemConfiguration itemConfiguration = new ItemConfiguration(StubItem.class, tableName);
final Collection<ItemConfiguration> itemConfigurations = Arrays.asList(itemConfiguration);
final Table mockTable = mock(Table.class);
final Item mockTableItem = mock(Item.class);
final PrimaryKey primaryKey = new PrimaryKey();
primaryKey.addComponent("id", itemId.value());
final Item previousItem = mock(Item.class);
when(mockDatabaseSchemaHolder.itemConfigurations()).thenReturn(itemConfigurations);
when(mockDynamoDBClient.getTable(schemaName + "." + tableName)).thenReturn(mockTable);
when(mockTable.getItem(any(PrimaryKey.class))).thenReturn(previousItem);
final DynamoDocumentStoreTemplate dynamoDocumentStoreTemplate = new DynamoDocumentStoreTemplate(
mockDatabaseSchemaHolder);
when(previousItem.toJSON()).thenReturn(dynamoDocumentStoreTemplate.itemToString(previousStubItem));
when(mockTableItem.toJSON()).thenReturn(dynamoDocumentStoreTemplate.itemToString(stubItem));
when(mockTable.putItem(any(PutItemSpec.class))).thenThrow(ConditionalCheckFailedException.class);
dynamoDocumentStoreTemplate.initialize(mockAmazonDynamoDbClient);
// When
OptimisticLockException thrownException = null;
try {
dynamoDocumentStoreTemplate.update(stubItem);
} catch (final OptimisticLockException optimisticLockException) {
thrownException = optimisticLockException;
}
// Then
assertNotNull(thrownException);
}
private PutItemOutcome persistData(PersonRequest personRequest) throws ConditionalCheckFailedException {
return this.dynamoDb.getTable(DYNAMODB_TABLE_NAME)
.putItem(
new PutItemSpec().withItem(new Item()
.withNumber("id", personRequest.getId())
.withString("firstName", personRequest.getFirstName())
.withString("lastName", personRequest.getLastName())
.withNumber("age", personRequest.getAge())
.withString("address", personRequest.getAddress())));
}
private static void updateExistingAttributeConditionally() {
try {
HashMap<String, AttributeValue> key = new HashMap<String, AttributeValue>();
key.put("Id", new AttributeValue().withN("120"));
// Specify the desired price (25.00) and also the condition (price = 20.00)
Map<String, AttributeValue> expressionAttributeValues = new HashMap<String, AttributeValue>();
expressionAttributeValues.put(":val1", new AttributeValue().withN("25.00"));
expressionAttributeValues.put(":val2", new AttributeValue().withN("20.00"));
ReturnValue returnValues = ReturnValue.ALL_NEW;
UpdateItemRequest updateItemRequest = new UpdateItemRequest()
.withTableName(tableName)
.withKey(key)
.withUpdateExpression("set Price = :val1")
.withConditionExpression("Price = :val2")
.withExpressionAttributeValues(expressionAttributeValues)
.withReturnValues(returnValues);
UpdateItemResult result = client.updateItem(updateItemRequest);
// Check the response.
System.out.println("Printing item after conditional update to new attribute...");
printItem(result.getAttributes());
} catch (ConditionalCheckFailedException cse) {
// Reload object and retry code.
System.err.println("Conditional check failed in " + tableName);
} catch (AmazonServiceException ase) {
System.err.println("Error updating item in " + tableName);
}
}
public static void main(String[] args) {
AmazonDynamoDBClient client = new AmazonDynamoDBClient();
client.setEndpoint("http://localhost:8000");
DynamoDB dynamoDB = new DynamoDB(client);
Table table = dynamoDB.getTable("Movies");
int year = 2015;
String title = "The Big New Movie";
final Map<String, Object> infoMap = new HashMap<String, Object>();
infoMap.put("plot", "Nothing happens at all.");
infoMap.put("rating", 0.0);
Item item = new Item()
.withPrimaryKey(new PrimaryKey("year", year, "title", title))
.withMap("info", infoMap);
// Attempt a conditional write. We expect this to fail.
PutItemSpec putItemSpec = new PutItemSpec()
.withItem(item)
.withConditionExpression("attribute_not_exists(#yr) and attribute_not_exists(title)")
.withNameMap(new NameMap()
.with("#yr", "year"));
System.out.println("Attempting a conditional write...");
try {
table.putItem(putItemSpec);
System.out.println("PutItem succeeded: " + table.getItem("year", year, "title", title).toJSONPretty());
} catch (ConditionalCheckFailedException e) {
e.printStackTrace(System.err);
System.out.println("PutItem failed");
}
}
public static void main(String[] args) {
AmazonDynamoDBClient client = new AmazonDynamoDBClient();
client.setEndpoint("http://localhost:8000");
DynamoDB dynamoDB = new DynamoDB(client);
Table table = dynamoDB.getTable("Movies");
int year = 2015;
String title = "The Big New Movie";
// Conditional update (will fail)
UpdateItemSpec updateItemSpec = new UpdateItemSpec()
.withPrimaryKey(new PrimaryKey("year", 2015, "title", "The Big New Movie"))
.withUpdateExpression("remove info.actors[0]")
.withConditionExpression("size(info.actors) > :num")
.withValueMap(new ValueMap().withNumber(":num", 3));
System.out.println("Attempting a conditional update...");
try {
table.updateItem(updateItemSpec);
System.out.println("UpdateItem succeeded: " + table.getItem("year", year, "title", title).toJSONPretty());
} catch (ConditionalCheckFailedException e) {
e.printStackTrace();
System.out.println("UpdateItem failed");
}
}
@Test
public void deleteVersionedItemWithOutOfDateVersion() {
ExampleVersionedHashKeyItem item1 = newVersionedItem();
// establish the item with version 1
Transaction t1 = manager.newTransaction();
t1.save(item1);
t1.commit();
// update the item to version 2
Transaction t2 = manager.newTransaction();
ExampleVersionedHashKeyItem item2 = t2.load(item1);
t2.save(item2);
t2.commit();
// try to delete with an outdated view of the item
Transaction t3 = manager.newTransaction();
try {
t3.delete(item1);
fail();
} catch (ConditionalCheckFailedException e) {
t3.rollback();
}
assertItemNotLocked(INTEG_HASH_TABLE_NAME, item2.getKey(), item2.getExpectedValues(), true);
t1.delete(Long.MAX_VALUE);
t2.delete(Long.MAX_VALUE);
t3.delete(Long.MAX_VALUE);
}
@Test
public void reusingMapperInstanceWithOutOfDateVersionThrowsOnSave() {
ExampleVersionedHashKeyItem item1 = newVersionedItem();
// establish the item with version 1
Transaction t1 = manager.newTransaction();
t1.save(item1);
t1.commit();
// update the item to version 2 and save
Transaction t2 = manager.newTransaction();
ExampleVersionedHashKeyItem item2 = t2.load(item1);
t2.save(item2);
t2.commit();
Transaction t3 = manager.newTransaction();
t3.load(item1);
try {
t3.save(item1);
fail();
} catch (ConditionalCheckFailedException e) {
t3.rollback();
}
assertItemNotLocked(INTEG_HASH_TABLE_NAME, item2.getKey(), item2.getExpectedValues(), true);
t1.delete(Long.MAX_VALUE);
t2.delete(Long.MAX_VALUE);
t3.delete(Long.MAX_VALUE);
}
/**
* Rolls back the transaction. You can only roll back a transaction that is in the PENDING state (not yet committed).
* <li>If you roll back a transaction in COMMITTED, this will continue committing the transaction if it isn't completed yet,
* but you will get back a TransactionCommittedException. </li>
* <li>If you roll back and already rolled back transaction, this will ensure the rollback completed, and return success</li>
* <li>If the transaction no longer exists, you'll get back an UnknownCompletedTransactionException</li>
*
* @throws TransactionCommittedException - the transaction was committed by a concurrent overlapping transaction
* @throws UnknownCompletedTransactionException - the transaction completed, but it is not known whether it was rolled back or committed
*/
public synchronized void rollback() throws TransactionCompletedException, UnknownCompletedTransactionException {
State state = null;
boolean alreadyRereadTxItem = false;
try {
txItem.finish(State.ROLLED_BACK, txItem.getVersion());
state = State.ROLLED_BACK;
} catch (ConditionalCheckFailedException e) {
try {
// Re-read state to see its actual state, since it wasn't in PENDING
txItem = new TransactionItem(txId, txManager, false);
alreadyRereadTxItem = true;
state = txItem.getState();
} catch (TransactionNotFoundException tnfe) {
throw new UnknownCompletedTransactionException(txId, "In transaction " + State.ROLLED_BACK + " attempt, transaction either rolled back or committed");
}
}
if(State.COMMITTED.equals(state)) {
if(! txItem.isCompleted()) {
doCommit();
}
throw new TransactionCommittedException(txId, "Transaction was committed");
} else if(State.ROLLED_BACK.equals(state)) {
if(! txItem.isCompleted()) {
doRollback();
}
return;
} else if (State.PENDING.equals(state)) {
if (! alreadyRereadTxItem) {
// The item was modified in the meantime (another request was added to it)
// so make sure we re-read it, and then try the rollback again
txItem = new TransactionItem(txId, txManager, false);
}
rollback();
return;
}
throw new TransactionAssertionException(txId, "Unexpected state in rollback(): " + state);
}
/**
* Releases the lock for the item. If the item was inserted only to acquire the lock (if the item didn't exist before
* for a DeleteItem or LockItem), it will be deleted now.
*
* Otherwise, all of the attributes uses for the transaction (tx id, transient flag, applied flag) will be removed.
*
* Conditions on our transaction id owning the item
*
* To be used once the transaction has committed only.
* @param request
*/
protected void unlockItemAfterCommit(Request request) {
try {
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>();
expected.put(AttributeName.TXID.toString(), new ExpectedAttributeValue().withValue(new AttributeValue(txId)));
if(request instanceof PutItem || request instanceof UpdateItem) {
Map<String, AttributeValueUpdate> updates = new HashMap<String, AttributeValueUpdate>();
updates.put(AttributeName.TXID.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
updates.put(AttributeName.TRANSIENT.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
updates.put(AttributeName.APPLIED.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
updates.put(AttributeName.DATE.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
UpdateItemRequest update = new UpdateItemRequest()
.withTableName(request.getTableName())
.withKey(request.getKey(txManager))
.withAttributeUpdates(updates)
.withExpected(expected);
txManager.getClient().updateItem(update);
} else if(request instanceof DeleteItem) {
DeleteItemRequest delete = new DeleteItemRequest()
.withTableName(request.getTableName())
.withKey(request.getKey(txManager))
.withExpected(expected);
txManager.getClient().deleteItem(delete);
} else if(request instanceof GetItem) {
releaseReadLock(request.getTableName(), request.getKey(txManager));
} else {
throw new TransactionAssertionException(txId, "Unknown request type: " + request.getClass());
}
} catch (ConditionalCheckFailedException e) {
// ignore, unlock already happened
// TODO if we really want to be paranoid we could condition on applied = 1, and then here
// we would have to read the item again and make sure that applied was 1 if we owned the lock (and assert otherwise)
}
}
protected static void releaseReadLock(String txId, TransactionManager txManager, String tableName, Map<String, AttributeValue> key) {
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>();
expected.put(AttributeName.TXID.toString(), new ExpectedAttributeValue().withValue(new AttributeValue(txId)));
expected.put(AttributeName.TRANSIENT.toString(), new ExpectedAttributeValue().withExists(false));
expected.put(AttributeName.APPLIED.toString(), new ExpectedAttributeValue().withExists(false));
try {
Map<String, AttributeValueUpdate> updates = new HashMap<String, AttributeValueUpdate>(1);
updates.put(AttributeName.TXID.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
updates.put(AttributeName.DATE.toString(), new AttributeValueUpdate().withAction(AttributeAction.DELETE));
UpdateItemRequest update = new UpdateItemRequest()
.withTableName(tableName)
.withAttributeUpdates(updates)
.withKey(key)
.withExpected(expected);
txManager.getClient().updateItem(update);
} catch (ConditionalCheckFailedException e) {
try {
expected.put(AttributeName.TRANSIENT.toString(), new ExpectedAttributeValue().withValue(new AttributeValue().withS(BOOLEAN_TRUE_ATTR_VAL)));
DeleteItemRequest delete = new DeleteItemRequest()
.withTableName(tableName)
.withKey(key)
.withExpected(expected);
txManager.getClient().deleteItem(delete);
} catch (ConditionalCheckFailedException e1) {
// Ignore, means it was definitely rolled back
// Re-read to ensure that it wasn't applied
Map<String, AttributeValue> item = getItem(txManager, tableName, key);
txAssert(! (item != null && txId.equals(getOwner(item)) && item.containsKey(AttributeName.APPLIED.toString())),
"Item should not have been applied. Unable to release lock", "item", item);
}
}
}
/**
* Saves the old copy of the item. Does not mutate the item, unless an exception is thrown.
*
* @param item
* @param rid
*/
public void saveItemImage(Map<String, AttributeValue> item, int rid) {
txAssert(! item.containsKey(AttributeName.APPLIED.toString()), txId, "The transaction has already applied this item image, it should not be saving over the item image with it");
AttributeValue existingTxId = item.put(AttributeName.TXID.toString(), new AttributeValue(txId));
if(existingTxId != null && ! txId.equals(existingTxId.getS())) {
throw new TransactionException(txId, "Items in transactions may not contain the attribute named " + AttributeName.TXID.toString());
}
// Don't save over the already saved item. Prevents us from saving the applied image instead of the previous image in the case
// of a re-drive.
// If we want to be extremely paranoid, we could expect every attribute to be set exactly already in a second write step, and assert
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>(1);
expected.put(AttributeName.IMAGE_ID.toString(), new ExpectedAttributeValue().withExists(false));
AttributeValue existingImageId = item.put(AttributeName.IMAGE_ID.toString(), new AttributeValue(txId + "#" + rid));
if(existingImageId != null) {
throw new TransactionException(txId, "Items in transactions may not contain the attribute named " + AttributeName.IMAGE_ID.toString() + ", value was already " + existingImageId);
}
// TODO failures? Size validation?
try {
txManager.getClient().putItem(new PutItemRequest()
.withTableName(txManager.getItemImageTableName())
.withExpected(expected)
.withItem(item));
} catch (ConditionalCheckFailedException e) {
// Already was saved
}
// do not mutate the item for the customer unless if there aren't exceptions
item.remove(AttributeName.IMAGE_ID.toString());
}
/**
* Marks the transaction item as either COMMITTED or ROLLED_BACK, but only if it was in the PENDING state.
* It will also condition on the expected version.
*
* @param targetState
* @param expectedVersion
* @throws ConditionalCheckFailedException if the transaction doesn't exist, isn't PENDING, is finalized,
* or the expected version doesn't match (if specified)
*/
public void finish(final State targetState, final int expectedVersion) throws ConditionalCheckFailedException {
txAssert(State.COMMITTED.equals(targetState) || State.ROLLED_BACK.equals(targetState),"Illegal state in finish(): " + targetState, "txItem", txItem);
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>(2);
expected.put(AttributeName.STATE.toString(), new ExpectedAttributeValue().withValue(new AttributeValue().withS(STATE_PENDING)));
expected.put(AttributeName.FINALIZED.toString(), new ExpectedAttributeValue().withExists(false));
expected.put(AttributeName.VERSION.toString(), new ExpectedAttributeValue().withValue(new AttributeValue().withN(Integer.toString(expectedVersion))));
Map<String, AttributeValueUpdate> updates = new HashMap<String, AttributeValueUpdate>();
updates.put(AttributeName.STATE.toString(), new AttributeValueUpdate()
.withAction(AttributeAction.PUT)
.withValue(new AttributeValue(stateToString(targetState))));
updates.put(AttributeName.DATE.toString(), new AttributeValueUpdate()
.withAction(AttributeAction.PUT)
.withValue(txManager.getCurrentTimeAttribute()));
UpdateItemRequest finishRequest = new UpdateItemRequest()
.withTableName(txManager.getTransactionTableName())
.withKey(txKey)
.withAttributeUpdates(updates)
.withReturnValues(ReturnValue.ALL_NEW)
.withExpected(expected);
UpdateItemResult finishResult = txManager.getClient().updateItem(finishRequest);
txItem = finishResult.getAttributes();
if(txItem == null) {
throw new TransactionAssertionException(txId, "Unexpected null tx item after committing " + targetState);
}
}
/**
* Completes a transaction by marking its "Finalized" attribute. This leaves the completed transaction item around
* so that the party who created the transaction can see whether it was completed or rolled back. They can then either
* delete the transaction record when they're done, or they can run a sweeper process to go and delete the completed transactions
* later on.
*
* @param expectedCurrentState
* @throws ConditionalCheckFailedException if the transaction is completed, doesn't exist anymore, or even if it isn't committed or rolled back
*/
public void complete(final State expectedCurrentState) throws ConditionalCheckFailedException {
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>(2);
if(State.COMMITTED.equals(expectedCurrentState)) {
expected.put(AttributeName.STATE.toString(), new ExpectedAttributeValue(new AttributeValue(STATE_COMMITTED)));
} else if(State.ROLLED_BACK.equals(expectedCurrentState)) {
expected.put(AttributeName.STATE.toString(), new ExpectedAttributeValue(new AttributeValue(STATE_ROLLED_BACK)));
} else {
throw new TransactionAssertionException(txId, "Illegal state in finish(): " + expectedCurrentState + " txItem " + txItem);
}
Map<String, AttributeValueUpdate> updates = new HashMap<String, AttributeValueUpdate>();
updates.put(AttributeName.FINALIZED.toString(), new AttributeValueUpdate()
.withAction(AttributeAction.PUT)
.withValue(new AttributeValue(Transaction.BOOLEAN_TRUE_ATTR_VAL)));
updates.put(AttributeName.DATE.toString(), new AttributeValueUpdate()
.withAction(AttributeAction.PUT)
.withValue(txManager.getCurrentTimeAttribute()));
UpdateItemRequest completeRequest = new UpdateItemRequest()
.withTableName(txManager.getTransactionTableName())
.withKey(txKey)
.withAttributeUpdates(updates)
.withReturnValues(ReturnValue.ALL_NEW)
.withExpected(expected);
txItem = txManager.getClient().updateItem(completeRequest).getAttributes();
}
/**
* Deletes the tx item, only if it was in the "finalized" state.
*
* @throws ConditionalCheckFailedException if the item does not exist or is not finalized
*/
public void delete() throws ConditionalCheckFailedException {
Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>(1);
expected.put(AttributeName.FINALIZED.toString(), new ExpectedAttributeValue().withValue(new AttributeValue(Transaction.BOOLEAN_TRUE_ATTR_VAL)));
DeleteItemRequest completeRequest = new DeleteItemRequest()
.withTableName(txManager.getTransactionTableName())
.withKey(txKey)
.withExpected(expected);
txManager.getClient().deleteItem(completeRequest);
}
private void checkExpectedValues(String tableName,
Map<String, AttributeValue> itemKey,
Map<String, ExpectedAttributeValue> expectedValues) {
if (expectedValues != null && !expectedValues.isEmpty()) {
for (Map.Entry<String, ExpectedAttributeValue> entry : expectedValues.entrySet()) {
if ((entry.getValue().isExists() == null || entry.getValue().isExists() == true)
&& entry.getValue().getValue() == null) {
throw new IllegalArgumentException("An explicit value is required when Exists is null or true, "
+ "but none was found in expected values for item with key " + itemKey +
": " + expectedValues);
}
}
// simulate by loading the item and checking the values;
// this also has the effect of locking the item, which gives the
// same behavior
GetItemResult result = getItem(new GetItemRequest()
.withAttributesToGet(expectedValues.keySet())
.withKey(itemKey)
.withTableName(tableName));
Map<String, AttributeValue> item = result.getItem();
try {
checkExpectedValues(expectedValues, item);
} catch (ConditionalCheckFailedException e) {
throw new ConditionalCheckFailedException("Item " + itemKey + " had unexpected attributes: " + e.getMessage());
}
}
}
/**
* This test ensures that optimistic locking can be successfully done through the {@link DynamoDBMapper} when
* combined with the @{link AttributeEncryptor}. Specifically it checks that {@link SaveBehavior#PUT} properly
* enforces versioning and will result in a {@link ConditionalCheckFailedException} when optimistic locking should
* prevent a write. Finally, it checks that {@link SaveBehavior#CLOBBER} properly ignores optimistic locking and
* overwrites the old value.
*/
@Test
public void optimisticLockingTest() {
DynamoDBMapper mapper = new DynamoDBMapper(client,
DynamoDBMapperConfig.builder()
.withSaveBehavior(SaveBehavior.PUT).build(),
new AttributeEncryptor(symProv));
DynamoDBMapper clobberMapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(symProv));
/*
* Lineage of objects
* expected -> v1 -> v2 -> v3
* |
* -> v2_2 -> clobbered
* Splitting the lineage after v1 is what should
* cause the ConditionalCheckFailedException.
*/
final int hashKey = 0;
final int rangeKey = 15;
final Mixed expected = new Mixed();
expected.setHashKey(hashKey);
expected.setRangeKey(rangeKey);
expected.setIntSet(new HashSet<Integer>());
expected.getIntSet().add(3);
expected.getIntSet().add(5);
expected.getIntSet().add(7);
expected.setDoubleValue(15);
expected.setStringValue("Blargh!");
expected.setDoubleSet(
new HashSet<Double>(Arrays.asList(15.0D, 7.6D, -3D, -34.2D, 0.0D)));
mapper.save(expected);
Mixed v1 = mapper.load(Mixed.class, hashKey, rangeKey);
assertEquals(expected, v1);
v1.setStringValue("New value");
mapper.save(v1);
Mixed v2 = mapper.load(Mixed.class, hashKey, rangeKey);
assertEquals(v1, v2);
Mixed v2_2 = mapper.load(Mixed.class, hashKey, rangeKey);
v2.getIntSet().add(-37);
mapper.save(v2);
Mixed v3 = mapper.load(Mixed.class, hashKey, rangeKey);
assertEquals(v2, v3);
assertTrue(v3.getIntSet().contains(-37));
// This should fail due to optimistic locking
v2_2.getIntSet().add(38);
try {
mapper.save(v2_2);
fail("Expected ConditionalCheckFailedException");
} catch (ConditionalCheckFailedException ex) {
// Expected exception
}
// Force the update with clobber
clobberMapper.save(v2_2);
Mixed clobbered = mapper.load(Mixed.class, hashKey, rangeKey);
assertEquals(v2_2, clobbered);
assertTrue(clobbered.getIntSet().contains(38));
assertFalse(clobbered.getIntSet().contains(-37));
}