From 1ad6c10ffdd5f332e5b9cc0ad65bfd688103f65d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 20 Mar 2025 15:54:00 +0100 Subject: [PATCH 1/5] Add responseHook definition to IdempotencyConfig and call from IdempotencyHandler. Update idempotency example with an example use-case modifying API GW headers. --- .../src/main/java/helloworld/App.java | 42 ++++++--- .../idempotency/IdempotencyConfig.java | 85 +++++++++++++++---- .../internal/IdempotencyHandler.java | 48 ++++++++--- 3 files changed, 132 insertions(+), 43 deletions(-) diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java index 0c4693230..80be058f4 100644 --- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java @@ -45,20 +45,36 @@ public App() { public App(DynamoDbClient client) { Idempotency.config().withConfig( - IdempotencyConfig.builder() - .withEventKeyJMESPath( - "powertools_json(body).address") // will retrieve the address field in the body which is a string transformed to json with `powertools_json` - .build()) + IdempotencyConfig.builder() + .withEventKeyJMESPath("powertools_json(body).address") + .withResponseHook((responseData, dataRecord) -> { + if (responseData instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent proxyResponse = (APIGatewayProxyResponseEvent) responseData; + final Map headers = new HashMap<>(); + headers.putAll(proxyResponse.getHeaders()); + headers.put("x-idempotency-response", "true"); + headers.put("x-idempotency-expiration", + String.valueOf(dataRecord.getExpiryTimestamp())); + + proxyResponse.setHeaders(headers); + + return proxyResponse; + } + + return responseData; + }) + .build()) .withPersistenceStore( DynamoDBPersistenceStore.builder() .withDynamoDbClient(client) .withTableName(System.getenv("IDEMPOTENCY_TABLE")) - .build() - ).configure(); + .build()) + .configure(); } /** - * This is our Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the given URL. Requests are made idempotent + * This is our Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the + * given URL. Requests are made idempotent * by the idempotency library, and results are cached for the default 1h expiry time. *

* You can test the endpoint like this: @@ -67,8 +83,10 @@ public App(DynamoDbClient client) { * curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/helloidem/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}' * *

*/ @Idempotent // The magic is here! @@ -101,14 +119,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } } - /** * Helper to retrieve the contents of the given URL and return them as a string. *

* We could also put the @Idempotent annotation here if we only wanted this sub-operation to be idempotent. Putting * it on the handler, however, reduces total execution time and saves us time! * - * @param address The URL to fetch + * @param address + * The URL to fetch * @return The contents of the given URL * @throws IOException */ @@ -118,4 +136,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} \ No newline at end of file +} diff --git a/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java index 2b22cac51..9d5c66cac 100644 --- a/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java @@ -14,10 +14,13 @@ package software.amazon.lambda.powertools.idempotency; +import java.time.Duration; +import java.util.function.BiFunction; + import com.amazonaws.services.lambda.runtime.Context; -import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache; -import java.time.Duration; +import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; /** * Configuration of the idempotency feature. Use the {@link Builder} to create an instance. @@ -30,11 +33,12 @@ public class IdempotencyConfig { private final String payloadValidationJMESPath; private final boolean throwOnNoIdempotencyKey; private final String hashFunction; + private final BiFunction responseHook; private Context lambdaContext; private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESPath, - boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, - long expirationInSeconds, String hashFunction) { + boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, + long expirationInSeconds, String hashFunction, BiFunction responseHook) { this.localCacheMaxItems = localCacheMaxItems; this.useLocalCache = useLocalCache; this.expirationInSeconds = expirationInSeconds; @@ -42,6 +46,7 @@ private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESP this.payloadValidationJMESPath = payloadValidationJMESPath; this.throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; this.hashFunction = hashFunction; + this.responseHook = responseHook; } /** @@ -89,6 +94,10 @@ public void setLambdaContext(Context lambdaContext) { this.lambdaContext = lambdaContext; } + public BiFunction getResponseHook() { + return responseHook; + } + public static class Builder { private int localCacheMaxItems = 256; @@ -98,14 +107,18 @@ public static class Builder { private String payloadValidationJMESPath; private boolean throwOnNoIdempotencyKey = false; private String hashFunction = "MD5"; + private BiFunction responseHook; /** * Initialize and return an instance of {@link IdempotencyConfig}.
* Example:
+ * *

          * IdempotencyConfig.builder().withUseLocalCache().build();
          * 
+ * * This instance must then be passed to the {@link Idempotency.Config}: + * *
          * Idempotency.config().withConfig(config).configure();
          * 
@@ -120,13 +133,15 @@ public IdempotencyConfig build() { useLocalCache, localCacheMaxItems, expirationInSeconds, - hashFunction); + hashFunction, + responseHook); } /** * A JMESPath expression to extract the idempotency key from the event record.
* See https://jmespath.org/ for more details.
- * Common paths are: