Skip to content

Commit e425e51

Browse files
committed
Merge branch 'v2' into phipag/docs-v2
2 parents 4d4400f + 374b38d commit e425e51

File tree

24 files changed

+422
-124
lines changed

24 files changed

+422
-124
lines changed

docs/core/logging.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ Your logs will always include the following keys in your structured logging:
354354
| **xray_trace_id** | String | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when [Tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html){target="_blank"} |
355355
| **error** | Map | `{ "name": "InvalidAmountException", "message": "Amount must be superior to 0", "stack": "at..." }` | Eventual exception (e.g. when doing `logger.error("Error", new InvalidAmountException("Amount must be superior to 0"));`) |
356356

357+
???+ note
358+
If you emit a log message with a key that matches one of the [standard structured keys](#standard-structured-keys) or one of the [additional structured keys](#additional-structured-keys), the Logger will log a warning message and ignore the key.
359+
357360
## Additional structured keys
358361

359362
### Logging Lambda context information
@@ -640,6 +643,9 @@ To append additional keys in your logs, you can use the `StructuredArguments` cl
640643
}
641644
```
642645

646+
???+ warning "Do not use reserved keys in `StructuredArguments`"
647+
If the key name of your structured argument matches any of the [standard structured keys](#standard-structured-keys) or any of the [additional structured keys](#additional-structured-keys), the Logger will log a warning message and ignore the key. This is to protect you from accidentally overwriting reserved keys such as the log level or Lambda context information.
648+
643649
**Using MDC**
644650

645651
Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supported by the [SLF4J API](https://www.slf4j.org/manual.html#mdc){target="_blank"},
@@ -650,6 +656,9 @@ Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supporte
650656
???+ warning "Custom keys stored in the MDC are persisted across warm invocations"
651657
Always set additional keys as part of your handler method to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-state).
652658

659+
???+ warning "Do not add reserved keys to MDC"
660+
Avoid adding any of the keys listed in [standard structured keys](#standard-structured-keys) and [additional structured keys](#additional-structured-keys) to your MDC. This may cause unindented behavior and will overwrite the context set by the Logger. Unlike with `StructuredArguments`, the Logger will **not** ignore reserved keys set via MDC.
661+
653662

654663
### Removing additional keys
655664

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@
1414

1515
package software.amazon.lambda.powertools.idempotency.exceptions;
1616

17+
import java.util.Optional;
18+
19+
import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
20+
1721
/**
1822
* Exception thrown when trying to store an item which already exists.
1923
*/
2024
public class IdempotencyItemAlreadyExistsException extends RuntimeException {
2125
private static final long serialVersionUID = 9027152772149436500L;
26+
// transient because we don't want to accidentally dump any payloads in logs / stack traces
27+
private transient Optional<DataRecord> dr = Optional.empty();
2228

2329
public IdempotencyItemAlreadyExistsException() {
2430
super();
@@ -27,4 +33,13 @@ public IdempotencyItemAlreadyExistsException() {
2733
public IdempotencyItemAlreadyExistsException(String msg, Throwable e) {
2834
super(msg, e);
2935
}
36+
37+
public IdempotencyItemAlreadyExistsException(String msg, Throwable e, DataRecord dr) {
38+
super(msg, e);
39+
this.dr = Optional.ofNullable(dr);
40+
}
41+
42+
public Optional<DataRecord> getDataRecord() {
43+
return dr;
44+
}
3045
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ private Object processIdempotency() throws Throwable {
9494
// already exists. If it succeeds, there's no need to call getRecord.
9595
persistenceStore.saveInProgress(data, Instant.now(), getRemainingTimeInMillis());
9696
} catch (IdempotencyItemAlreadyExistsException iaee) {
97-
DataRecord record = getIdempotencyRecord();
98-
if (record != null) {
99-
return handleForStatus(record);
97+
// If a DataRecord is already present on the Exception we can immediately take that one instead of trying
98+
// to fetch it first.
99+
DataRecord dr = iaee.getDataRecord().orElseGet(this::getIdempotencyRecord);
100+
if (dr != null) {
101+
return handleForStatus(dr);
100102
}
101103
} catch (IdempotencyKeyException ike) {
102104
throw ike;

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class DataRecord {
5353
private final OptionalLong inProgressExpiryTimestamp;
5454

5555
public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData,
56-
String payloadHash) {
56+
String payloadHash) {
5757
this.idempotencyKey = idempotencyKey;
5858
this.status = status.toString();
5959
this.expiryTimestamp = expiryTimestamp;
@@ -63,7 +63,7 @@ public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, St
6363
}
6464

6565
public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData,
66-
String payloadHash, OptionalLong inProgressExpiryTimestamp) {
66+
String payloadHash, OptionalLong inProgressExpiryTimestamp) {
6767
this.idempotencyKey = idempotencyKey;
6868
this.status = status.toString();
6969
this.expiryTimestamp = expiryTimestamp;
@@ -131,13 +131,22 @@ public int hashCode() {
131131
return Objects.hash(idempotencyKey, status, expiryTimestamp, responseData, payloadHash);
132132
}
133133

134+
@Override
135+
public String toString() {
136+
return "DataRecord{" +
137+
"idempotencyKey='" + idempotencyKey + '\'' +
138+
", status='" + status + '\'' +
139+
", expiryTimestamp=" + expiryTimestamp +
140+
", payloadHash='" + payloadHash + '\'' +
141+
'}';
142+
}
134143

135144
/**
136145
* Status of the record:
137146
* <ul>
138-
* <li>INPROGRESS: record initialized when function starts</li>
139-
* <li>COMPLETED: record updated with the result of the function when it ends</li>
140-
* <li>EXPIRED: record expired, idempotency will not happen</li>
147+
* <li>INPROGRESS: record initialized when function starts</li>
148+
* <li>COMPLETED: record updated with the result of the function when it ends</li>
149+
* <li>EXPIRED: record expired, idempotency will not happen</li>
141150
* </ul>
142151
*/
143152
public enum Status {

powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.mockito.ArgumentMatchers.any;
2121
import static org.mockito.Mockito.doReturn;
2222
import static org.mockito.Mockito.doThrow;
23+
import static org.mockito.Mockito.never;
2324
import static org.mockito.Mockito.spy;
2425
import static org.mockito.Mockito.verify;
2526
import static org.mockito.Mockito.verifyNoInteractions;
@@ -154,7 +155,7 @@ public void secondCall_notExpired_shouldGetFromStore() throws JsonProcessingExce
154155
.build())
155156
.configure();
156157

157-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
158+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
158159

159160
Product p = new Product(42, "fake product", 12);
160161
Basket b = new Basket(p);
@@ -175,6 +176,44 @@ public void secondCall_notExpired_shouldGetFromStore() throws JsonProcessingExce
175176
assertThat(function.handlerCalled()).isFalse();
176177
}
177178

179+
@Test
180+
public void secondCall_notExpired_shouldNotGetFromStoreIfPresentOnIdempotencyException()
181+
throws JsonProcessingException {
182+
// GIVEN
183+
Idempotency.config()
184+
.withPersistenceStore(store)
185+
.withConfig(IdempotencyConfig.builder()
186+
.withEventKeyJMESPath("id")
187+
.build())
188+
.configure();
189+
190+
Product p = new Product(42, "fake product", 12);
191+
Basket b = new Basket(p);
192+
DataRecord dr = new DataRecord(
193+
"42",
194+
DataRecord.Status.COMPLETED,
195+
Instant.now().plus(356, SECONDS).getEpochSecond(),
196+
JsonConfig.get().getObjectMapper().writer().writeValueAsString(b),
197+
null);
198+
199+
// A data record on this exception should take precedence over fetching a record from the store / cache
200+
doThrow(new IdempotencyItemAlreadyExistsException(
201+
"Test message",
202+
new RuntimeException("Test Cause"),
203+
dr))
204+
.when(store).saveInProgress(any(), any(), any());
205+
206+
// WHEN
207+
IdempotencyEnabledFunction function = new IdempotencyEnabledFunction();
208+
Basket basket = function.handleRequest(p, context);
209+
210+
// THEN
211+
assertThat(basket).isEqualTo(b);
212+
assertThat(function.handlerCalled()).isFalse();
213+
// Should never call the store because item is already present on IdempotencyItemAlreadyExistsException
214+
verify(store, never()).getRecord(any(), any());
215+
}
216+
178217
@Test
179218
public void secondCall_notExpired_shouldGetStringFromStore() {
180219
// GIVEN
@@ -185,7 +224,7 @@ public void secondCall_notExpired_shouldGetStringFromStore() {
185224
.build())
186225
.configure();
187226

188-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
227+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
189228

190229
Product p = new Product(42, "fake product", 12);
191230
DataRecord dr = new DataRecord(
@@ -220,7 +259,7 @@ public void secondCall_notExpired_shouldGetStringFromStoreWithResponseHook() {
220259
.build())
221260
.configure();
222261

223-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
262+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
224263

225264
Product p = new Product(42, "fake product", 12);
226265
DataRecord dr = new DataRecord(
@@ -251,7 +290,7 @@ public void secondCall_inProgress_shouldThrowIdempotencyAlreadyInProgressExcepti
251290
.build())
252291
.configure();
253292

254-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
293+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
255294

256295
Product p = new Product(42, "fake product", 12);
257296
Basket b = new Basket(p);
@@ -283,7 +322,7 @@ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowIncons
283322
.build())
284323
.configure();
285324

286-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
325+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
287326

288327
Product p = new Product(42, "fake product", 12);
289328
Basket b = new Basket(p);
@@ -412,7 +451,7 @@ public void idempotencyOnSubMethodAnnotated_secondCall_notExpired_shouldGetFromS
412451
.withPersistenceStore(store)
413452
.configure();
414453

415-
doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any());
454+
doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any());
416455

417456
Product p = new Product(42, "fake product", 12);
418457
Basket b = new Basket(p);

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,17 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre
189189
"attribute_not_exists(#id) OR #expiry < :now OR (attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_milliseconds AND #status = :inprogress)")
190190
.expressionAttributeNames(expressionAttributeNames)
191191
.expressionAttributeValues(expressionAttributeValues)
192-
.build()
193-
);
192+
.returnValuesOnConditionCheckFailure("ALL_OLD")
193+
.build());
194194
} catch (ConditionalCheckFailedException e) {
195195
LOG.debug("Failed to put record for already existing idempotency key: {}", record.getIdempotencyKey());
196+
if (e.hasItem()) {
197+
DataRecord existingRecord = itemToRecord(e.item());
198+
throw new IdempotencyItemAlreadyExistsException(
199+
"Failed to put record for already existing idempotency key: " + record.getIdempotencyKey()
200+
+ ". Existing record: " + existingRecord,
201+
e, existingRecord);
202+
}
196203
throw new IdempotencyItemAlreadyExistsException(
197204
"Failed to put record for already existing idempotency key: " + record.getIdempotencyKey(), e);
198205
}
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
*
1313
*/
1414

15-
package software.amazon.lambda.powertools.idempotency.dynamodb;
15+
package software.amazon.lambda.powertools.idempotency.persistence.dynamodb;
1616

17-
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
18-
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
1917
import java.io.IOException;
2018
import java.net.ServerSocket;
2119
import java.net.URI;
20+
2221
import org.junit.jupiter.api.AfterAll;
2322
import org.junit.jupiter.api.BeforeAll;
23+
24+
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
25+
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
26+
2427
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
2528
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
2629
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
@@ -66,13 +69,12 @@ public static void setupDynamo() {
6669
.tableName(TABLE_NAME)
6770
.keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build())
6871
.attributeDefinitions(
69-
AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build()
70-
)
72+
AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build())
7173
.billingMode(BillingMode.PAY_PER_REQUEST)
7274
.build());
7375

74-
DescribeTableResponse response =
75-
client.describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build());
76+
DescribeTableResponse response = client
77+
.describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build());
7678
if (response == null) {
7779
throw new RuntimeException("Table was not created within expected time");
7880
}

powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
3333
import software.amazon.lambda.powertools.idempotency.Constants;
3434
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
35-
import software.amazon.lambda.powertools.idempotency.dynamodb.DynamoDBConfig;
35+
3636
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
3737
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
3838
import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
@@ -155,13 +155,14 @@ public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordA
155155
DataRecord.Status.INPROGRESS,
156156
expiry2,
157157
null,
158-
null
159-
), now)
160-
).isInstanceOf(IdempotencyItemAlreadyExistsException.class);
158+
null),
159+
now)).isInstanceOf(IdempotencyItemAlreadyExistsException.class)
160+
// DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD")
161+
.matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent());
161162

162163
// THEN: item was not updated, retrieve the initial one
163-
Map<String, AttributeValue> itemInDb =
164-
client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
164+
Map<String, AttributeValue> itemInDb = client
165+
.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
165166
assertThat(itemInDb).isNotNull();
166167
assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED");
167168
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
@@ -190,13 +191,16 @@ public void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpire
190191
DataRecord.Status.INPROGRESS,
191192
expiry2,
192193
"Fake Data 2",
193-
null
194-
), now))
195-
.isInstanceOf(IdempotencyItemAlreadyExistsException.class);
194+
null),
195+
now))
196+
.isInstanceOf(IdempotencyItemAlreadyExistsException.class)
197+
// DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD")
198+
.matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent());
199+
;
196200

197201
// THEN: item was not updated, retrieve the initial one
198-
Map<String, AttributeValue> itemInDb =
199-
client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
202+
Map<String, AttributeValue> itemInDb = client
203+
.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
200204
assertThat(itemInDb).isNotNull();
201205
assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
202206
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@
1212
*
1313
*/
1414

15-
package software.amazon.lambda.powertools.idempotency.dynamodb;
16-
15+
package software.amazon.lambda.powertools.idempotency.persistence.dynamodb;
1716

1817
import static org.assertj.core.api.Assertions.assertThat;
1918

20-
import com.amazonaws.services.lambda.runtime.Context;
21-
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
22-
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
2319
import org.junit.jupiter.api.BeforeEach;
2420
import org.junit.jupiter.api.Test;
2521
import org.mockito.Mock;
2622
import org.mockito.MockitoAnnotations;
23+
24+
import com.amazonaws.services.lambda.runtime.Context;
25+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
26+
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
27+
2728
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
28-
import software.amazon.lambda.powertools.idempotency.dynamodb.handlers.IdempotencyFunction;
29+
import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.handlers.IdempotencyFunction;
2930

3031
public class IdempotencyTest extends DynamoDBConfig {
3132

@@ -41,14 +42,14 @@ void setUp() {
4142
public void endToEndTest() {
4243
IdempotencyFunction function = new IdempotencyFunction(client);
4344

44-
APIGatewayProxyResponseEvent response =
45-
function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
45+
APIGatewayProxyResponseEvent response = function
46+
.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
4647
assertThat(function.handlerExecuted).isTrue();
4748

4849
function.handlerExecuted = false;
4950

50-
APIGatewayProxyResponseEvent response2 =
51-
function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
51+
APIGatewayProxyResponseEvent response2 = function
52+
.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
5253
assertThat(function.handlerExecuted).isFalse();
5354

5455
assertThat(response).isEqualTo(response2);

0 commit comments

Comments
 (0)