Skip to content

Commit b61df77

Browse files
committed
Changes the default behavior for DynamoDb Enhanced atomic counter extension to filter out any counter attributes in the item map if present
1 parent 6530cd7 commit b61df77

File tree

4 files changed

+84
-10
lines changed

4 files changed

+84
-10
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": "AWS SDK for Java v2 - DynamoDb Enhanced",
4+
"contributor": "",
5+
"description": "Changes the default behavior of the DynamoDb Enhanced atomic counter extension to automatically filter out any counter attributes in the item to be updated. This allows users to read and update items without DynamoDb collision errors."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AtomicCounterExtension.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
2020
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.ifNotExists;
2121

22+
import java.util.ArrayList;
2223
import java.util.Collections;
2324
import java.util.HashMap;
25+
import java.util.List;
2426
import java.util.Map;
2527
import java.util.stream.Collectors;
2628
import software.amazon.awssdk.annotations.SdkPublicApi;
29+
import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider;
2730
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
2831
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
2932
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
@@ -37,10 +40,11 @@
3740
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
3841
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3942
import software.amazon.awssdk.utils.CollectionUtils;
43+
import software.amazon.awssdk.utils.Logger;
4044

4145
/**
42-
* This extension enables atomic counter attributes to be written to the database.
43-
* The extension is loaded by default when you instantiate a
46+
* This extension enables atomic counter attributes to be changed in DynamoDb by creating instructions for modifying
47+
* an existing value or setting a start value. The extension is loaded by default when you instantiate a
4448
* {@link DynamoDbEnhancedClient} and only needs to be added to the client if you
4549
* are adding custom extensions to the client.
4650
* <p>
@@ -56,8 +60,7 @@
5660
* <p>
5761
* Every time a new update of the record is successfully written to the database, the counter will be updated automatically.
5862
* By default, the counter starts at 0 and increments by 1 for each update. The tags provide the capability of adjusting
59-
* the counter start and increment/decrement values such as described in
60-
* {@link DynamoDbAtomicCounter}.
63+
* the counter start and increment/decrement values such as described in {@link DynamoDbAtomicCounter}.
6164
* <p>
6265
* Example 1: Using a bean based table schema
6366
* <pre>
@@ -86,10 +89,16 @@
8689
* }
8790
* </pre>
8891
* <p>
89-
* <b>NOTE: </b>When using putItem, the counter will be reset to its start value.
92+
* <b>NOTES: </b>
93+
* <ul>
94+
* <li>When using putItem, the counter will be reset to its start value.</li>
95+
* <li>The extension will remove any existing occurrences of the atomic counter attributes.</li>
96+
* </ul>
9097
*/
9198
@SdkPublicApi
9299
public final class AtomicCounterExtension implements DynamoDbEnhancedClientExtension {
100+
101+
private static final Logger log = Logger.loggerFor(AtomicCounterExtension.class);
93102
private AtomicCounterExtension() {
94103
}
95104

@@ -118,6 +127,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
118127
break;
119128
case UPDATE_ITEM:
120129
modificationBuilder.updateExpression(createUpdateExpression(counters));
130+
modificationBuilder.transformedItem(filterFromItem(counters, context.items()));
121131
break;
122132
default: break;
123133
}
@@ -136,6 +146,22 @@ private Map<String, AttributeValue> addToItem(Map<String, AtomicCounter> counter
136146
return Collections.unmodifiableMap(itemToTransform);
137147
}
138148

