Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ sonar.exclusions=examples/**/*,powertools-e2e-tests/handlers/**/*

# Ignore code duplicates in the examples
sonar.cpd.exclusions=examples/**/*,powertools-e2e-tests/**/*

# Ignore singleton pattern detection for CRaC Resource implementations
# Singleton pattern is required for CRaC (Coordinated Restore at Checkpoint) Resource interface
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=java:S6548
sonar.issue.ignore.multicriteria.e1.resourceKey=**/TracingUtils.java,**/JsonConfig.java
50 changes: 50 additions & 0 deletions docs/utilities/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,53 @@ to powertools.You can then use it to do your validation or in idempotency module
}
}
```

## Advanced

### Lambda SnapStart priming

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 `JsonConfig` 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"}.

If you don't set a custom `JsonConfig` in your code yet, make sure to reference `JsonConfig` in your Lambda handler initialization code. This can be done by adding one of the following lines to your handler class:

=== "Constructor"

```java hl_lines="7"
import software.amazon.lambda.powertools.utilities.JsonConfig;
import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;

public class MyFunctionHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

public MyFunctionHandler() {
JsonConfig.get(); // Ensure JsonConfig is loaded for SnapStart
}

@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
Product product = extractDataFrom(input).as(Product.class);
// ...
return something;
}
}
```

=== "Static Initializer"

```java hl_lines="7"
import software.amazon.lambda.powertools.utilities.JsonConfig;
import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;

public class MyFunctionHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

static {
JsonConfig.get(); // Ensure JsonConfig is loaded for SnapStart
}

