Skip to content

Commit fe60961

Browse files
committed
Optimistic locking for delete scenario with DeleteItemEnhancedRequest and TransactWriteItemsEnhancedRequest
1 parent d9fa6c4 commit fe60961

File tree

16 files changed

+1522
-20
lines changed

16 files changed

+1522
-20
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Optimistic delete while using DynamoDbEnhancedClient - DeleteItem with DeleteItemEnhancedRequest and TransactWriteItemsEnhancedRequest"
6+
}

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AsyncCrudWithResponseIntegrationTest.java

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb;
1717

18-
import static org.assertj.core.api.Assertions.as;
1918
import static org.assertj.core.api.Assertions.assertThat;
2019
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2120

@@ -24,15 +23,18 @@
2423
import org.junit.AfterClass;
2524
import org.junit.BeforeClass;
2625
import org.junit.Test;
26+
import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension;
2727
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
2828
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
2929
import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex;
3030
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
3131
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3232
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3333
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
34+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3435
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3536
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
37+
import software.amazon.awssdk.enhanced.dynamodb.model.VersionedRecord;
3638
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3739
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
3840
import software.amazon.awssdk.services.dynamodb.model.Projection;
@@ -41,6 +43,7 @@
4143
import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics;
4244
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
4345
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
46+
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
4447

4548
public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4649

@@ -56,13 +59,18 @@ public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegr
5659
private static DynamoDbAsyncClient dynamoDbClient;
5760
private static DynamoDbEnhancedAsyncClient enhancedClient;
5861
private static DynamoDbAsyncTable<Record> mappedTable;
62+
private static DynamoDbAsyncTable<VersionedRecord> versionedRecordTable;
5963

6064
@BeforeClass
6165
public static void beforeClass() {
6266
dynamoDbClient = createAsyncDynamoDbClient();
63-
enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(dynamoDbClient).build();
67+
enhancedClient = DynamoDbEnhancedAsyncClient.builder()
68+
.dynamoDbClient(dynamoDbClient)
69+
.extensions(VersionedRecordExtension.builder().build())
70+
.build();
6471
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6572
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX)).join();
73+
versionedRecordTable = enhancedClient.table(TABLE_NAME, VERSIONED_RECORD_TABLE_SCHEMA);
6674
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)).join();
6775
}
6876

@@ -72,6 +80,11 @@ public void tearDown() {
7280
.items()
7381
.subscribe(record -> mappedTable.deleteItem(record).join())
7482
.join();
83+
84+
versionedRecordTable.scan()
85+
.items()
86+
.subscribe(versionedRecord -> versionedRecordTable.deleteItem(versionedRecord).join())
87+
.join();
7588
}
7689

