Skip to content

Commit aded8c8

Browse files
feat(serialization): add CRaC/SnapStart priming
Add automatic class preloading and invoke priming for powertools-serialization so SnapStart snapshots a warm Jackson deserialization path. Closes #2003
1 parent 28802fd commit aded8c8

6 files changed

Lines changed: 5673 additions & 1 deletion

File tree

docs/utilities/serialization.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,49 @@ to powertools.You can then use it to do your validation or in idempotency module
472472
}
473473
}
474474
```
475+
476+
## Advanced
477+
478+
### Lambda SnapStart priming
479+
480+
The Serialization utility integrates with AWS Lambda SnapStart to improve restore durations. To make sure the SnapStart priming logic of this utility runs correctly, you need an explicit reference to `EventDeserializer` in your code to allow the library to register before SnapStart takes a memory snapshot. Learn more about what priming is in this [blog post](https://aws.amazon.com/blogs/compute/optimizing-cold-start-performance-of-aws-lambda-using-advanced-priming-strategies-with-snapstart/){target="_blank"}.
481+
482+
If you don't use `EventDeserializer` during initialization yet, reference it in your Lambda handler. This can be done by adding one of the following lines to your handler class:
483+
484+
=== "Constructor"
485+
486+
```java hl_lines="6"
487+
import software.amazon.lambda.powertools.utilities.EventDeserializer;
488+
489+
public class MyFunctionHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
490+
491+
public MyFunctionHandler() {
492+
EventDeserializer.init(); // Ensure EventDeserializer is loaded for SnapStart
493+
}
494+
495+
@Override
496+
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
497+
// ...
498+
return something;
499+
}
500+
}
501+
```
502+
503+
=== "Static Initializer"
504+
505+
```java hl_lines="6"
506+
import software.amazon.lambda.powertools.utilities.EventDeserializer;
507+
508+
public class MyFunctionHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
509+
510+
static {
511+
EventDeserializer.init(); // Ensure EventDeserializer is loaded for SnapStart
512+
}
513+
514+
@Override
515+
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
516+
// ...
517+
return something;
518+
}
519+
}
520+
```

powertools-serialization/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
<description>Utilities for JSON serialization used across the project.</description>
3232

3333
<dependencies>
34+
<dependency>
35+
<groupId>org.crac</groupId>
36+
<artifactId>crac</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>software.amazon.lambda</groupId>
40+
<artifactId>powertools-common</artifactId>
41+
</dependency>
3442
<dependency>
3543
<groupId>io.burt</groupId>
3644
<artifactId>jmespath-jackson</artifactId>
@@ -100,6 +108,24 @@
100108
</build>
101109

102110
<profiles>
111+
<profile>
112+
<id>generate-classesloaded-file</id>
113+
<build>
114+
<plugins>
115+
<plugin>
116+
<groupId>org.apache.maven.plugins</groupId>
117+
<artifactId>maven-surefire-plugin</artifactId>
118+
<configuration>
119+
<argLine>
120+
-Xlog:class+load=info:classesloaded.txt
121+
--add-opens java.base/java.util=ALL-UNNAMED
122+
--add-opens java.base/java.lang=ALL-UNNAMED
123+
</argLine>
124+
</configuration>
125+
</plugin>
126+
</plugins>
127+
</build>
128+
</profile>
103129
<profile>
104130
<id>native</id>
105131
<build>

powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,51 @@
4141
import java.util.List;
4242
import java.util.Map;
4343
import java.util.stream.Collectors;
44+
import org.crac.Context;
45+
import org.crac.Core;
46+
import org.crac.Resource;
4447
import org.slf4j.Logger;
4548
import org.slf4j.LoggerFactory;
49+
import software.amazon.lambda.powertools.common.internal.ClassPreLoader;
4650

4751
/**
4852
* Class that can be used to extract the meaningful part of an event and deserialize it into a Java object.<br/>
4953
* For example, extract the body of an API Gateway event, or messages from an SQS event.
5054
*/
51-
public class EventDeserializer {
55+
public class EventDeserializer implements Resource {
5256

5357
private static final Logger LOG = LoggerFactory.getLogger(EventDeserializer.class);
58+
private static final EventDeserializer INSTANCE = new EventDeserializer();
59+
60+
static {
61+
Core.getGlobalContext().register(INSTANCE);
62+
}
63+
64+
public EventDeserializer() {
65+
}
66+
67+
/**
68+
* Ensures this class is loaded so CRaC hooks register before SnapStart takes a snapshot.
69+
*/
70+
public static void init() {
71+
// Referencing this method loads the class and runs the static CRaC registration.
72+
}
73+
74+
@Override
75+
public void beforeCheckpoint(Context<? extends Resource> context) {
76+
try {
77+
int primed = EventDeserializerPriming.prime().size();
78+
LOG.debug("SnapStart invoke priming completed for {} event types", primed);
79+
} catch (RuntimeException e) {
80+
LOG.debug("SnapStart invoke priming failed", e);
81+
}
82+
ClassPreLoader.preloadClasses();
83+
}
84+
85+
@Override
86+
public void afterRestore(Context<? extends Resource> context) {
87+
// No action needed after restore
88+
}
5489

5590
/**
5691
* Extract the meaningful part of a Lambda Event object. Main events are built-in:
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
15+
package software.amazon.lambda.powertools.utilities;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;
19+
20+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
21+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
22+
import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent;
23+
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent;
24+
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
25+
import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent;
26+
import com.amazonaws.services.lambda.runtime.events.KafkaEvent;
27+
import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent;
28+
import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent;
29+
import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
30+
import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent;
31+
import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent;
32+
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
33+
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
34+
import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
35+
import java.nio.ByteBuffer;
36+
import java.util.Base64;
37+
import java.util.LinkedHashSet;
38+
import java.util.List;
39+
import java.util.Map;
40+
import java.util.Objects;
41+
import java.util.Set;
42+
43+
/**
44+
* Invoke-primes {@link EventDeserializer} by running {@code extractDataFrom} plus Jackson
45+
* {@code as}/{@code asListOf} for every built-in event type.
46+
*/
47+
final class EventDeserializerPriming {
48+
49+
static final String SAMPLE_JSON = "{\"id\":1234,\"name\":\"product\",\"price\":42}";
50+
51+
private EventDeserializerPriming() {
52+
}
53+
54+
static Set<Class<?>> prime() {
55+
Objects.requireNonNull(JsonConfig.get().getObjectMapper());
56+
Objects.requireNonNull(JsonConfig.get().getJmesPath());
57+
58+
String sampleBase64 = Base64.getEncoder().encodeToString(SAMPLE_JSON.getBytes(UTF_8));
59+
Map<String, Object> sampleMap = Map.of("id", 1234, "name", "product", "price", 42);
60+
61+
Set<Class<?>> primed = new LinkedHashSet<>();
62+
primeAs(primed, String.class, SAMPLE_JSON);
63+
primeAs(primed, Map.class, sampleMap);
64+
// Warm the JSON-array asListOf path (same source type as a raw String event)
65+
Objects.requireNonNull(extractDataFrom("[" + SAMPLE_JSON + "]").asListOf(Map.class));
66+
67+
APIGatewayProxyRequestEvent apiV1 = new APIGatewayProxyRequestEvent();
68+
apiV1.setBody(SAMPLE_JSON);
69+
primeAs(primed, APIGatewayProxyRequestEvent.class, apiV1);
70+
71+
APIGatewayV2HTTPEvent apiV2 = new APIGatewayV2HTTPEvent();
72+
apiV2.setBody(SAMPLE_JSON);
73+
primeAs(primed, APIGatewayV2HTTPEvent.class, apiV2);
74+
75+
SNSEvent.SNS sns = new SNSEvent.SNS();
76+
sns.setMessage(SAMPLE_JSON);
77+
SNSEvent.SNSRecord snsRecord = new SNSEvent.SNSRecord();
78+
snsRecord.setSns(sns);
79+
SNSEvent snsEvent = new SNSEvent();
80+
snsEvent.setRecords(List.of(snsRecord));
81+
primeAs(primed, SNSEvent.class, snsEvent);
82+
83+
SQSEvent.SQSMessage sqsMessage = new SQSEvent.SQSMessage();
84+
sqsMessage.setBody(SAMPLE_JSON);
85+
SQSEvent sqsEvent = new SQSEvent();
86+
sqsEvent.setRecords(List.of(sqsMessage));
87+
primeAsList(primed, SQSEvent.class, sqsEvent);
88+
primeAs(primed, SQSEvent.SQSMessage.class, sqsMessage);
89+
90+
ScheduledEvent scheduledEvent = new ScheduledEvent();
91+
scheduledEvent.setDetail(sampleMap);
92+
primeAs(primed, ScheduledEvent.class, scheduledEvent);
93+
94+
ApplicationLoadBalancerRequestEvent albEvent = new ApplicationLoadBalancerRequestEvent();
95+
albEvent.setBody(SAMPLE_JSON);
96+
primeAs(primed, ApplicationLoadBalancerRequestEvent.class, albEvent);
97+
98+
CloudWatchLogsEvent.AWSLogs awsLogs = new CloudWatchLogsEvent.AWSLogs();
99+
awsLogs.setData(sampleBase64);
100+
CloudWatchLogsEvent cloudWatchLogsEvent = new CloudWatchLogsEvent();
101+
cloudWatchLogsEvent.setAwsLogs(awsLogs);
102+
primeAs(primed, CloudWatchLogsEvent.class, cloudWatchLogsEvent);
103+
104+
CloudFormationCustomResourceEvent cloudFormationEvent = new CloudFormationCustomResourceEvent();
105+
cloudFormationEvent.setResourceProperties(sampleMap);
106+
primeAs(primed, CloudFormationCustomResourceEvent.class, cloudFormationEvent);
107+
108+
KinesisEvent.KinesisEventRecord kinesisEventRecord = kinesisEventRecord();
109+
KinesisEvent kinesisEvent = new KinesisEvent();
110+
kinesisEvent.setRecords(List.of(kinesisEventRecord));
111+
primeAsList(primed, KinesisEvent.class, kinesisEvent);
112+
// Use a fresh record: decode(ByteBuffer) consumes the buffer position
113+
primeAs(primed, KinesisEvent.KinesisEventRecord.class, kinesisEventRecord());
114+
115+
KinesisFirehoseEvent.Record firehoseRecord = new KinesisFirehoseEvent.Record();
116+
firehoseRecord.setData(jsonBuffer());
117+
KinesisFirehoseEvent firehoseEvent = new KinesisFirehoseEvent();
118+
firehoseEvent.setRecords(List.of(firehoseRecord));
119+
primeAsList(primed, KinesisFirehoseEvent.class, firehoseEvent);
120+
121+
KafkaEvent.KafkaEventRecord kafkaRecord = new KafkaEvent.KafkaEventRecord();
122+
kafkaRecord.setValue(sampleBase64);
123+
KafkaEvent kafkaEvent = new KafkaEvent();
124+
kafkaEvent.setRecords(Map.of("topic", List.of(kafkaRecord)));
125+
primeAsList(primed, KafkaEvent.class, kafkaEvent);
126+
127+
ActiveMQEvent.ActiveMQMessage activeMqMessage = new ActiveMQEvent.ActiveMQMessage();
128+
activeMqMessage.setData(sampleBase64);
129+
ActiveMQEvent activeMqEvent = new ActiveMQEvent();
130+
activeMqEvent.setMessages(List.of(activeMqMessage));
131+
primeAsList(primed, ActiveMQEvent.class, activeMqEvent);
132+
133+
RabbitMQEvent.RabbitMessage rabbitMessage = new RabbitMQEvent.RabbitMessage();
134+
rabbitMessage.setData(sampleBase64);
135+
RabbitMQEvent rabbitMqEvent = new RabbitMQEvent();
136+
rabbitMqEvent.setRmqMessagesByQueue(Map.of("queue", List.of(rabbitMessage)));
137+
primeAsList(primed, RabbitMQEvent.class, rabbitMqEvent);
138+
139+
KinesisAnalyticsFirehoseInputPreprocessingEvent.Record kaFirehoseRecord =
140+
new KinesisAnalyticsFirehoseInputPreprocessingEvent.Record();
141+
kaFirehoseRecord.setData(jsonBuffer());
142+
KinesisAnalyticsFirehoseInputPreprocessingEvent kaFirehoseEvent =
143+
new KinesisAnalyticsFirehoseInputPreprocessingEvent();
144+
kaFirehoseEvent.setRecords(List.of(kaFirehoseRecord));
145+
primeAsList(primed, KinesisAnalyticsFirehoseInputPreprocessingEvent.class, kaFirehoseEvent);
146+
147+
KinesisAnalyticsStreamsInputPreprocessingEvent.Record kaStreamsRecord =
148+
new KinesisAnalyticsStreamsInputPreprocessingEvent.Record();
149+
kaStreamsRecord.setData(jsonBuffer());
150+
KinesisAnalyticsStreamsInputPreprocessingEvent kaStreamsEvent =
151+
new KinesisAnalyticsStreamsInputPreprocessingEvent();
152+
kaStreamsEvent.setRecords(List.of(kaStreamsRecord));
153+
primeAsList(primed, KinesisAnalyticsStreamsInputPreprocessingEvent.class, kaStreamsEvent);
154+
155+
return primed;
156+
}
157+
158+
private static void primeAs(Set<Class<?>> primed, Class<?> eventType, Object event) {
159+
Objects.requireNonNull(extractDataFrom(event).as(Map.class));
160+
primed.add(eventType);
161+
}
162+
163+
private static void primeAsList(Set<Class<?>> primed, Class<?> eventType, Object event) {
164+
Objects.requireNonNull(extractDataFrom(event).asListOf(Map.class));
165+
primed.add(eventType);
166+
}
167+
168+
private static ByteBuffer jsonBuffer() {
169+
return ByteBuffer.wrap(SAMPLE_JSON.getBytes(UTF_8));
170+
}
171+
172+
private static KinesisEvent.KinesisEventRecord kinesisEventRecord() {
173+
KinesisEvent.Record kinesisRecord = new KinesisEvent.Record();
174+
kinesisRecord.setData(jsonBuffer());
175+
KinesisEvent.KinesisEventRecord kinesisEventRecord = new KinesisEvent.KinesisEventRecord();
176+
kinesisEventRecord.setKinesis(kinesisRecord);
177+
return kinesisEventRecord;
178+
}
179+
}

0 commit comments

Comments
 (0)