149+
private Map<String, AttributeValue> filterFromItem(Map<String, AtomicCounter> counters, Map<String, AttributeValue> items) {
150+
Map<String, AttributeValue> itemToTransform = new HashMap<>(items);
151+
List<String> removedAttributes = new ArrayList<>();
152+
for (String attributeName : counters.keySet()) {
153+
if (itemToTransform.containsKey(attributeName)) {
154+
itemToTransform.remove(attributeName);
155+
removedAttributes.add(attributeName);
156+
}
157+
}
158+
if (!removedAttributes.isEmpty()) {
159+
log.debug(() -> String.format("Filtered atomic counter attributes from existing update item to avoid collisions: %s",
160+
String.join(",", removedAttributes)));
161+
}
162+
return Collections.unmodifiableMap(itemToTransform);
163+
}
164+
139165
private SetAction counterAction(Map.Entry<String, AtomicCounter> e) {
140166
String attributeName = e.getKey();
141167
AtomicCounter counter = e.getValue();

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AtomicCounterExtensionTest.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,37 @@ public void beforeWrite_updateItemOperation_noCounters_noChanges() {
112112
.tableMetadata(SIMPLE_ITEM_MAPPER.tableMetadata())
113113
.operationName(OperationName.UPDATE_ITEM)
114114
.operationContext(PRIMARY_CONTEXT).build());
115-
116115
assertThat(result.transformedItem()).isNull();
117116
assertThat(result.updateExpression()).isNull();
118117
}
119118

119+
@Test
120+
public void beforeWrite_updateItemOperation_hasCountersInItem_createsUpdateExpressionAndFilters() {
121+
AtomicCounterItem atomicCounterItem = new AtomicCounterItem();
122+
atomicCounterItem.setId(RECORD_ID);
123+
atomicCounterItem.setCustomCounter(255L);
124+
125+
Map<String, AttributeValue> items = ITEM_MAPPER.itemToMap(atomicCounterItem, true);
126+
assertThat(items).hasSize(2);
127+
128+
WriteModification result =
129+
atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
130+
.items(items)
131+
.tableMetadata(ITEM_MAPPER.tableMetadata())
132+
.operationName(OperationName.UPDATE_ITEM)
133+
.operationContext(PRIMARY_CONTEXT).build());
134+
135+
assertThat(result.transformedItem()).isNotNull();
136+
assertThat(result.transformedItem()).hasSize(1);
137+
assertThat(result.updateExpression()).isNotNull();
138+
139+
List<SetAction> setActions = result.updateExpression().setActions();
140+
assertThat(setActions).hasSize(2);
141+
142+
verifyAction(setActions, "customCounter", "5", "5");
143+
verifyAction(setActions, "defaultCounter", "-1", "1");
144+
}
145+
120146
@Test
121147
public void beforeWrite_putItemOperation_hasCounters_createsItemTransform() {
122148
AtomicCounterItem atomicCounterItem = new AtomicCounterItem();

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,31 @@ public void createViaPut_incrementsCorrectly() {
112112
}
113113

114114
@Test
115-
public void createViaUpdate_settingCounterInPojo_throwsException() {
115+
public void createViaUpdate_settingCounterInPojo_hasNoEffect() {
116116
AtomicCounterRecord record = new AtomicCounterRecord();
117117
record.setId(RECORD_ID);
118118
record.setDefaultCounter(10L);
119119
record.setAttribute1(STRING_VALUE);
120120

121-
assertThatThrownBy(() -> mappedTable.updateItem(record))
122-
.isInstanceOf(DynamoDbException.class)
123-
.hasMessageContaining("Two document paths");
121+
mappedTable.updateItem(record);
122+
AtomicCounterRecord persistedRecord = mappedTable.getItem(record);
123+
assertThat(persistedRecord.getAttribute1()).isEqualTo(STRING_VALUE);
124+
assertThat(persistedRecord.getDefaultCounter()).isEqualTo(0L);
125+
assertThat(persistedRecord.getCustomCounter()).isEqualTo(10L);
126+
assertThat(persistedRecord.getDecreasingCounter()).isEqualTo(-20L);
127+
}
128+
129+
@Test
130+
public void updateItem_retrievedFromDb_shouldNotThrowException() {
131+
AtomicCounterRecord record = new AtomicCounterRecord();
132+
record.setId(RECORD_ID);
133+
record.setAttribute1(STRING_VALUE);
134+
mappedTable.updateItem(record);
135+
136+
AtomicCounterRecord retrievedRecord = mappedTable.getItem(record);
137+
retrievedRecord.setAttribute1("ChangingThisAttribute");
138+
139+
mappedTable.updateItem(retrievedRecord);
124140
}
125141

126142
@Test

0 commit comments

Comments
 (0)