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

Commit d5ffff2

Browse files
Merge pull request #14 from jeromevdl/idempotency
sample for idempotency
2 parents a02e5da + bec2500 commit d5ffff2

File tree

7 files changed

+508
-0
lines changed

7 files changed

+508
-0
lines changed

java/Idempotency/Function/pom.xml

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<groupId>powertools</groupId>
5+
<artifactId>Function</artifactId>
6+
<version>1.0</version>
7+
<packaging>jar</packaging>
8+
<name>A sample Hello World using powertools idempotency</name>
9+
<properties>
10+
<maven.compiler.source>11</maven.compiler.source>
11+
<maven.compiler.target>11</maven.compiler.target>
12+
<log4j.version>2.17.1</log4j.version>
13+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14+
</properties>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>software.amazon.lambda</groupId>
19+
<artifactId>powertools-tracing</artifactId>
20+
<version>1.11.0</version>
21+
</dependency>
22+
<dependency>
23+
<groupId>software.amazon.lambda</groupId>
24+
<artifactId>powertools-logging</artifactId>
25+
<version>1.11.0</version>
26+
</dependency>
27+
<dependency>
28+
<groupId>software.amazon.lambda</groupId>
29+
<artifactId>powertools-idempotency</artifactId>
30+
<version>1.11.0</version>
31+
</dependency>
32+
<dependency>
33+
<groupId>com.amazonaws</groupId>
34+
<artifactId>aws-lambda-java-core</artifactId>
35+
<version>1.2.1</version>
36+
</dependency>
37+
<dependency>
38+
<groupId>com.amazonaws</groupId>
39+
<artifactId>aws-lambda-java-events</artifactId>
40+
<version>3.11.0</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.apache.logging.log4j</groupId>
44+
<artifactId>log4j-core</artifactId>
45+
<version>${log4j.version}</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>org.apache.logging.log4j</groupId>
49+
<artifactId>log4j-api</artifactId>
50+
<version>${log4j.version}</version>
51+
</dependency>
52+
53+
<dependency>
54+
<groupId>org.mockito</groupId>
55+
<artifactId>mockito-core</artifactId>
56+
<version>4.3.1</version>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.junit.jupiter</groupId>
61+
<artifactId>junit-jupiter-api</artifactId>
62+
<version>5.8.2</version>
63+
<scope>test</scope>
64+
</dependency>
65+
<dependency>
66+
<groupId>com.amazonaws</groupId>
67+
<artifactId>DynamoDBLocal</artifactId>
68+
<version>[1.12,2.0)</version>
69+
<scope>test</scope>
70+
</dependency>
71+
<dependency>
72+
<groupId>com.amazonaws</groupId>
73+
<artifactId>aws-lambda-java-tests</artifactId>
74+
<version>1.1.1</version>
75+
</dependency>
76+
</dependencies>
77+
78+
<build>
79+
<plugins>
80+
<plugin>
81+
<groupId>org.codehaus.mojo</groupId>
82+
<artifactId>aspectj-maven-plugin</artifactId>
83+
<version>1.14.0</version>
84+
<configuration>
85+
<source>${maven.compiler.source}</source>
86+
<target>${maven.compiler.target}</target>
87+
<complianceLevel>${maven.compiler.target}</complianceLevel>
88+
<aspectLibraries>
89+
<aspectLibrary>
90+
<groupId>software.amazon.lambda</groupId>
91+
<artifactId>powertools-tracing</artifactId>
92+
</aspectLibrary>
93+
<aspectLibrary>
94+
<groupId>software.amazon.lambda</groupId>
95+
<artifactId>powertools-logging</artifactId>
96+
</aspectLibrary>
97+
<aspectLibrary>
98+
<groupId>software.amazon.lambda</groupId>
99+
<artifactId>powertools-idempotency</artifactId>
100+
</aspectLibrary>
101+
</aspectLibraries>
102+
</configuration>
103+
<executions>
104+
<execution>
105+
<goals>
106+
<goal>compile</goal>
107+
</goals>
108+
</execution>
109+
</executions>
110+
</plugin>
111+
<plugin>
112+
<groupId>org.apache.maven.plugins</groupId>
113+
<artifactId>maven-dependency-plugin</artifactId>
114+
<executions>
115+
<execution>
116+
<id>copy</id>
117+
<phase>test-compile</phase>
118+
<goals>
119+
<goal>copy-dependencies</goal>
120+
</goals>
121+
<configuration>
122+
<includeScope>test</includeScope>
123+
<includeTypes>so,dll,dylib</includeTypes>
124+
<outputDirectory>${project.build.directory}/native-libs</outputDirectory>
125+
</configuration>
126+
</execution>
127+
</executions>
128+
</plugin>
129+
<plugin>
130+
<groupId>org.apache.maven.plugins</groupId>
131+
<artifactId>maven-surefire-plugin</artifactId>
132+
<version>3.0.0-M5</version>
133+
<configuration>
134+
<systemPropertyVariables>
135+
<sqlite4java.library.path>${project.build.directory}/native-libs</sqlite4java.library.path>
136+
</systemPropertyVariables>
137+
<environmentVariables>
138+
<IDEMPOTENCY_TABLE>idempotency</IDEMPOTENCY_TABLE>
139+
<AWS_REGION>eu-central-1</AWS_REGION>
140+
<AWS_XRAY_CONTEXT_MISSING>LOG_ERROR</AWS_XRAY_CONTEXT_MISSING>
141+
</environmentVariables>
142+
</configuration>
143+
</plugin>
144+
<plugin>
145+
<groupId>org.apache.maven.plugins</groupId>
146+
<artifactId>maven-shade-plugin</artifactId>
147+
<version>3.2.4</version>
148+
<executions>
149+
<execution>
150+
<phase>package</phase>
151+
<goals>
152+
<goal>shade</goal>
153+
</goals>
154+
<configuration>
155+
<transformers>
156+
<transformer
157+
implementation="com.github.edwgiz.maven_shade_plugin.log4j2_cache_transformer.PluginsCacheFileTransformer">
158+
</transformer>
159+
</transformers>
160+
</configuration>
161+
</execution>
162+
</executions>
163+
<dependencies>
164+
<dependency>
165+
<groupId>com.github.edwgiz</groupId>
166+
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
167+
<version>2.15</version>
168+
</dependency>
169+
</dependencies>
170+
</plugin>
171+
</plugins>
172+
</build>
173+
<repositories>
174+
<repository>
175+
<id>dynamodb-local-oregon</id>
176+
<name>DynamoDB Local Release Repository</name>
177+
<url>https://s3.eu-central-1.amazonaws.com/dynamodb-local-frankfurt/release</url>
178+
</repository>
179+
</repositories>
180+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package helloworld;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.RequestHandler;
5+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
6+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
7+
import org.apache.logging.log4j.LogManager;
8+
import org.apache.logging.log4j.Logger;
9+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
10+
import software.amazon.lambda.powertools.idempotency.Idempotency;
11+
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
12+
import software.amazon.lambda.powertools.idempotency.Idempotent;
13+
import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore;
14+
import software.amazon.lambda.powertools.logging.Logging;
15+
import software.amazon.lambda.powertools.utilities.JsonConfig;
16+
17+
import java.io.BufferedReader;
18+
import java.io.IOException;
19+
import java.io.InputStreamReader;
20+
import java.net.URL;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
24+
25+
public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
26+
private final static Logger log = LogManager.getLogger();
27+
28+
public App() {
29+
this(null);
30+
}
31+
32+
public App(DynamoDbClient client) {
33+
Idempotency.config().withConfig(
34+
IdempotencyConfig.builder()
35+
.withEventKeyJMESPath("powertools_json(body).address") // will retrieve the address field in the body which is a string transformed to json with `powertools_json`
36+
.build())
37+
.withPersistenceStore(
38+
DynamoDBPersistenceStore.builder()
39+
.withDynamoDbClient(client)
40+
.withTableName(System.getenv("IDEMPOTENCY_TABLE"))
41+
.build()
42+
).configure();
43+
}
44+
45+
/**
46+
* Try with:
47+
* <pre>
48+
* 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"}'
49+
* </pre>
50+
* <ul>
51+
* <li>First call will execute the handleRequest normally, and store the response in the idempotency table (Look into DynamoDB)</li>
52+
* <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>
53+
* </ul>
54+
*/
55+
@Idempotent // *** THE MAGIC IS HERE ***
56+
@Logging(logEvent = true)
57+
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
58+
Map<String, String> headers = new HashMap<>();
59+
60+
headers.put("Content-Type", "application/json");
61+
headers.put("Access-Control-Allow-Origin", "*");
62+
headers.put("Access-Control-Allow-Methods", "GET, OPTIONS");
63+
headers.put("Access-Control-Allow-Headers", "*");
64+
65+
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
66+
.withHeaders(headers);
67+
try {
68+
String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText();
69+
final String pageContents = this.getPageContents(address);
70+
String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);
71+
72+
log.info("ip is {}", pageContents);
73+
return response
74+
.withStatusCode(200)
75+
.withBody(output);
76+
77+
} catch (IOException e) {
78+
return response
79+
.withBody("{}")
80+
.withStatusCode(500);
81+
}
82+
}
83+
84+
// we could also put the @Idempotent annotation here, but using it on the handler avoids executing the handler (cost reduction).
85+
// Use it on other methods to handle multiple items (with SQS batch processing for example)
86+
private String getPageContents(String address) throws IOException {
87+
URL url = new URL(address);
88+
try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) {
89+
return br.lines().collect(Collectors.joining(System.lineSeparator()));
90+
}
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package helloworld;
2+
3+
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
4+
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
5+
import com.amazonaws.services.lambda.runtime.Context;
6+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
7+
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
8+
import org.junit.jupiter.api.Assertions;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.mockito.Mock;
13+
import org.mockito.MockitoAnnotations;
14+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
15+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
16+
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
17+
import software.amazon.awssdk.regions.Region;
18+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
19+
import software.amazon.awssdk.services.dynamodb.model.*;
20+
21+
import java.io.IOException;
22+
import java.net.ServerSocket;
23+
import java.net.URI;
24+
25+
public class AppTest {
26+
@Mock
27+
private Context context;
28+
private App app;
29+
private static DynamoDbClient client;
30+
31+
@BeforeAll
32+
public static void setupDynamoLocal() {
33+
int port = getFreePort();
34+
try {
35+
DynamoDBProxyServer dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[]{
36+
"-inMemory",
37+
"-port",
38+
Integer.toString(port)
39+
});
40+
dynamoProxy.start();
41+
} catch (Exception e) {
42+
throw new RuntimeException();
43+
}
44+
45+
client = DynamoDbClient.builder()
46+
.httpClient(UrlConnectionHttpClient.builder().build())
47+
.region(Region.EU_WEST_1)
48+
.endpointOverride(URI.create("http://localhost:" + port))
49+
.credentialsProvider(StaticCredentialsProvider.create(
50+
AwsBasicCredentials.create("FAKE", "FAKE")))
51+
.build();
52+
53+
client.createTable(CreateTableRequest.builder()
54+
.tableName("idempotency")
55+
.keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build())
56+
.attributeDefinitions(
57+
AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build()
58+
)
59+
.billingMode(BillingMode.PAY_PER_REQUEST)
60+
.build());
61+
}
62+
63+
private static int getFreePort() {
64+
try {
65+
ServerSocket socket = new ServerSocket(0);
66+
int port = socket.getLocalPort();
67+
socket.close();
68+
return port;
69+
} catch (IOException ioe) {
70+
throw new RuntimeException(ioe);
71+
}
72+
}
73+
74+
@BeforeEach
75+
void setUp() {
76+
MockitoAnnotations.openMocks(this);
77+
app = new App(client);
78+
}
79+
80+
@Test
81+
public void testApp() {
82+
APIGatewayProxyResponseEvent response = app.handleRequest(EventLoader.loadApiGatewayRestEvent("event.json"), context);
83+
Assertions.assertNotNull(response);
84+
Assertions.assertTrue(response.getBody().contains("hello world"));
85+
}
86+
}

0 commit comments

Comments
 (0)