Skip to content

Commit 21f44df

Browse files
committed
Optimistic locking for version id extension
1 parent 9d15c61 commit 21f44df

File tree

10 files changed

+753
-50
lines changed

10 files changed

+753
-50
lines changed

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

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16+
1617
package software.amazon.awssdk.enhanced.dynamodb;
1718

18-
import static org.assertj.core.api.Assertions.as;
1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2121

@@ -31,6 +31,8 @@
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.RecordWithVersion;
35+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3436
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3537
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
3638
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
@@ -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,15 @@ 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<RecordWithVersion> recordWithVersionMappedTable;
5963

6064
@BeforeClass
6165
public static void beforeClass() {
6266
dynamoDbClient = createAsyncDynamoDbClient();
6367
enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(dynamoDbClient).build();
6468
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6569
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX)).join();
70+
recordWithVersionMappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_VERSION_TABLE_SCHEMA);
6671
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)).join();
6772
}
6873

@@ -113,8 +118,8 @@ public void putItem_returnItemCollectionMetrics_set_itemCollectionMetricsNotNull
113118
public void updateItem_returnItemCollectionMetrics_set_itemCollectionMetricsNull() {
114119
Record record = new Record().setId("1").setSort(10);
115120
UpdateItemEnhancedRequest<Record> request = UpdateItemEnhancedRequest.builder(Record.class)
116-
.item(record)
117-
.build();
121+
.item(record)
122+
.build();
118123

119124
UpdateItemEnhancedResponse<Record> response = mappedTable.updateItemWithResponse(request).join();
120125

@@ -196,8 +201,8 @@ public void updateItem_returnValues_all_old() {
196201

197202

198203
UpdateItemEnhancedResponse<Record> response = mappedTable.updateItemWithResponse(r -> r.item(updatedRecord)
199-
.returnValues(ReturnValue.ALL_OLD))
200-
.join();
204+
.returnValues(ReturnValue.ALL_OLD))
205+
.join();
201206

202207
assertThat(response.attributes().getId()).isEqualTo(record.getId());
203208
assertThat(response.attributes().getSort()).isEqualTo(record.getSort());
@@ -341,4 +346,167 @@ public void getItem_withoutReturnConsumedCapacity() {
341346
GetItemEnhancedResponse<Record> response = mappedTable.getItemWithResponse(req -> req.key(key)).join();
342347
assertThat(response.consumedCapacity()).isNull();
343348
}
349+
350+
@Test
351+
public void deleteItemWithoutVersion_andOptimisticLockingEnabled_shouldSucceed() {
352+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
353+
Key recordKey = Key.builder()
354+
.partitionValue(originalItem.getId())
355+
.sortValue(originalItem.getSort())
356+
.build();
357+
mappedTable.putItem(originalItem).join();
358+
359+
// Retrieve the item
360+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
361+
362+
// Delete the item using a transaction
363+
TransactWriteItemsEnhancedRequest request =
364+
TransactWriteItemsEnhancedRequest.builder()
365+
.addDeleteItem(mappedTable, retrievedItem)
366+
.build();
367+
368+
enhancedClient.transactWriteItems(request).join();
369+
370+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
371+
assertThat(deletedItem).isNull();
372+
}
373+
374+
@Test
375+
public void deleteItemWithoutVersion_andOptimisticLockingDisabled_shouldSucceed() {
376+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
377+
Key recordKey = Key.builder()
378+
.partitionValue(originalItem.getId())
379+
.sortValue(originalItem.getSort())
380+
.build();
381+
mappedTable.putItem(originalItem).join();
382+
383+
// Retrieve the item
384+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
385+
386+
// Delete the item using a transaction
387+
TransactWriteItemsEnhancedRequest request =
388+
TransactWriteItemsEnhancedRequest.builder()
389+
.addDeleteItem(mappedTable, retrievedItem)
390+
.build();
391+
392+
enhancedClient.transactWriteItems(request).join();
393+
394+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
395+
assertThat(deletedItem).isNull();
396+
}
397+
398+
@Test
399+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMatch_shouldSucceed() {
400+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
401+
Key recordKey = Key.builder()
402+
.partitionValue(originalItem.getId())
403+
.sortValue(originalItem.getSort())
404+
.build();
405+
recordWithVersionMappedTable.putItem(originalItem).join();
406+
407+
// Retrieve the item
408+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
409+
410+
// Delete the item using a transaction
411+
TransactWriteItemsEnhancedRequest request =
412+
TransactWriteItemsEnhancedRequest.builder()
413+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
414+
.build();
415+
416+
enhancedClient.transactWriteItems(request).join();
417+
418+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
419+
assertThat(deletedItem).isNull();
420+
}
421+
422+
@Test
423+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMismatch_shouldFail() {
424+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
425+
Key recordKey = Key.builder()
426+
.partitionValue(originalItem.getId())
427+
.sortValue(originalItem.getSort())
428+
.build();
429+
430+
recordWithVersionMappedTable.putItem(originalItem).join();
431+
432+
// Retrieve the item and modify it separately
433+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
434+
modifiedItem.setStringAttribute("Updated Item");
435+
436+
// Update the item, which will increment the version
437+
recordWithVersionMappedTable.updateItem(modifiedItem);
438+
439+
440+
// Now attempt to delete the original item using a transaction
441+
TransactWriteItemsEnhancedRequest request =
442+
TransactWriteItemsEnhancedRequest.builder()
443+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
444+
.build();
445+
446+
// enhancedClient.transactWriteItems(request).join();
447+
448+
assertThatThrownBy(() -> enhancedClient.transactWriteItems(request).join())
449+
.isInstanceOf(CompletionException.class)
450+
.satisfies(e ->
451+
assertThat(((TransactionCanceledException) e.getCause())
452+
.cancellationReasons()
453+
.stream()
454+
.anyMatch(reason ->
455+
"ConditionalCheckFailed".equals(reason.code())
456+
&& "The conditional request failed".equals(reason.message())))
457+
.isTrue());
458+
}
459+
460+
@Test
461+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMatch_shouldSucceed() {
462+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
463+
Key recordKey = Key.builder()
464+
.partitionValue(originalItem.getId())
465+
.sortValue(originalItem.getSort())
466+
.build();
467+
recordWithVersionMappedTable.putItem(originalItem).join();
468+
469+
// Retrieve the item
470+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
471+
472+
// Delete the item using a transaction
473+
TransactWriteItemsEnhancedRequest request =
474+
TransactWriteItemsEnhancedRequest.builder()
475+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
476+
.build();
477+
478+
enhancedClient.transactWriteItems(request).join();
479+
480+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
481+
assertThat(deletedItem).isNull();
482+
}
483+
484+
@Test
485+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMismatch_shouldSucceed() {
486+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
487+
Key recordKey = Key.builder()
488+
.partitionValue(originalItem.getId())
489+
.sortValue(originalItem.getSort())
490+
.build();
491+
492+
recordWithVersionMappedTable.putItem(originalItem).join();
493+
494+
// Retrieve the item and modify it separately
495+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
496+
modifiedItem.setStringAttribute("Updated Item");
497+
498+
// Update the item, which will increment the version
499+
recordWithVersionMappedTable.updateItem(modifiedItem);
500+
501+
// Now attempt to delete the original item using a transaction
502+
TransactWriteItemsEnhancedRequest request =
503+
TransactWriteItemsEnhancedRequest.builder()
504+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
505+
.build();
506+
507+
enhancedClient.transactWriteItems(request).join();
508+
509+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
510+
assertThat(deletedItem).isNull();
511+
}
344512
}

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey;
2020
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey;
2121
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey;
22+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute;
2223

