Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

sample for idempotency #14

Merged
merged 3 commits into from
Feb 17, 2022
Merged
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
180 changes: 180 additions & 0 deletions java/Idempotency/Function/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>powertools</groupId>
<artifactId>Function</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>A sample Hello World using powertools idempotency</name>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<log4j.version>2.17.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-tracing</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-logging</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>[1.12,2.0)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-tests</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<complianceLevel>${maven.compiler.target}</complianceLevel>
<aspectLibraries>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-tracing</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-logging</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>test-compile</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>test</includeScope>
<includeTypes>so,dll,dylib</includeTypes>
<outputDirectory>${project.build.directory}/native-libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<systemPropertyVariables>
<sqlite4java.library.path>${project.build.directory}/native-libs</sqlite4java.library.path>
</systemPropertyVariables>
<environmentVariables>
<IDEMPOTENCY_TABLE>idempotency</IDEMPOTENCY_TABLE>
<AWS_REGION>eu-central-1</AWS_REGION>
<AWS_XRAY_CONTEXT_MISSING>LOG_ERROR</AWS_XRAY_CONTEXT_MISSING>
</environmentVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="com.github.edwgiz.maven_shade_plugin.log4j2_cache_transformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>2.15</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>dynamodb-local-oregon</id>
<name>DynamoDB Local Release Repository</name>
<url>https://s3.eu-central-1.amazonaws.com/dynamodb-local-frankfurt/release</url>
</repository>
</repositories>
</project>
92 changes: 92 additions & 0 deletions java/Idempotency/Function/src/main/java/helloworld/App.java
Original file line number Diff line number Diff line change
@@ -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<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
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:
* <pre>
* 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"}'
* </pre>
* <ul>
* <li>First call will execute the handleRequest normally, and store the response in the idempotency table (Look into DynamoDB)</li>
* <li>Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from the store, the handler won't be called. Until the expiration happens (by default 1 hour).</li>
* </ul>
*/
@Idempotent // *** THE MAGIC IS HERE ***
@Logging(logEvent = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
Map<String, String> 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()));
}
}
}
86 changes: 86 additions & 0 deletions java/Idempotency/Function/src/test/java/helloworld/AppTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading