Skip to content

Commit 515b581

Browse files
committed
[POC] Optimistic Locking for Delete Operations
1 parent 1d6bb4c commit 515b581

File tree

14 files changed

+1130
-11
lines changed

14 files changed

+1130
-11
lines changed

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

Lines changed: 228 additions & 2 deletions
Large diffs are not rendered by default.

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

Lines changed: 233 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,43 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
2023

2124
import org.assertj.core.data.Offset;
2225
import org.junit.After;
2326
import org.junit.AfterClass;
2427
import org.junit.BeforeClass;
2528
import org.junit.Test;
29+
import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension;
2630
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
2731
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
2832
import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex;
2933
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
3034
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3135
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3236
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
37+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest;
38+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3339
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3440
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
41+
import software.amazon.awssdk.enhanced.dynamodb.model.VersionedRecord;
3542
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
43+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3644
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
3745
import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity;
3846
import software.amazon.awssdk.services.dynamodb.model.Projection;
3947
import software.amazon.awssdk.services.dynamodb.model.ProjectionType;
4048
import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity;
4149
import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics;
4250
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
51+
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
4352

4453
public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4554

4655
private static final String TABLE_NAME = createTestTableName();
56+
private static final String VERSIONED_TABLE_NAME = createTestTableName();
4757

4858
private static final EnhancedLocalSecondaryIndex LOCAL_SECONDARY_INDEX =
4959
EnhancedLocalSecondaryIndex.builder()
@@ -56,27 +66,39 @@ public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegration
5666
private static DynamoDbClient dynamoDbClient;
5767
private static DynamoDbEnhancedClient enhancedClient;
5868
private static DynamoDbTable<Record> mappedTable;
69+
private static DynamoDbTable<VersionedRecord> versionedRecordTable;
5970

6071
@BeforeClass
6172
public static void beforeClass() {
6273
dynamoDbClient = createDynamoDbClient();
63-
enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build();
74+
enhancedClient = DynamoDbEnhancedClient.builder()
75+
.dynamoDbClient(dynamoDbClient)
76+
.extensions(VersionedRecordExtension.builder().build())
77+
.build();
6478
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6579
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX));
80+
versionedRecordTable = enhancedClient.table(VERSIONED_TABLE_NAME, VERSIONED_RECORD_TABLE_SCHEMA);
81+
versionedRecordTable.createTable();
6682
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME));
83+
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(VERSIONED_TABLE_NAME));
6784
}
6885

6986
@After
7087
public void tearDown() {
7188
mappedTable.scan()
7289
.items()
7390
.forEach(record -> mappedTable.deleteItem(record));
91+
92+
versionedRecordTable.scan()
93+
.items()
94+
.forEach(versionedRecord -> versionedRecordTable.deleteItem(versionedRecord));
7495
}
7596

