Skip to content

Commit 7cacd91

Browse files
committed
fix function timeout case
1 parent a2c33da commit 7cacd91

File tree

15 files changed

+390
-42
lines changed

15 files changed

+390
-42
lines changed

powertools-e2e-tests/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>powertools-parent</artifactId>
7+
<groupId>software.amazon.lambda</groupId>
8+
<version>1.12.3</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>powertools-e2e-tests</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>11</maven.compiler.source>
16+
<maven.compiler.target>11</maven.compiler.target>
17+
</properties>
18+
19+
</project>

powertools-idempotency/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
<dependency>
123123
<groupId>com.amazonaws</groupId>
124124
<artifactId>aws-lambda-java-tests</artifactId>
125+
<scope>test</scope>
125126
</dependency>
126127
<dependency>
127128
<groupId>com.amazonaws</groupId>

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ public static Idempotency getInstance() {
6565
return Holder.instance;
6666
}
6767

68+
/**
69+
* Can be used in a method which is not the handler to capture the Lambda context,
70+
* to calculate the remaining time before the invocation times out.
71+
*
72+
* @param lambdaContext
73+
*/
74+
public static void registerLambdaContext(Context lambdaContext) {
75+
getInstance().getConfig().setLambdaContext(lambdaContext);
76+
}
77+
6878
/**
6979
* Acts like a builder that can be used to configure {@link Idempotency}
7080
*

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
*/
1414
package software.amazon.lambda.powertools.idempotency;
1515

16+
import com.amazonaws.services.lambda.runtime.Context;
1617
import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
1718

19+
import java.security.MessageDigest;
1820
import java.time.Duration;
1921

2022
/**
@@ -28,6 +30,7 @@ public class IdempotencyConfig {
2830
private final String payloadValidationJMESPath;
2931
private final boolean throwOnNoIdempotencyKey;
3032
private final String hashFunction;
33+
private Context lambdaContext;
3134

3235
private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESPath, boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, long expirationInSeconds, String hashFunction) {
3336
this.localCacheMaxItems = localCacheMaxItems;
@@ -71,12 +74,20 @@ public String getHashFunction() {
7174
/**
7275
* Create a builder that can be used to configure and create a {@link IdempotencyConfig}.
7376
*
74-
* @return a new instance of {@link IdempotencyConfig.Builder}
77+
* @return a new instance of {@link Builder}
7578
*/
7679
public static Builder builder() {
7780
return new Builder();
7881
}
7982

