aws-sdk-java icon indicating copy to clipboard operation
aws-sdk-java copied to clipboard

TransactionWriteRequest / DynamoDBMapper::transactionWrite ignores SaveBehavior.CLOBBER

Open joelc opened this issue 5 years ago • 6 comments

  • Expected behavior of the SDK

Version field should be ignored if SaveBehavior.CLOBBER is provided in the DynamoDBMapperConfig passed to DynamoDBMapper::transactionWrite.

  • Actual behavior exhibited

Version field is not ignored.

  • Details of my application environment

SDK version: 1.11.722 JRE version: Oracle 11.0.6

  • The exception stacktrace
Exception in thread "main" com.amazonaws.services.dynamodbv2.model.TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed] (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: TransactionCanceledException; Request ID: a1e23b6c-73c8-4a55-9e8c-c90d6582a113)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1799)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleServiceErrorResponse(AmazonHttpClient.java:1383)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1359)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1139)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:796)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:764)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:738)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:698)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:680)
    at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:544)
    at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:524)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:5110)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:5077)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeTransactWriteItems(AmazonDynamoDBClient.java:4201)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.transactWriteItems(AmazonDynamoDBClient.java:4167)
    at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.transactionWrite(DynamoDBMapper.java:1122)
    at com.example.reconpoc.document.Driver.selfContainedTestCase(Driver.java:73)
    at com.example.reconpoc.document.Driver.main(Driver.java:31)
  • Minimal Working Example

Class MyItem:

@DynamoDBTable(tableName = "IssueSampleTable")
public class MyItem {
    @DynamoDBHashKey(attributeName = "id")
    private String id;

    @DynamoDBVersionAttribute
    private Long version;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }
}

Driver code (you will need to provide your own AmazonDynamoDB instance):

public static void selfContainedTestCase(AmazonDynamoDB ddb) {
    DynamoDBMapper mapper = new DynamoDBMapper(ddb);

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SETUP TABLE

    // try to delete the table, if it exists

    try {
        ddb.deleteTable(mapper.generateDeleteTableRequest(MyItem.class));
    } catch (ResourceNotFoundException e) {
        // no problemo, table did not exist
    }

    // create new table

    ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput(5L, 5L);
    CreateTableRequest request = mapper.generateCreateTableRequest(MyItem.class);
    request.setProvisionedThroughput(provisionedThroughput);
    ddb.createTable(request);

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SAVE AN INSTANCE OF MYITEM

    MyItem myItemToSave = new MyItem();
    myItemToSave.setId("SOME ID");

    mapper.save(myItemToSave);

    System.out.println("Saved! Id = " + myItemToSave.getVersion());

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // ATTEMPT TO DELETE THE SAME INSTANCE IN A TRANSACTION, WITH CLOBBER

    MyItem myItemToDelete = new MyItem();
    myItemToDelete.setId("SOME ID");

    TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest();

    transactionWriteRequest.addDelete(myItemToDelete);

    // this should not throw - but it does!
    mapper.transactionWrite(transactionWriteRequest, DynamoDBMapperConfig.builder()
                                                                         .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.CLOBBER)
                                                                         .build());

}

joelc avatar Feb 16 '20 03:02 joelc

Issue is present in .723 also.

joelc avatar Feb 16 '20 04:02 joelc

Hi @joelc actually, transactionWrite does not support neither SaveBehavior nor versioning annotations, per documentation - javadoc

debora-ito avatar Feb 18 '20 20:02 debora-ito

Hi @debora-ito,

I edited my previous response about 12 times. Doing a sneaky ninja delete and repost:

I think the documentation says one thing and the code does another.

If this were true:

Furthermore, put and update work as if SaveBehavior is set as CLOBBER.

Then using the MyItem class above in conjunction with this method should not throw because of a version mismatch:

public static void selfContainedTestCase_TransactionWriteDoesNOTClobber(AmazonDynamoDB ddb) {
    DynamoDBMapper mapper = new DynamoDBMapper(ddb);

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SETUP TABLE

    // try to delete the table, if it exists
    try {
        ddb.deleteTable(mapper.generateDeleteTableRequest(MyItem.class));
    } catch (ResourceNotFoundException e) {
        // no problemo, table did not exis.
    }

    // create new table
    ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput(5L, 5L);
    CreateTableRequest request = mapper.generateCreateTableRequest(MyItem.class);
    request.setProvisionedThroughput(provisionedThroughput);
    ddb.createTable(request);

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SAVE AN INSTANCE OF MYITEM
    {
        System.out.println("Attempting to save MyItem instance (version not set)");

        MyItem myItemToSave = new MyItem();
        myItemToSave.setId("SOME ID");

        TransactionWriteRequest twr = new TransactionWriteRequest();
        twr.addPut(myItemToSave);
        mapper.transactionWrite(twr);

        System.out.println("Saved! Assigned Id = " + myItemToSave.getVersion());
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SAVE ANOTHER INSTANCE OF MYITEM
    {
        System.out.println("Attempting to save MyItem instance (version not set)");

        MyItem myItemToSave = new MyItem();
        myItemToSave.setId("SOME ID");

        TransactionWriteRequest twr = new TransactionWriteRequest();
        twr.addPut(myItemToSave);
        mapper.transactionWrite(twr);

        System.out.println("Saved! Assigned Id = " + myItemToSave.getVersion());
    }

}

However, it does throw - this code yields:

Attempting to save MyItem instance (version not set)
Saved! Assigned Id = 1
Attempting to save MyItem instance (version not set)
Exception in thread "main" com.amazonaws.services.dynamodbv2.model.TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed] (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: TransactionCanceledException; Request ID: 72ce4cc4-6981-482b-a911-ab5cd97d17e2)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1799)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleServiceErrorResponse(AmazonHttpClient.java:1383)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1359)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1139)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:796)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:764)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:738)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:698)
	at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:680)
	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:544)
	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:524)
	at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:5110)
	at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:5077)
	at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeTransactWriteItems(AmazonDynamoDBClient.java:4201)
	at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.transactWriteItems(AmazonDynamoDBClient.java:4167)
	at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.transactionWrite(DynamoDBMapper.java:1122)
	at com.amazonaws.services.dynamodbv2.datamodeling.AbstractDynamoDBMapper.transactionWrite(AbstractDynamoDBMapper.java:163)
	at com.example.reconpoc.document.Driver.selfContainedTestCase_TransactionWriteDoesNOTClobber(Driver.java:71)
	at com.example.reconpoc.document.Driver.main(Driver.java:24)

It appears that DynamoDBMapper::generateTransactWriteItem explicitly computes / adds a condition using versionAttributeConditionExpressionGenerator.

joelc avatar Feb 19 '20 04:02 joelc

You are right, transactionWrite added support for optimistic locking at the end of 2019, the documentation is out of date.

Marking as a feature request, will discuss it with the team.

debora-ito avatar Mar 03 '20 23:03 debora-ito

Do we have any updates on this feature?

stathari avatar Sep 22 '20 09:09 stathari

You are right, transactionWrite added support for optimistic locking at the end of 2019, the documentation is out of date.

Marking as a feature request, will discuss it with the team.

Maybe I'm not following, but if SaveBehavior.CLOBBER is no longer the default then what is? SaveBehavior.PUT?

aguerard avatar Jan 18 '21 06:01 aguerard

This is a very old issue that is probably not getting as much attention as it deserves. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to provide a comment or open a new issue.

github-actions[bot] avatar Jan 18 '24 09:01 github-actions[bot]