7697
@AfterClass
7798
public static void afterClass() {
7899
try {
79100
dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME));
101+
dynamoDbClient.deleteTable(r -> r.tableName(VERSIONED_TABLE_NAME));
80102
} finally {
81103
dynamoDbClient.close();
82104
}
@@ -321,4 +343,213 @@ public void getItem_set_stronglyConsistent() {
321343
// A strongly consistent read request of an item up to 4 KB requires one read request unit.
322344
assertThat(consumedCapacity.capacityUnits()).isCloseTo(20.0, Offset.offset(1.0));
323345
}
324-
}
346+
347+
// ========== OPTIMISTIC LOCKING TESTS ==========
348+
349+
// 1. deleteItem(T item) - Non-versioned record
350+
@Test
351+
public void deleteItem_nonVersionedRecord_shouldSucceed() {
352+
Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item");
353+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
354+
355+
mappedTable.putItem(item);
356+
mappedTable.deleteItem(item);
357+
358+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
359+
assertThat(deletedItem).isNull();
360+
}
361+
362+
// 2. deleteItem(T item) - Versioned record, versions match
363+
@Test
364+
public void deleteItem_versionedRecord_versionMatch_shouldSucceed() {
365+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
366+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
367+
368+
versionedRecordTable.putItem(item);
369+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
370+
versionedRecordTable.deleteItem(savedItem);
371+
372+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
373+
assertThat(deletedItem).isNull();
374+
}
375+
376+
// 3. deleteItem(T item, false) - Versioned record, should not use optimistic locking
377+
@Test
378+
public void deleteItem_versionedRecord_flagFalse_shouldSucceed() {
379+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
380+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
381+
382+
versionedRecordTable.putItem(item);
383+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
384+
385+
// Update the item to change its version
386+
savedItem.setStringAttribute("Updated Item");
387+
versionedRecordTable.updateItem(savedItem);
388+
389+
// Delete with old version but flag=false - should succeed (no optimistic locking)
390+
VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1);
391+
versionedRecordTable.deleteItem(oldVersionItem, false);
392+
393+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
394+
assertThat(deletedItem).isNull();
395+
}
396+
397+
// 4. deleteItem(T item, true) - Versioned record, versions match
398+
@Test
399+
public void deleteItem_versionedRecord_flagTrue_versionMatch_shouldSucceed() {
400+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
401+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
402+
403+
versionedRecordTable.putItem(item);
404+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
405+
versionedRecordTable.deleteItem(savedItem, true);
406+
407+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
408+
assertThat(deletedItem).isNull();
409+
}
410+
411+
// 5. deleteItem(T item, true) - Versioned record, versions mismatch
412+
@Test
413+
public void deleteItem_versionedRecord_flagTrue_versionMismatch_shouldFail() {
414+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
415+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
416+
417+
versionedRecordTable.putItem(item);
418+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
419+
420+
// Update the item to change its version
421+
savedItem.setStringAttribute("Updated Item");
422+
versionedRecordTable.updateItem(savedItem);
423+
424+
// Try to delete with old version and flag=true - should fail
425+
VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1);
426+
427+
assertThatThrownBy(() -> versionedRecordTable.deleteItem(oldVersionItem, true))
428+
.isInstanceOf(ConditionalCheckFailedException.class)
429+
.satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed"));
430+
}
431+
432+
433+
434+
435+
// 6. deleteItem(DeleteItemEnhancedRequest) with builder method - versions match
436+
@Test
437+
public void deleteItemWithBuilder_versionMatch_shouldSucceed() {
438+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
439+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
440+
441+
versionedRecordTable.putItem(item);
442+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
443+
444+
DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder()
445+
.key(recordKey)
446+
.withOptimisticLocking(AttributeValue.builder().n(savedItem.getVersion().toString()).build(), "version")
447+
.build();
448+
449+
versionedRecordTable.deleteItem(requestWithLocking);
450+
451+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
452+
assertThat(deletedItem).isNull();
453+
}
454+
455+
// 7. deleteItem(DeleteItemEnhancedRequest) with builder method - versions mismatch
456+
@Test
457+
public void deleteItemWithBuilder_versionMismatch_shouldFail() {
458+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
459+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
460+
461+
versionedRecordTable.putItem(item);
462+
463+
DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder()
464+
.key(recordKey)
465+
.withOptimisticLocking(AttributeValue.builder().n("999").build(), "version")
466+
.build();
467+
468+
assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking))
469+
.isInstanceOf(ConditionalCheckFailedException.class)
470+
.satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed"));
471+
}
472+
473+
// 8. TransactWriteItems.addDeleteItem(T item) - Non-versioned record
474+
@Test
475+
public void transactDeleteItem_nonVersionedRecord_shouldSucceed() {
476+
Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item");
477+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
478+
479+
mappedTable.putItem(item);
480+
481+
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
482+
.addDeleteItem(mappedTable, item)
483+
.build());
484+
485+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
486+
assertThat(deletedItem).isNull();
487+
}
488+
489+
// 9. TransactWriteItems.addDeleteItem(T item) - Versioned record, versions match
490+
@Test
491+
public void transactDeleteItem_versionedRecord_versionMatch_shouldSucceed() {
492+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
493+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
494+
495+
versionedRecordTable.putItem(item);
496+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
497+
498+
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
499+
.addDeleteItem(versionedRecordTable, savedItem)
500+
.build());
501+
502+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
503+
assertThat(deletedItem).isNull();
504+
}
505+
506+
507+
508+
509+
// 10. TransactWriteItems with builder method - versions match
510+
@Test
511+
public void transactDeleteItemWithBuilder_versionMatch_shouldSucceed() {
512+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
513+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
514+
515+
versionedRecordTable.putItem(item);
516+
VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
517+
518+
TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder()
519+
.key(recordKey)
520+
.withOptimisticLocking(AttributeValue.builder().n(savedItem.getVersion().toString()).build(), "version")
521+
.build();
522+
523+
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
524+
.addDeleteItem(versionedRecordTable,
525+
requestWithLocking)
526+
.build());
527+
528+
VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey));
529+
assertThat(deletedItem).isNull();
530+
}
531+
532+
// 11. TransactWriteItems with builder method - versions mismatch
533+
@Test
534+
public void transactDeleteItemWithBuilder_versionMismatch_shouldFail() {
535+
VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
536+
Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build();
537+
538+
versionedRecordTable.putItem(item);
539+
540+
TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder()
541+
.key(recordKey)
542+
.withOptimisticLocking(AttributeValue.builder().n("999").build(), "version")
543+
.build();
544+
545+
TransactionCanceledException ex = assertThrows(TransactionCanceledException.class,
546+
() -> enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
547+
.addDeleteItem(versionedRecordTable, requestWithLocking)
548+
.build()));
549+
550+
assertTrue(ex.hasCancellationReasons());
551+
assertEquals(1, ex.cancellationReasons().size());
552+
assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code());
553+
assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message());
554+
}
555+
}

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

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

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

