aws / aws-sdk-java-v2

The official AWS SDK for Java - Version 2
Apache License 2.0
2.16k stars 840 forks source link

@DynamoDbVersionAttribute Annotation Does not work if used in conjunction with @DynamoDbSortKey for subsequent PUT operations. #5316

Open jodom961 opened 3 months ago

jodom961 commented 3 months ago

Describe the bug

If you use the @DynamoDbVersionAttribute in conjunction with @SortKey, the initial PutItem request will work, and correctly store the item with 1 for Version, but subsequent PutItems will not auto increment to Version 2 (Note, it does work with UpdateItem if this is used on a normal attribute that's not a @SortKey/@PrimaryKey, like the SomeLatestItem table below)

Put Item Fails on a condition Check from this VersionedRecordExtension whether or not the version passed exists(and is == current version) or does not exist (and is == current version + 1 )

@Value
@Builder
@AllArgsConstructor
@DynamoDbImmutable(builder = SomeVersionsItem.SomeVersionsItemBuilder.class)
public class SomeVersionsItem {
    @Getter(onMethod_ = {@DynamoDbPartitionKey, @DynamoDbAttribute("Id")})
    private String id;

    // DDbVersionAttribute must be type (int, long)
    @Getter(onMethod_ = {@DynamoDbSortKey, @DynamoDbVersionAttribute, @DynamoDbAttribute("Version")})
    private Long version;

    // Non PK/SK Additional fields
    @Getter(onMethod_ = {@DynamoDbAttribute("SomeAttribute")})
    private String someAttribute;
}

@Value
@Builder
@AllArgsConstructor
@DynamoDbImmutable(builder = SomeLatestItem.SomeLatestItemBuilder.class)
public class SomeLatestItem {

    @Getter(onMethod_ = {@DynamoDbPartitionKey, @DynamoDbAttribute("Id")})
    private String id;

    // DDbVersionAttribute must be type (int, long)
    @With(onMethod_ = {@DynamoDbIgnore})
    @Getter(onMethod_ = {@DynamoDbVersionAttribute, @DynamoDbAttribute("Version")})
    private Long version;
}

Expected Behavior

To bump the version to N+1, and write the new item.

For an update item request when using the @DynamoDbVersionAttribute on a table where Version is not the sort key, it expects you to pass the current version and it auto increments for you.

With the current implementation, and using the ddb enhanced client, you can not write subsequent items to the table. This forces the user to now handle their own versioning logic, and remove the annotation. This is not called out in any docs, and if anything, many of the AWS articles on versioning lead you to make this mistake.

Current Behavior

 ERROR TransmutingContinuationHandler:157 - Internal Failure
software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None] (Service: DynamoDb, Status Code: 400, Request ID: KNKSB2C6NH1T4U57EF26P6C2MNVV4KQNSO5AEMVJF66Q9ASUAAJG)
    at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handleErrorResponse(CombinedResponseHandler.java:125) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handleResponse(CombinedResponseHandler.java:82) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handle(CombinedResponseHandler.java:60) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handle(CombinedResponseHandler.java:41) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:50) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:38) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:72) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:78) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:40) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:55) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:39) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:81) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:36) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:56) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:36) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:50) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:32) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:224) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:103) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:173) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.lambda$execute$1(BaseSyncClientHandler.java:80) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.measureApiCallSuccess(BaseSyncClientHandler.java:182) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:74) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45) ~[AwsJavaSdk-Core-2.0.jar:?]
    at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:53) ~[AwsJavaSdk-Core-AwsCore-2.0.jar:?]
    at software.amazon.awssdk.services.dynamodb.DefaultDynamoDbClient.transactWriteItems(DefaultDynamoDbClient.java:6042) ~[AwsJavaSdk-DynamoDb-2.0.jar:?]
    at software.amazon.awssdk.enhanced.dynamodb.internal.operations.DatabaseOperation.execute(DatabaseOperation.java:82) ~[AwsJavaSdk-DynamoDb-Enhanced-2.0.jar:?]
    at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedClient.transactWriteItems(DefaultDynamoDbEnhancedClient.java:103) ~[AwsJavaSdk-DynamoDb-Enhanced-2.0.jar:?]

Reproduction Steps

public class DynamoDbDAO {
    private final DynamoDbTable<SomeLatestItem> enhancedSomeLatestTable;
    private final DynamoDbTable<SomeVersionsItem> enhancedSomeVersionsTable;
    private final DynamoDbEnhancedClient dynamoDbEnhancedClient;

...

public SomeLatestItem edit() {

       // before edit is called, currentVersion = 1 and item "1234" exists. 

        final SomeLatestItem item = SomeLatestItem.builder().id("1234").version(currentVersion).build();

       // SomeVersionsItem write fails for both currentVersion passed  (Version = 1) 
      // also fails for current version + 1 passed (Version = 2) 
      // since i want a new item, I can't call addUpdateItem for this table. 

        final SomeVersionsItem item = SomeVersionsItem.builder().id("1234").version(currentVersion).build();

        final TransactPutItemEnhancedRequest<SomeVersionsItem> putRequestVersionItem =
                TransactPutItemEnhancedRequest.builder(SomeVersionsItem.class)
                        .item(updatedVersionItemToWrite)
                        .build();

        final TransactUpdateItemEnhancedRequest<SomeLatestItem> updateRequestLatestItem =
                TransactUpdateItemEnhancedRequest.builder(SomeLatestItem.class)
                        .item(updatedLatestItem)
                        .ignoreNulls(true)
                        .build();

        final TransactWriteItemsEnhancedRequest transactReq = TransactWriteItemsEnhancedRequest.builder()
                .addPutItem(enhancedSomeVersionsTable, putRequestVersionItem)
                .addUpdateItem(enhancedSomeLatestTable, updateRequestLatestItem)
                .build();

        dynamoDbEnhancedClient.transactWriteItems(transactReq);

        return SomeLatestItem.builder().id("1234").build();
    }

Possible Solution

No response

Additional Information/Context

No response

AWS Java SDK version used

aws-sdk-java-v2

JDK version used

JDK21

Operating System and version

Amazon Linux 2023 (Lambda JDK 21)