1

I am trying to setup an Optimistic version check before I update the record in DynamoDB. But I keep getting ConditionalCheckFailedException from the first item save - when there is no hash key existing.

Can someone tell me what's wrong in my code?

Here's my code

final DynamoDBMapperConfig.Builder builder = new DynamoDBMapperConfig.Builder();
builder.setConsistentReads(DynamoDBMapperConfig.ConsistentReads.CONSISTENT);
builder.setTableNameOverride(new DynamoDBMapperConfig.TableNameOverride(tableName));
DynamoDBMapperConfig mapperConfig = builder.build();
this.dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB, mapperConfig);
@Data
@DynamoDBTable(tableName = "OVERRIDE_AT_RUNTIME")
public class ItemRecord
{
    public static final String CONTENT_ID = "content_id";
    public static final String ACTION_COUNT = "action_count";
    private static final String VERSION = "version";

    @DynamoDBHashKey(attributeName = CONTENT_ID)
    private String contentId;
    @DynamoDBAttribute(attributeName = ACTION_COUNT)
    private int actionCount;
    @DynamoDBVersionAttribute(attributeName = VERSION)
    private long version;

    public TitleMatchProgressRecord() {}

    public TitleMatchProgressRecord(final String contentId)
    {
        this.contentId = contentId;
        this.actionCount = 0;
    }

    @DynamoDBIgnore
    public void incrementActionCount()
    {
        actionCount++;
    }
}
try {
    ItemRecord itemRecord = loadRecord(contentId);
    if (itemRecord == null) { // first item
        itemRecord = new ItemRecord(contentId);
        itemRecord.incrementActionCount();
        DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression();
        final Map<String, ExpectedAttributeValue> expectedAttributes = new HashMap<>();
        expectedAttributes.put(ItemRecord.CONTENT_ID, new ExpectedAttributeValue(false));
        saveExpression.setExpected(expectedAttributes);

        dynamoDBMapper.save(itemRecord, saveExpression);

    } else {
        itemRecord.incrementActionCount();
        itemRecord.resetLastModifiedAt();
        dynamoDBMapper.save(itemRecord);
    }
} catch (ConditionalCheckFailedException e) {
    // retry
}

Error trace:

   [testng] com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException: The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: UV4V9BSIE78ETRQC9LG4BOB23; Proxy: null)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1811)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleServiceErrorResponse(AmazonHttpClient.java:1395)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1371)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1145)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:802)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:770)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:744)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:704)
   [testng]     at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:686)
   [testng]     at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:550)
   [testng]     at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:530)
   [testng]     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:5110)
   [testng]     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:5077)
   [testng]     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeUpdateItem(AmazonDynamoDBClient.java:4696)
   [testng]     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.updateItem(AmazonDynamoDBClient.java:4662)
   [testng]     at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper$SaveObjectHandler.doUpdateItem(DynamoDBMapper.java:871)
   [testng]     at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper$2.executeLowLevelRequest(DynamoDBMapper.java:611)
   [testng]     at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper$SaveObjectHandler.execute(DynamoDBMapper.java:750)
   [testng]     at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.save(DynamoDBMapper.java:640)
   [testng]     at com.amazonaws.services.dynamodbv2.datamodeling.AbstractDynamoDBMapper.save(AbstractDynamoDBMapper.java:123)
6
  • It’s almost certainly because the attribute does not exist, and DynamoDB mapper is creating a condition expression that requires the attribute to exist. Do you have any logs where you could see the request sent by the low-level client? Commented Jun 1, 2020 at 2:22
  • @MatthewPope I updated with the error trace. But I am not sure if you can get useful info from it Commented Jun 1, 2020 at 2:38
  • Actually, it looks the the condition expression you need (roughly attribute_not_exists(x) OR x = y) is not supported by DynamoDBMapper. github.com/aws/aws-sdk-java/issues/534 Commented Jun 1, 2020 at 3:03
  • @MatthewPope but it's throwing the same error without any expressions. Doesn't backend code have to handle the version check automatically? Commented Jun 1, 2020 at 3:42
  • Sorry, I forgot that you had a version attribute and a save expression. The version attribute should probably work automatically. Commented Jun 1, 2020 at 3:44

1 Answer 1

1

To do "optimistic locking" in DynamoDB, you keep a version attribute on items - which you called "action count". If there is a possibility that the item did not exist yet, and needs to be created for the first time - you must handle this case explicitly. You cannot just assume the item always exists and has a previous action count... For example, you can do this:

  1. Read the item (you can do the read with the cheaper "eventual consistency"). The item may not previously exist - this is fine.

  2. Based on what you read, create the new version of the item. Increment "action count" by one if the item existed, or if it didn't, set action count to 0.

  3. Perform the write, with a condition expression. If the item previously existed, the condition should require that the action count is still equal to its read (pre-increment) value. If the item did not previously exist, the condition should be that the item does not exit.

  4. If the conditional update failed on a failed condition, goto 1 to try again.

Note how if two concurrent writes try to create a new item, one will succeed to create it (with action count 0) and the other will need to try again and actually update the item (resulting eventually with action count 1).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.