7790
@AfterClass
@@ -341,4 +354,149 @@ public void getItem_withoutReturnConsumedCapacity() {
341354
GetItemEnhancedResponse<Record> response = mappedTable.getItemWithResponse(req -> req.key(key)).join();
342355
assertThat(response.consumedCapacity()).isNull();
343356
}
357+
358+
@Test
359+
public void transactWriteItems_recordWithoutVersion_andOptimisticLockingOnDeleteOnDelete_shouldSucceed() {
360+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
361+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
362+
363+
// Put the item
364+
mappedTable.putItem(originalItem).join();
365+
366+
// Retrieve the item, modify it separately and update it, which will increment the version
367+
Record savedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
368+
savedItem.setStringAttribute("Updated Item");
369+
mappedTable.updateItem(savedItem).join();
370+
371+
// Get the updated item and try to delete it
372+
Record updatedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
373+
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
374+
.addDeleteItem(mappedTable, updatedItem)
375+
.build()).join();
376+
377+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
378+
assertThat(deletedItem).isNull();
379+
}
380+
381+
@Test
382+
public void transactWriteItems_recordWithVersion_andOptimisticLockingOnDelete_ifVersionMatch_shouldSucceed() {
383+
VersionedRecord originalItem = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Original Item");
384+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
385+
386+
// Put the item
387+
versionedRecordTable.putItem(originalItem).join();
388+
389+
// Retrieve the item, modify it separately and update it, which will increment the version
390+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
391+
savedItem.setStringAttribute("Updated Item");
392+
versionedRecordTable.updateItem(savedItem).join();
393+
394+
// Get the updated item and try to delete it
395+
VersionedRecord updatedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
396+
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
397+
.addDeleteItem(versionedRecordTable, updatedItem)
398+
.build()).join();
399+
400+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
401+
assertThat(deletedItem).isNull();
402+
}
403+
404+
@Test
405+
public void transactWriteItems_recordWithVersion_andOptimisticLockingOnDelete_ifVersionMismatch_shouldFail() {
406+
VersionedRecord originalItem = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Original Item");
407+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
408+
409+
// Put the item
410+
versionedRecordTable.putItem(originalItem).join();
411+
412+
// Retrieve the item, modify it separately and update it, which will increment the version
413+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
414+
savedItem.setStringAttribute("Updated Item");
415+
versionedRecordTable.updateItem(savedItem).join();
416+
417+
// Get the updated item and try to delete it
418+
VersionedRecord updatedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
419+
updatedItem.setVersion(3); // Intentionally set a version that does not match the current version
420+
421+
TransactWriteItemsEnhancedRequest request =
422+
TransactWriteItemsEnhancedRequest.builder()
423+
.addDeleteItem(versionedRecordTable, updatedItem)
424+
.build();
425+
426+
assertThatThrownBy(() -> enhancedClient.transactWriteItems(request).join())
427+
.isInstanceOf(CompletionException.class)
428+
.satisfies(e ->
429+
assertThat(((TransactionCanceledException) e.getCause())
430+
.cancellationReasons()
431+
.stream()
432+
.anyMatch(reason ->
433+
"ConditionalCheckFailed".equals(reason.code())
434+
&& "The conditional request failed".equals(reason.message())))
435+
.isTrue());
436+
}
437+
438+
@Test
439+
public void delete_recordWithoutVersion_andOptimisticLockingOnDelete_shouldSucceed() {
440+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
441+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
442+
443+
// Put the item
444+
mappedTable.putItem(originalItem).join();
445+
446+
// Retrieve the item, modify it separately and update it, which will increment the version
447+
Record savedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
448+
savedItem.setStringAttribute("Updated Item");
449+
mappedTable.updateItem(savedItem).join();
450+
451+
// Get the updated item and try to delete it
452+
Record updatedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
453+
mappedTable.deleteItem(updatedItem).join();
454+
455+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
456+
assertThat(deletedItem).isNull();
457+
}
458+
459+
@Test
460+
public void delete_recordWithVersion_andOptimisticLockingOnDelete_ifVersionMatch_shouldSucceed() {
461+
VersionedRecord originalItem = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Original Item");
462+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
463+
464+
// Put the item
465+
versionedRecordTable.putItem(originalItem).join();
466+
467+
// Retrieve the item, modify it separately and update it, which will increment the version
468+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
469+
savedItem.setStringAttribute("Updated Item");
470+
versionedRecordTable.updateItem(savedItem).join();
471+
472+
// Get the updated item and try to delete it
473+
VersionedRecord updatedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
474+
versionedRecordTable.deleteItem(updatedItem).join();
475+
476+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
477+
assertThat(deletedItem).isNull();
478+
}
479+
480+
@Test
481+
public void delete_recordWithoutVersion_andOptimisticLockingOnDelete_ifVersionMismatch_shouldFail() {
482+
VersionedRecord originalItem = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Original Item");
483+
Key recordKey = Key.builder().partitionValue(originalItem.getId()).sortValue(originalItem.getSort()).build();
484+
485+
// Put the item
486+
versionedRecordTable.putItem(originalItem).join();
487+
488+
// Retrieve the item, modify it separately and update it, which will increment the version
489+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
490+
savedItem.setStringAttribute("Updated Item");
491+
versionedRecordTable.updateItem(savedItem).join();
492+
493+
// Get the updated item and try to delete it
494+
VersionedRecord updatedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join();
495+
updatedItem.setVersion(3); // Intentionally set a version that does not match the current version
496+
497+
assertThatThrownBy(() -> versionedRecordTable.deleteItem(updatedItem).join())
498+
.isInstanceOf(CompletionException.class)
499+
.satisfies(e ->
500+
assertThat(e.getMessage()).contains("The conditional request failed"));
501+
}
344502
}

0 commit comments

Comments
 (0)