Skip to content

Support timeToLive attributes in DynamoDb Enhanced Client #6152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon DynamoDB Enhanced Client",
"contributor": "",
"description": "Added support for TimeToLive attributes in DynamoDB Enhanced Client"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.Page;
Expand All @@ -34,6 +35,7 @@
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity;
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
Expand Down Expand Up @@ -947,4 +949,12 @@ default CompletableFuture<Void> deleteTable() {
default CompletableFuture<DescribeTableEnhancedResponse> describeTable() {
throw new UnsupportedOperationException();
}

default CompletableFuture<DescribeTimeToLiveEnhancedResponse> describeTimeToLive() {
throw new UnsupportedOperationException();
}

default CompletableFuture<UpdateTimeToLiveEnhancedResponse> updateTimeToLive(boolean enabled) {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.Page;
Expand All @@ -34,6 +35,7 @@
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity;
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
Expand Down Expand Up @@ -926,4 +928,12 @@ default void deleteTable() {
default DescribeTableEnhancedResponse describeTable() {
throw new UnsupportedOperationException();
}

default DescribeTimeToLiveEnhancedResponse describeTimeToLive() {
throw new UnsupportedOperationException();
}

default UpdateTimeToLiveEnhancedResponse updateTimeToLive(boolean enabled) {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.enhanced.dynamodb.extensions;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.Validate;

@SdkPublicApi
@ThreadSafe
public final class TimeToLiveExtension implements DynamoDbEnhancedClientExtension {

public static final String CUSTOM_METADATA_KEY = "TimeToLiveExtension:TimeToLiveAttribute";

private TimeToLiveExtension() {
}

public static TimeToLiveExtension.Builder builder() {
return new TimeToLiveExtension.Builder();
}

/**
* @return an Instance of {@link TimeToLiveExtension}
*/
public static TimeToLiveExtension create() {
return new TimeToLiveExtension();
}

@Override
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
Map<String, ?> customTTLMetadata = context.tableMetadata()
.customMetadataObject(CUSTOM_METADATA_KEY, Map.class).orElse(null);

if (customTTLMetadata != null) {
String ttlAttributeName = (String) customTTLMetadata.get("attributeName");
String baseFieldName = (String) customTTLMetadata.get("baseField");
Long duration = (Long) customTTLMetadata.get("duration");
TemporalUnit unit = (TemporalUnit) customTTLMetadata.get("unit");

Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());

if (!itemToTransform.containsKey(ttlAttributeName) && StringUtils.isNotBlank(baseFieldName)
&& itemToTransform.containsKey(baseFieldName)) {
Object baseFieldValue = context.tableSchema().converterForAttribute(baseFieldName)
.transformTo(itemToTransform.get(baseFieldName));
Long ttlEpochSeconds = computeTTLFromBase(baseFieldValue, duration, unit);
itemToTransform.put(ttlAttributeName, AttributeValue.builder().n(String.valueOf(ttlEpochSeconds)).build());

return WriteModification.builder().transformedItem(Collections.unmodifiableMap(itemToTransform)).build();
}
}

return WriteModification.builder().build();
}

private static Long computeTTLFromBase(Object baseValue, long duration, TemporalUnit unit) {
if (baseValue instanceof Instant) {
return ((Instant) baseValue).plus(duration, unit).getEpochSecond();
}
if (baseValue instanceof LocalDate) {
return ((LocalDate) baseValue).atStartOfDay(ZoneOffset.UTC).plus(duration, unit).toEpochSecond();
}
if (baseValue instanceof LocalDateTime) {
return ((LocalDateTime) baseValue).plus(duration, unit).toEpochSecond(ZoneOffset.UTC);
}
if (baseValue instanceof LocalTime) {
return LocalDate.now().atTime((LocalTime) baseValue).plus(duration, unit).toEpochSecond(ZoneOffset.UTC);
}
if (baseValue instanceof ZonedDateTime) {
return ((ZonedDateTime) baseValue).plus(duration, unit).toEpochSecond();
}
if (baseValue instanceof Long) {
return (Long) baseValue + Duration.of(duration, unit).getSeconds();
}

throw new IllegalArgumentException("Unsupported base field type for TTL computation: " + baseValue.getClass().getName());
}

public static final class Builder {
private Builder() {
}

public TimeToLiveExtension build() {
return new TimeToLiveExtension();
}
}

public static final class AttributeTags {
private AttributeTags() {
}

/**
* Used to explicitly designate an attribute to determine the TTL on the table.
*
* <p><b>How this works</b></p>
* <ul>
* <li>If a TTL attribute is set, it takes precedence over <i>baseField</i>.</li>
* <li>If no TTL attribute is set, it checks for <i>baseField</i>.</li>
* <li>If <i>baseField</i> is present, the TTL is calculated using its value, <i>duration</i>, and <i>unit</i>.</li>
* <li>The final TTL value is converted to epoch seconds before storing in DynamoDB.</li>
* </ul>
*
* @param baseField Optional attribute name used to determine the TTL value.
* @param duration Additional long value used for TTL calculation.
* @param unit {@link ChronoUnit} value specifying the TTL duration unit.
*/
public static StaticAttributeTag timeToLiveAttribute(String baseField, long duration, ChronoUnit unit) {
return new TimeToLiveAttribute(baseField, duration, unit);
}
}

private static final class TimeToLiveAttribute implements StaticAttributeTag {

public String baseField;
public long duration;
public ChronoUnit unit;

private TimeToLiveAttribute(String baseField, long duration, ChronoUnit unit) {
this.baseField = baseField;
this.duration = duration;
this.unit = unit;
}

@Override
public <R> void validateType(String attributeName, EnhancedType<R> type,
AttributeValueType attributeValueType) {

Validate.notNull(type, "type is null");
Validate.notNull(type.rawClass(), "rawClass is null");
Validate.notNull(attributeValueType, "attributeValueType is null");

if (!type.rawClass().equals(Long.class)) {
throw new IllegalArgumentException(String.format(
"Attribute '%s' of type %s is not a suitable type to be used as a TTL attribute. Only type Long " +
"is supported.", attributeName, type.rawClass()));
}
}

@Override
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
AttributeValueType attributeValueType) {
Map<String, Object> customMetadataMap = new HashMap<>();
customMetadataMap.put("attributeName", attributeName);
customMetadataMap.put("baseField", baseField);
customMetadataMap.put("duration", duration);
customMetadataMap.put("unit", unit);

return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY,
Collections.unmodifiableMap(customMetadataMap));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.enhanced.dynamodb.extensions.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.temporal.ChronoUnit;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.TimeToLiveAttributeTags;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;

/**
* Annotation used to mark an attribute in a DynamoDB-enhanced client model as a Time-To-Live (TTL) field.
* <p>
* This annotation allows automatic computation and assignment of a TTL value based on another field (the {@code baseField})
* and a time offset defined by {@code duration} and {@code unit}. The TTL value is stored in epoch seconds and
* can be configured to expire items from the table automatically.
* <p>
* To use this, the annotated method should return a {@link Long} value, which will be populated by the SDK at write time.
* The {@code baseField} can be a temporal type such as {@link java.time.Instant}, {@link java.time.LocalDate},
* {@link java.time.LocalDateTime}, etc., or a {@link Long} representing epoch seconds directly, serving as the reference point
* for TTL calculation.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BeanTableSchemaAttributeTag(TimeToLiveAttributeTags.class)
@SdkPublicApi
public @interface DynamoDbTimeToLiveAttribute {

/**
* The name of the attribute whose value will serve as the base for TTL computation.
* This can be a temporal type (e.g., {@link java.time.Instant}, {@link java.time.LocalDateTime})
* or a {@link Long} representing epoch seconds.
*
* @return the attribute name to use as the base timestamp for TTL
*/
String baseField() default "";

/**
* The amount of time to add to the {@code baseField} when computing the TTL value.
* The resulting time will be converted to epoch seconds.
*
* @return the time offset to apply to the base field
*/
long duration() default 0;

/**
* The time unit associated with the {@code duration}. Defaults to {@link ChronoUnit#SECONDS}.
*
* @return the time unit to use with the duration
*/
ChronoUnit unit() default ChronoUnit.SECONDS;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,20 @@
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteTableOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DescribeTableOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DescribeTimeToLiveOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.GetItemOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.PaginatedTableOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.QueryOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.ScanOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.TableOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateTimeToLiveOperation;
import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.PagePublisher;
Expand All @@ -52,9 +55,14 @@
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse;
import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveRequest;
import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveResponse;
import software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveResponse;

@SdkInternalApi
public final class DefaultDynamoDbAsyncTable<T> implements DynamoDbAsyncTable<T> {
Expand Down Expand Up @@ -326,6 +334,20 @@ public CompletableFuture<DescribeTableEnhancedResponse> describeTable() {
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
}

@Override
public CompletableFuture<DescribeTimeToLiveEnhancedResponse> describeTimeToLive() {
TableOperation<T, DescribeTimeToLiveRequest, DescribeTimeToLiveResponse, DescribeTimeToLiveEnhancedResponse> operation =
DescribeTimeToLiveOperation.create();
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
}

@Override
public CompletableFuture<UpdateTimeToLiveEnhancedResponse> updateTimeToLive(boolean enabled) {
TableOperation<T, UpdateTimeToLiveRequest, UpdateTimeToLiveResponse, UpdateTimeToLiveEnhancedResponse> operation =
UpdateTimeToLiveOperation.create(enabled);
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Loading