18+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute;
1819
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
1920
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey;
2021
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey;
@@ -27,6 +28,7 @@
2728
import java.util.stream.IntStream;
2829
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
2930
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
31+
import software.amazon.awssdk.enhanced.dynamodb.model.VersionedRecord;
3032
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3133
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
3234
import software.amazon.awssdk.testutils.service.AwsIntegrationTestBase;
@@ -75,6 +77,37 @@ protected static DynamoDbAsyncClient createAsyncDynamoDbClient() {
7577
.setter(Record::setStringAttribute))
7678
.build();
7779

80+
protected static final TableSchema<VersionedRecord> VERSIONED_RECORD_TABLE_SCHEMA =
81+
StaticTableSchema.builder(VersionedRecord.class)
82+
.newItemSupplier(VersionedRecord::new)
83+
.addAttribute(String.class, a -> a.name("id")
84+
.getter(VersionedRecord::getId)
85+
.setter(VersionedRecord::setId)
86+
.tags(primaryPartitionKey(), secondaryPartitionKey("index1")))
87+
.addAttribute(Integer.class, a -> a.name("sort")
88+
.getter(VersionedRecord::getSort)
89+
.setter(VersionedRecord::setSort)
90+
.tags(primarySortKey(), secondarySortKey("index1")))
91+
.addAttribute(Integer.class, a -> a.name("value")
92+
.getter(VersionedRecord::getValue)
93+
.setter(VersionedRecord::setValue))
94+
.addAttribute(String.class, a -> a.name("gsi_id")
95+
.getter(VersionedRecord::getGsiId)
96+
.setter(VersionedRecord::setGsiId)
97+
.tags(secondaryPartitionKey("gsi_keys_only")))
98+
.addAttribute(Integer.class, a -> a.name("gsi_sort")
99+
.getter(VersionedRecord::getGsiSort)
100+
.setter(VersionedRecord::setGsiSort)
101+
.tags(secondarySortKey("gsi_keys_only")))
102+
.addAttribute(String.class, a -> a.name("stringAttribute")
103+
.getter(VersionedRecord::getStringAttribute)
104+
.setter(VersionedRecord::setStringAttribute))
105+
.addAttribute(Integer.class, a -> a.name("version")
106+
.getter(VersionedRecord::getVersion)
107+
.setter(VersionedRecord::setVersion)
108+
.tags(versionAttribute()))
109+
.build();
110+
78111

79112
protected static final List<Record> RECORDS =
80113
IntStream.range(0, 9)

0 commit comments

Comments
 (0)