83+
public void setLambdaContext(Context lambdaContext) {
84+
this.lambdaContext = lambdaContext;
85+
}
86+
87+
public Context getLambdaContext() {
88+
return lambdaContext;
89+
}
90+
8091
public static class Builder {
8192

8293
private int localCacheMaxItems = 256;
@@ -203,7 +214,7 @@ public Builder withThrowOnNoIdempotencyKey() {
203214
/**
204215
* Function to use for calculating hashes, by default MD5.
205216
*
206-
* @param hashFunction Can be any algorithm supported by {@link java.security.MessageDigest}, most commons are<ul>
217+
* @param hashFunction Can be any algorithm supported by {@link MessageDigest}, most commons are<ul>
207218
* <li>MD5</li>
208219
* <li>SHA-1</li>
209220
* <li>SHA-256</li></ul>

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
/**
1717
* IdempotencyInconsistentStateException can happen under rare but expected cases
18-
* when persistent state changes in the small-time between put & get requests.
18+
* when persistent state changes in the small-time between put &amp; get requests.
1919
*/
2020
public class IdempotencyInconsistentStateException extends RuntimeException {
2121
private static final long serialVersionUID = -4293951999802300672L;

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package software.amazon.lambda.powertools.idempotency.internal;
1515

16+
import com.amazonaws.services.lambda.runtime.Context;
1617
import com.fasterxml.jackson.databind.JsonNode;
1718
import org.aspectj.lang.ProceedingJoinPoint;
1819
import org.aspectj.lang.reflect.MethodSignature;
@@ -25,6 +26,8 @@
2526
import software.amazon.lambda.powertools.utilities.JsonConfig;
2627

2728
import java.time.Instant;
29+
import java.util.Optional;
30+
import java.util.OptionalInt;
2831

2932
import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.EXPIRED;
3033
import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
@@ -40,10 +43,12 @@ public class IdempotencyHandler {
4043
private final ProceedingJoinPoint pjp;
4144
private final JsonNode data;
4245
private final BasePersistenceStore persistenceStore;
46+
private final Context lambdaContext;
4347

44-
public IdempotencyHandler(ProceedingJoinPoint pjp, String functionName, JsonNode payload) {
48+
public IdempotencyHandler(ProceedingJoinPoint pjp, String functionName, JsonNode payload, Context lambdaContext) {
4549
this.pjp = pjp;
4650
this.data = payload;
51+
this.lambdaContext = lambdaContext;
4752
persistenceStore = Idempotency.getInstance().getPersistenceStore();
4853
persistenceStore.configure(Idempotency.getInstance().getConfig(), functionName);
4954
}
@@ -77,7 +82,7 @@ private Object processIdempotency() throws Throwable {
7782
try {
7883
// We call saveInProgress first as an optimization for the most common case where no idempotent record
7984
// already exists. If it succeeds, there's no need to call getRecord.
80-
persistenceStore.saveInProgress(data, Instant.now());
85+
persistenceStore.saveInProgress(data, Instant.now(), getRemainingTimeInMillis());
8186
} catch (IdempotencyItemAlreadyExistsException iaee) {
8287
DataRecord record = getIdempotencyRecord();
8388
return handleForStatus(record);
@@ -89,6 +94,21 @@ private Object processIdempotency() throws Throwable {
8994
return getFunctionResponse();
9095
}
9196

97+
/**
98+
* Tries to determine the remaining time available for the current lambda invocation.
99+
* Currently, it only works if the idempotent handler decorator is used or using {@link Idempotency#registerLambdaContext(Context)}
100+
*
101+
* @return the remaining time in milliseconds or empty if the context was not provided/found
102+
*/
103+
private OptionalInt getRemainingTimeInMillis() {
104+
if (lambdaContext != null) {
105+
return OptionalInt.of(lambdaContext.getRemainingTimeInMillis());
106+
} else {
107+
LOG.warn("Couldn't determine the remaining time left. Did you call registerLambdaContext on Idempotency?");
108+
}
109+
return OptionalInt.empty();
110+
}
111+
92112
/**
93113
* Retrieve the idempotency record from the persistence layer.
94114
*
@@ -121,6 +141,10 @@ private Object handleForStatus(DataRecord record) {
121141
}
122142

123143
if (INPROGRESS.equals(record.getStatus())) {
144+
if (record.getInProgressExpiryTimestamp().isPresent()
145+
&& record.getInProgressExpiryTimestamp().getAsLong() < Instant.now().toEpochMilli()) {
146+
throw new IdempotencyInconsistentStateException("Item should have been expired in-progress because it already time-outed.");
147+
}
124148
throw new IdempotencyAlreadyInProgressException("Execution already in progress with idempotency key: " + record.getIdempotencyKey());
125149
}
126150

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import org.aspectj.lang.annotation.Aspect;
2020
import org.aspectj.lang.annotation.Pointcut;
2121
import org.aspectj.lang.reflect.MethodSignature;
22+
import com.amazonaws.services.lambda.runtime.Context;
2223
import software.amazon.lambda.powertools.idempotency.Constants;
24+
import software.amazon.lambda.powertools.idempotency.Idempotency;
2325
import software.amazon.lambda.powertools.idempotency.IdempotencyKey;
2426
import software.amazon.lambda.powertools.idempotency.Idempotent;
2527
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyConfigurationException;
@@ -56,12 +58,20 @@ public Object around(ProceedingJoinPoint pjp,
5658
throw new IdempotencyConfigurationException("The annotated method doesn't return anything. Unable to perform idempotency on void return type");
5759
}
5860

59-
JsonNode payload = getPayload(pjp, method);
61+
boolean isHandler = (isHandlerMethod(pjp) && placedOnRequestHandler(pjp));
62+
JsonNode payload = getPayload(pjp, method, isHandler);
6063
if (payload == null) {
6164
throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
6265
}
6366

64-
IdempotencyHandler idempotencyHandler = new IdempotencyHandler(pjp, method.getName(), payload);
67+
Context lambdaContext;
68+
if (isHandler) {
69+
lambdaContext = (Context) pjp.getArgs()[1];
70+
} else {
71+
lambdaContext = Idempotency.getInstance().getConfig().getLambdaContext();
72+
}
73+
74+
IdempotencyHandler idempotencyHandler = new IdempotencyHandler(pjp, method.getName(), payload, lambdaContext);
6575
return idempotencyHandler.handle();
6676
}
6777

@@ -71,11 +81,10 @@ public Object around(ProceedingJoinPoint pjp,
7181
* @param method the annotated method
7282
* @return the payload used for idempotency
7383
*/
74-
private JsonNode getPayload(ProceedingJoinPoint pjp, Method method) {
84+
private JsonNode getPayload(ProceedingJoinPoint pjp, Method method, boolean isHandler) {
7585
JsonNode payload = null;
7686
// handleRequest or method with one parameter: get the first one
77-
if ((isHandlerMethod(pjp) && placedOnRequestHandler(pjp))
78-
|| pjp.getArgs().length == 1) {
87+
if (isHandler || pjp.getArgs().length == 1) {
7988
payload = JsonConfig.get().getObjectMapper().valueToTree(pjp.getArgs()[0]);
8089
} else {
8190
// Look for a parameter annotated with @IdempotencyKey

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
import java.security.NoSuchAlgorithmException;
3636
import java.time.Instant;
3737
import java.time.temporal.ChronoUnit;
38-
import java.util.Map;
39-
import java.util.Spliterator;
40-
import java.util.Spliterators;
38+
import java.util.*;
4139
import java.util.stream.Stream;
4240
import java.util.stream.StreamSupport;
4341

@@ -131,19 +129,25 @@ public void saveSuccess(JsonNode data, Object result, Instant now) {
131129
* @param data Payload
132130
* @param now
133131
*/
134-
public void saveInProgress(JsonNode data, Instant now) throws IdempotencyItemAlreadyExistsException {
132+
public void saveInProgress(JsonNode data, Instant now, OptionalInt remainingTimeInMs) throws IdempotencyItemAlreadyExistsException {
135133
String idempotencyKey = getHashedIdempotencyKey(data);
136134

137135
if (retrieveFromCache(idempotencyKey, now) != null) {
138136
throw new IdempotencyItemAlreadyExistsException();
139137
}
140138

139+
OptionalLong inProgressExpirationMsTimestamp = OptionalLong.empty();
140+
if (remainingTimeInMs.isPresent()) {
141+
inProgressExpirationMsTimestamp = OptionalLong.of(now.plus(remainingTimeInMs.getAsInt(), ChronoUnit.MILLIS).toEpochMilli());
142+
}
143+
141144
DataRecord record = new DataRecord(
142145
idempotencyKey,
143146
DataRecord.Status.INPROGRESS,
144147
getExpiryEpochSecond(now),
145148
null,
146-
getHashedPayload(data)
149+
getHashedPayload(data),
150+
inProgressExpirationMsTimestamp
147151
);
148152
LOG.debug("saving in progress record for idempotency key: {}", record.getIdempotencyKey());
149153
putRecord(record, now);
@@ -212,7 +216,8 @@ private String getHashedIdempotencyKey(JsonNode data) {
212216
}
213217

214218
String hash = generateHash(node);
215-
return functionName + "#" + hash;
219+
hash = functionName + "#" + hash;
220+
return hash;
216221
}
217222

218223
private boolean isMissingIdemPotencyKey(JsonNode data) {

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
*/
1414
package software.amazon.lambda.powertools.idempotency.persistence;
1515

16+
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
17+
1618
import java.time.Instant;
1719
import java.util.Objects;
20+
import java.util.OptionalInt;
21+
import java.util.OptionalLong;
1822

1923
/**
2024
* Data Class for idempotency records. This is actually the item that will be stored in the persistence layer.
@@ -25,21 +29,32 @@ public class DataRecord {
2529
private final long expiryTimestamp;
2630
private final String responseData;
2731
private final String payloadHash;
32+
private final OptionalLong inProgressExpiryTimestamp;
2833

2934
public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData, String payloadHash) {
3035
this.idempotencyKey = idempotencyKey;
3136
this.status = status.toString();
3237
this.expiryTimestamp = expiryTimestamp;
3338
this.responseData = responseData;
3439
this.payloadHash = payloadHash;
40+
this.inProgressExpiryTimestamp = OptionalLong.empty();
41+
}
42+
43+
public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData, String payloadHash, OptionalLong inProgressExpiryTimestamp) {
44+
this.idempotencyKey = idempotencyKey;
45+
this.status = status.toString();
46+
this.expiryTimestamp = expiryTimestamp;
47+
this.responseData = responseData;
48+
this.payloadHash = payloadHash;
49+
this.inProgressExpiryTimestamp = inProgressExpiryTimestamp;
3550
}
3651

3752
public String getIdempotencyKey() {
3853
return idempotencyKey;
3954
}
4055

4156
/**
42-
* Check if data record is expired (based on expiration configured in the {@link software.amazon.lambda.powertools.idempotency.IdempotencyConfig})
57+
* Check if data record is expired (based on expiration configured in the {@link IdempotencyConfig})
4358
*
4459
* @return Whether the record is currently expired or not
4560
*/
@@ -60,6 +75,10 @@ public long getExpiryTimestamp() {
6075
return expiryTimestamp;
6176
}
6277

78+
public OptionalLong getInProgressExpiryTimestamp() {
79+
return inProgressExpiryTimestamp;
80+
}
81+
6382
public String getResponseData() {
6483
return responseData;
6584
}
@@ -85,6 +104,7 @@ public int hashCode() {
85104
return Objects.hash(idempotencyKey, status, expiryTimestamp, responseData, payloadHash);
86105
}
87106

107+
88108
/**
89109
* Status of the record:
90110
* <ul>

0 commit comments

Comments
 (0)