@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
Product product = extractDataFrom(input).as(Product.class);
// ...
return something;
}
}
```
5,295 changes: 5,295 additions & 0 deletions powertools-serialization/classesloaded.txt

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions powertools-serialization/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.crac</groupId>
<artifactId>crac</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-common</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down Expand Up @@ -74,6 +82,11 @@
<artifactId>aws-lambda-java-tests</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand All @@ -96,6 +109,24 @@
</build>

<profiles>
<profile>
<id>generate-classesloaded-file</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
-Xlog:class+load=info:classesloaded.txt
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>generate-graalvm-files</id>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,33 @@
import io.burt.jmespath.function.FunctionRegistry;
import io.burt.jmespath.jackson.JacksonRuntime;
import java.util.function.Supplier;
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent;
import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent;
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent;
import com.amazonaws.services.lambda.runtime.events.KafkaEvent;
import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent;
import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent;
import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent;
import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
import software.amazon.lambda.powertools.common.internal.ClassPreLoader;
import software.amazon.lambda.powertools.utilities.jmespath.Base64Function;
import software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction;
import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction;

public final class JsonConfig {
/**
* Factory for accessing the singleton JsonConfig instance
*/
public final class JsonConfig implements Resource {

private static final Supplier<ObjectMapper> objectMapperSupplier = () -> JsonMapper.builder()
// Don't throw an exception when json has extra fields you are not serializing on.
Expand Down Expand Up @@ -61,11 +83,21 @@ public final class JsonConfig {

private JmesPath<JsonNode> jmesPath = new JacksonRuntime(configuration, getObjectMapper());

private JsonConfig() {
// Dummy instance to register JsonConfig with CRaC
private static final JsonConfig INSTANCE = new JsonConfig();

// Static block to ensure CRaC registration happens at class loading time
static {
Core.getGlobalContext().register(INSTANCE);
}

/**
* Get the singleton instance of the JsonConfig
*
* @return the singleton JsonConfig instance
*/
public static JsonConfig get() {
return ConfigHolder.instance;
return INSTANCE;
}

/**
Expand Down Expand Up @@ -103,7 +135,56 @@ public <T extends BaseFunction> void addFunction(T function) {
jmesPath = new JacksonRuntime(updatedConfig, getObjectMapper());
}

private static class ConfigHolder {
private static final JsonConfig instance = new JsonConfig();
/**
* Prime JsonConfig for AWS Lambda SnapStart.
* This method has no side-effects and can be safely called to trigger SnapStart priming.
*/
public static void prime() {
// This method intentionally does nothing but ensures JsonConfig is loaded
// The actual priming happens in the beforeCheckpoint() method via CRaC hooks
JsonConfig.get();
}

@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
// Preload classes first to ensure this always runs
ClassPreLoader.preloadClasses();

// Initialize key components
ObjectMapper mapper = getObjectMapper();
getJmesPath();

// Prime common AWS Lambda event types with realistic events (invoke priming)
primeEventType(mapper, APIGatewayProxyRequestEvent.class,
"{\"httpMethod\":\"GET\",\"path\":\"/test\",\"headers\":{\"Content-Type\":\"application/json\"},\"requestContext\":{\"accountId\":\"123456789012\"}}");
primeEventType(mapper, APIGatewayV2HTTPEvent.class,
"{\"version\":\"2.0\",\"routeKey\":\"GET /test\",\"requestContext\":{\"http\":{\"method\":\"GET\"},\"accountId\":\"123456789012\"}}");
primeEventType(mapper, SQSEvent.class,
"{\"Records\":[{\"messageId\":\"test-id\",\"body\":\"test message\",\"eventSource\":\"aws:sqs\"}]}");
primeEventType(mapper, SNSEvent.class,
"{\"Records\":[{\"Sns\":{\"Message\":\"test message\",\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:test\"}}]}");
primeEventType(mapper, KinesisEvent.class,
"{\"Records\":[{\"kinesis\":{\"data\":\"dGVzdA==\",\"partitionKey\":\"test\"},\"eventSource\":\"aws:kinesis\"}]}");
primeEventType(mapper, ScheduledEvent.class,
"{\"source\":\"aws.events\",\"detail-type\":\"Scheduled Event\",\"detail\":{}}");

// Warm up JMESPath function registry
getJmesPath().compile("@").search(mapper.readTree("{\"test\":\"value\"}"));
}

@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
// No action needed after restore
}

private void primeEventType(ObjectMapper mapper, Class<?> eventClass, String sampleJson) throws JsonPrimingException {
try {
// Deserialize sample JSON to the event class
Object event = mapper.readValue(sampleJson, eventClass);
// Serialize back to JSON to warm up both directions
mapper.writeValueAsString(event);
} catch (Exception e) {
throw new JsonPrimingException("Failed to prime event type " + eventClass.getSimpleName(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.utilities;

/**
* Exception thrown when JSON priming operations fail during CRaC checkpoint preparation.
*/
public class JsonPrimingException extends Exception {

public JsonPrimingException(String message, Throwable cause) {
super(message, cause);
}

public JsonPrimingException(String message) {
super(message);
}
}
74 changes: 74 additions & 0 deletions powertools-serialization/src/main/resources/classesloaded.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
java.lang.Object
java.io.Serializable
java.lang.Comparable
java.lang.CharSequence
java.lang.String
java.lang.Class
java.lang.Cloneable
java.lang.ClassLoader
java.lang.System
java.lang.Throwable
java.lang.Error
java.lang.Exception
java.lang.RuntimeException
com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.databind.JsonNode
com.fasterxml.jackson.databind.node.ObjectNode
com.fasterxml.jackson.databind.node.ArrayNode
com.fasterxml.jackson.databind.node.TextNode
com.fasterxml.jackson.databind.node.NumericNode
com.fasterxml.jackson.databind.node.BooleanNode
com.fasterxml.jackson.databind.node.NullNode
com.fasterxml.jackson.databind.json.JsonMapper
com.fasterxml.jackson.core.JsonFactory
com.fasterxml.jackson.core.JsonGenerator
com.fasterxml.jackson.core.JsonParser
com.fasterxml.jackson.core.JsonToken
com.fasterxml.jackson.databind.DeserializationFeature
com.fasterxml.jackson.databind.SerializationFeature
com.fasterxml.jackson.databind.MapperFeature
com.fasterxml.jackson.databind.JsonSerializer
com.fasterxml.jackson.databind.JsonDeserializer
com.fasterxml.jackson.databind.SerializerProvider
com.fasterxml.jackson.databind.DeserializationContext
com.fasterxml.jackson.annotation.JsonInclude
com.fasterxml.jackson.annotation.JsonInclude$Include
io.burt.jmespath.JmesPath
io.burt.jmespath.RuntimeConfiguration
io.burt.jmespath.RuntimeConfiguration$Builder
io.burt.jmespath.function.BaseFunction
io.burt.jmespath.function.FunctionRegistry
io.burt.jmespath.jackson.JacksonRuntime
software.amazon.lambda.powertools.utilities.JsonConfig
software.amazon.lambda.powertools.utilities.EventDeserializer
software.amazon.lambda.powertools.utilities.EventDeserializationException
software.amazon.lambda.powertools.utilities.jmespath.Base64Function
software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction
software.amazon.lambda.powertools.utilities.jmespath.JsonFunction
com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent
com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse
com.amazonaws.services.lambda.runtime.events.ActiveMQEvent
com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent
com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent
com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent
com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent
com.amazonaws.services.lambda.runtime.events.KafkaEvent
com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent
com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent
com.amazonaws.services.lambda.runtime.events.KinesisEvent
com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent
com.amazonaws.services.lambda.runtime.events.RabbitMQEvent
com.amazonaws.services.lambda.runtime.events.SNSEvent
com.amazonaws.services.lambda.runtime.events.SQSEvent
com.amazonaws.services.lambda.runtime.events.ScheduledEvent
org.slf4j.Logger
org.slf4j.LoggerFactory
java.util.function.Supplier
java.lang.ThreadLocal
java.util.Map
java.util.HashMap
java.util.List
java.util.ArrayList
java.util.concurrent.ConcurrentHashMap
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.utilities;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mock;

import org.crac.Context;
import org.crac.Resource;
import org.junit.jupiter.api.Test;

class JsonConfigCracTest {

JsonConfig config = JsonConfig.get();
Context<Resource> context = mock(Context.class);

@Test
void testBeforeCheckpointDoesNotThrowException() {
assertThatNoException().isThrownBy(() -> config.beforeCheckpoint(context));
}

@Test
void testAfterRestoreDoesNotThrowException() {
assertThatNoException().isThrownBy(() -> config.afterRestore(context));
}
}
1 change: 1 addition & 0 deletions powertools-triaging-template-java
Submodule powertools-triaging-template-java added at f4a169
12 changes: 10 additions & 2 deletions spotbugs-exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,16 @@
</Match>
<Match>
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
<Class name="software.amazon.lambda.powertools.validation.ValidationConfig"/>
<Method name="beforeCheckpoint"/>
<Or>
<And>
<Class name="software.amazon.lambda.powertools.validation.ValidationConfig"/>
<Method name="beforeCheckpoint"/>
</And>
<And>
<Class name="software.amazon.lambda.powertools.utilities.JsonConfig"/>
<Method name="beforeCheckpoint"/>
</And>
</Or>
</Match>
<!--Functionally needed-->
<Match>
Expand Down