2324
import java.util.Arrays;
2425
import java.util.List;
2526
import java.util.UUID;
2627
import java.util.stream.Collectors;
2728
import java.util.stream.IntStream;
2829
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
30+
import software.amazon.awssdk.enhanced.dynamodb.model.RecordWithVersion;
2931
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
3032
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3133
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -102,4 +104,96 @@ protected static String getStringAttrValue(int numChars) {
102104
return new String(chars);
103105
}
104106

107+
108+
protected static final TableSchema<RecordWithVersion> RECORD_WITH_VERSION_TABLE_SCHEMA =
109+
110+
111+
112+
StaticTableSchema.builder(RecordWithVersion.class)
113+
114+
115+
.newItemSupplier(RecordWithVersion::new)
116+
117+
118+
.addAttribute(String.class, a -> a.name("id")
119+
120+
121+
.getter(RecordWithVersion::getId)
122+
123+
124+
.setter(RecordWithVersion::setId)
125+
126+
127+
.tags(primaryPartitionKey(), secondaryPartitionKey("index1")))
128+
129+
130+
.addAttribute(Integer.class, a -> a.name("sort")
131+
132+
133+
.getter(RecordWithVersion::getSort)
134+
135+
136+
.setter(RecordWithVersion::setSort)
137+
138+
139+
.tags(primarySortKey(), secondarySortKey("index1")))
140+
141+
142+
.addAttribute(Integer.class, a -> a.name("value")
143+
144+
145+
.getter(RecordWithVersion::getValue)
146+
147+
148+
.setter(RecordWithVersion::setValue))
149+
150+
151+
.addAttribute(String.class, a -> a.name("gsi_id")
152+
153+
154+
.getter(RecordWithVersion::getGsiId)
155+
156+
157+
.setter(RecordWithVersion::setGsiId)
158+
159+
160+
.tags(secondaryPartitionKey("gsi_keys_only")))
161+
162+
163+
.addAttribute(Integer.class, a -> a.name("gsi_sort")
164+
165+
166+
.getter(RecordWithVersion::getGsiSort)
167+
168+
169+
.setter(RecordWithVersion::setGsiSort)
170+
171+
172+
.tags(secondarySortKey("gsi_keys_only")))
173+
174+
175+
.addAttribute(String.class, a -> a.name("stringAttribute")
176+
177+
178+
.getter(RecordWithVersion::getStringAttribute)
179+
180+
181+
.setter(RecordWithVersion::setStringAttribute))
182+
183+
184+
.addAttribute(Integer.class, a -> a.name("version")
185+
186+
187+
.getter(RecordWithVersion::getVersion)
188+
189+
190+
.setter(RecordWithVersion::setVersion)
191+
192+
193+
.tags(versionAttribute(0L, 1L, true))) // startAt=0, incrementBy=1,
194+
// optimisticLockingOnDelete=true
195+
196+
197+
.build();
198+
105199
}

0 commit comments

Comments
 (0)