diff --git a/java/Idempotency/Function/pom.xml b/java/Idempotency/Function/pom.xml new file mode 100644 index 0000000..499f30e --- /dev/null +++ b/java/Idempotency/Function/pom.xml @@ -0,0 +1,180 @@ + + 4.0.0 + powertools + Function + 1.0 + jar + A sample Hello World using powertools idempotency + + 11 + 11 + 2.17.1 + UTF-8 + + + + + software.amazon.lambda + powertools-tracing + 1.11.0 + + + software.amazon.lambda + powertools-logging + 1.11.0 + + + software.amazon.lambda + powertools-idempotency + 1.11.0 + + + com.amazonaws + aws-lambda-java-core + 1.2.1 + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + org.mockito + mockito-core + 4.3.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + com.amazonaws + DynamoDBLocal + [1.12,2.0) + test + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + + + + + + + org.codehaus.mojo + aspectj-maven-plugin + 1.14.0 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-idempotency + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + test-compile + + copy-dependencies + + + test + so,dll,dylib + ${project.build.directory}/native-libs + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + ${project.build.directory}/native-libs + + + idempotency + eu-central-1 + LOG_ERROR + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.15 + + + + + + + + dynamodb-local-oregon + DynamoDB Local Release Repository + https://s3.eu-central-1.amazonaws.com/dynamodb-local-frankfurt/release + + + diff --git a/java/Idempotency/Function/src/main/java/helloworld/App.java b/java/Idempotency/Function/src/main/java/helloworld/App.java new file mode 100644 index 0000000..fcbdcd2 --- /dev/null +++ b/java/Idempotency/Function/src/main/java/helloworld/App.java @@ -0,0 +1,92 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.lambda.powertools.idempotency.Idempotency; +import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; +import software.amazon.lambda.powertools.idempotency.Idempotent; +import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class App implements RequestHandler { + private final static Logger log = LogManager.getLogger(); + + public App() { + this(null); + } + + 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()) + .withPersistenceStore( + DynamoDBPersistenceStore.builder() + .withDynamoDbClient(client) + .withTableName(System.getenv("IDEMPOTENCY_TABLE")) + .build() + ).configure(); + } + + /** + * Try with: + *
+     *     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 *** + @Logging(logEvent = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + + headers.put("Content-Type", "application/json"); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Access-Control-Allow-Methods", "GET, OPTIONS"); + headers.put("Access-Control-Allow-Headers", "*"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText(); + final String pageContents = this.getPageContents(address); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + log.info("ip is {}", pageContents); + return response + .withStatusCode(200) + .withBody(output); + + } catch (IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + // we could also put the @Idempotent annotation here, but using it on the handler avoids executing the handler (cost reduction). + // Use it on other methods to handle multiple items (with SQS batch processing for example) + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} \ No newline at end of file diff --git a/java/Idempotency/Function/src/test/java/helloworld/AppTest.java b/java/Idempotency/Function/src/test/java/helloworld/AppTest.java new file mode 100644 index 0000000..7a5304e --- /dev/null +++ b/java/Idempotency/Function/src/test/java/helloworld/AppTest.java @@ -0,0 +1,86 @@ +package helloworld; + +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.tests.EventLoader; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; + +public class AppTest { + @Mock + private Context context; + private App app; + private static DynamoDbClient client; + + @BeforeAll + public static void setupDynamoLocal() { + int port = getFreePort(); + try { + DynamoDBProxyServer dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[]{ + "-inMemory", + "-port", + Integer.toString(port) + }); + dynamoProxy.start(); + } catch (Exception e) { + throw new RuntimeException(); + } + + client = DynamoDbClient.builder() + .httpClient(UrlConnectionHttpClient.builder().build()) + .region(Region.EU_WEST_1) + .endpointOverride(URI.create("http://localhost:" + port)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("FAKE", "FAKE"))) + .build(); + + client.createTable(CreateTableRequest.builder() + .tableName("idempotency") + .keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build() + ) + .billingMode(BillingMode.PAY_PER_REQUEST) + .build()); + } + + private static int getFreePort() { + try { + ServerSocket socket = new ServerSocket(0); + int port = socket.getLocalPort(); + socket.close(); + return port; + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + app = new App(client); + } + + @Test + public void testApp() { + APIGatewayProxyResponseEvent response = app.handleRequest(EventLoader.loadApiGatewayRestEvent("event.json"), context); + Assertions.assertNotNull(response); + Assertions.assertTrue(response.getBody().contains("hello world")); + } +} diff --git a/java/Idempotency/Function/src/test/resources/event.json b/java/Idempotency/Function/src/test/resources/event.json new file mode 100644 index 0000000..fd7f5ac --- /dev/null +++ b/java/Idempotency/Function/src/test/resources/event.json @@ -0,0 +1,63 @@ +{ + "body": "{\"address\": \"https://checkip.amazonaws.com\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + \ No newline at end of file diff --git a/java/Idempotency/README.md b/java/Idempotency/README.md new file mode 100644 index 0000000..a919f0c --- /dev/null +++ b/java/Idempotency/README.md @@ -0,0 +1,21 @@ +# Idempotency + +This project contains an example of Lambda function using the idempotency module of Lambda Powertools for Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/idempotency/). + +## Deploy the sample application + +This sample is based on Serverless Application Model (SAM) and you can use the SAM Command Line Interface (SAM CLI) to build it and deploy it to AWS. + +To use the SAM CLI, you need the following tools. + +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) +* Maven - [Install Maven](https://maven.apache.org/install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +To build and deploy your application for the first time, run the following in your shell: + +```bash +Coreutilities$ sam build +Coreutilities$ sam deploy --guided +``` diff --git a/java/Idempotency/template.yaml b/java/Idempotency/template.yaml new file mode 100644 index 0000000..d0708c6 --- /dev/null +++ b/java/Idempotency/template.yaml @@ -0,0 +1,59 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + Idempotency demo + +Globals: + Function: + Timeout: 20 + Runtime: java11 + MemorySize: 512 + Tracing: Active + Environment: + Variables: + POWERTOOLS_LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + +Resources: + IdempotencyTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiration + Enabled: true + BillingMode: PAY_PER_REQUEST + + IdempotencyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: helloworld.App::handleRequest + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref IdempotencyTable + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: idempotency + IDEMPOTENCY_TABLE: !Ref IdempotencyTable + Events: + HelloWorld: + Type: Api + Properties: + Path: /helloidem + Method: post + +Outputs: + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Idempotent function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/helloidem/" + HelloWorldFunction: + Description: "Idempotent Lambda Function ARN" + Value: !GetAtt IdempotencyFunction.Arn + diff --git a/manifest.json b/manifest.json index bb35f06..8e0eaf3 100644 --- a/manifest.json +++ b/manifest.json @@ -13,6 +13,13 @@ "dependencyManager": "maven", "appTemplate": "CoreUtilities", "javaVersion": "11" + }, + { + "directory": "java/Idempotency/Function", + "displayName": "Demos setup of an idempotent Lambda function using Powertools", + "dependencyManager": "maven", + "appTemplate": "Idempotency", + "javaVersion": "11" } ] } \ No newline at end of file