DurableConfig.java
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.lambda.LambdaClient;
import software.amazon.awssdk.services.lambda.LambdaClientBuilder;
import software.amazon.awssdk.services.lambda.model.GetDurableExecutionStateRequest;
import software.amazon.lambda.durable.client.DurableExecutionClient;
import software.amazon.lambda.durable.client.LambdaDurableFunctionsClient;
import software.amazon.lambda.durable.logging.LoggerConfig;
import software.amazon.lambda.durable.retry.PollingStrategies;
import software.amazon.lambda.durable.retry.PollingStrategy;
import software.amazon.lambda.durable.serde.JacksonSerDes;
import software.amazon.lambda.durable.serde.SerDes;
/**
* Configuration for DurableHandler initialization. This class provides a builder pattern for configuring SDK components
* including LambdaClient, SerDes, and ExecutorService.
*
* <p>Configuration is initialized once during Lambda cold start and remains immutable throughout the execution
* lifecycle.
*
* <p>Example usage with default settings:
*
* <pre>{@code
* @Override
* protected DurableConfig createConfiguration() {
* return DurableConfig.builder()
* .withDurableExecutionClient(customClient)
* .withSerDes(customSerDes)
* .build();
* }
* }</pre>
*
* <p>Example usage with custom Lambda client:
*
* <pre>{@code
* @Override
* protected DurableConfig createConfiguration() {
* LambdaClient lambdaClient = LambdaClient.builder()
* .region(Region.US_WEST_2)
* .credentialsProvider(ProfileCredentialsProvider.create("my-profile"))
* .build();
*
* return DurableConfig.builder()
* .withLambdaClient(lambdaClient)
* .build();
* }
* }</pre>
*/
public final class DurableConfig {
private static final Logger logger = LoggerFactory.getLogger(DurableConfig.class);
/**
* Default AWS region used when AWS_REGION environment variable is not set. This prevents initialization failures in
* testing environments where AWS credentials may not be configured. In production Lambda environments, AWS_REGION
* is always set by the Lambda runtime.
*/
private static final String DEFAULT_REGION = "us-east-1";
private static final String VERSION_FILE = "/version.prop";
private static final String PROJECT_VERSION = getProjectVersion(VERSION_FILE);
private static final String USER_AGENT_SUFFIX = "@aws/durable-execution-sdk-java/" + PROJECT_VERSION;
private final DurableExecutionClient durableExecutionClient;
private final SerDes serDes;
private final ExecutorService executorService;
private final LoggerConfig loggerConfig;
private final PollingStrategy pollingStrategy;
private final Duration checkpointDelay;
private DurableConfig(Builder builder) {
this.durableExecutionClient = builder.durableExecutionClient != null
? builder.durableExecutionClient
: createDefaultDurableExecutionClient();
this.serDes = builder.serDes != null ? builder.serDes : new JacksonSerDes();
this.executorService = builder.executorService != null ? builder.executorService : createDefaultExecutor();
this.loggerConfig = builder.loggerConfig != null ? builder.loggerConfig : LoggerConfig.defaults();
this.pollingStrategy =
builder.pollingStrategy != null ? builder.pollingStrategy : PollingStrategies.Presets.DEFAULT;
this.checkpointDelay = builder.checkpointDelay != null ? builder.checkpointDelay : Duration.ofSeconds(0);
}
/**
* Creates a DurableConfig with default settings. Uses default DurableExecutionClient and JacksonSerDes.
*
* @return DurableConfig with default configuration
*/
public static DurableConfig defaultConfig() {
return new Builder().build();
}
/**
* Creates a new builder for DurableConfig.
*
* @return Builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Gets the configured DurableExecutionClient.
*
* @return DurableExecutionClient instance
*/
public DurableExecutionClient getDurableExecutionClient() {
return durableExecutionClient;
}
/**
* Gets the configured SerDes.
*
* @return SerDes instance
*/
public SerDes getSerDes() {
return serDes;
}
/**
* Gets the configured ExecutorService.
*
* @return ExecutorService instance (never null)
*/
public ExecutorService getExecutorService() {
return executorService;
}
/**
* Gets the configured LoggerConfig.
*
* @return LoggerConfig instance (never null)
*/
public LoggerConfig getLoggerConfig() {
return loggerConfig;
}
/**
* Gets the polling strategy.
*
* @return PollingStrategy instance (never null)
*/
public PollingStrategy getPollingStrategy() {
return pollingStrategy;
}
/**
* Gets the configured checkpoint delay.
*
* @return check point in Duration.
*/
public Duration getCheckpointDelay() {
return checkpointDelay;
}
/**
* Creates a default DurableExecutionClient with production LambdaClient. Uses
* EnvironmentVariableCredentialsProvider and region from AWS_REGION. If AWS_REGION is not set, defaults to
* us-east-1 to avoid initialization failures in testing environments.
*
* @return Default DurableExecutionClient instance
*/
private static DurableExecutionClient createDefaultDurableExecutionClient() {
logger.debug("Creating default DurableExecutionClient");
var region = System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable());
if (region == null || region.isEmpty()) {
region = DEFAULT_REGION;
logger.debug("AWS_REGION not set, defaulting to: {}", region);
}
var lambdaClient = addUserAgentSuffix(LambdaClient.builder()
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(Region.of(region)))
.build();
try {
// Make a dummy call to prime the SDK client. This leads to faster first call times because the HTTP client
// is already warmed up when the handler executes. More details, see here:
// https://github.com/aws/aws-sdk-java-v2/issues/1340
// https://github.com/aws/aws-sdk-java-v2/issues/3801
lambdaClient.getDurableExecutionState(GetDurableExecutionStateRequest.builder()
.checkpointToken("dummyToken")
.durableExecutionArn(String.format(
"arn:aws:lambda:%s:123456789012:function:dummy:$LATEST/durable-execution/a0c9cbab-3de6-49ea-8630-0ef3bb4874e4/ed8a29c0-6216-3f4a-ad2e-24e2ad70b2d6",
region))
.maxItems(0)
.build());
} catch (Exception e) {
// Ignore exceptions since this is a dummy call to prime the SDK client for faster startup times
}
logger.debug("Default DurableExecutionClient created for region: {}", region);
return new LambdaDurableFunctionsClient(lambdaClient);
}
static LambdaClientBuilder addUserAgentSuffix(LambdaClientBuilder builder) {
return builder.overrideConfiguration(builder.overrideConfiguration().toBuilder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, USER_AGENT_SUFFIX)
.build());
}
private static String getProjectVersion(String versionFile) {
InputStream stream = DurableConfig.class.getResourceAsStream(versionFile);
if (stream == null) {
return "UNKNOWN";
}
Properties props = new Properties();
try {
props.load(stream);
stream.close();
return (String) props.get("version");
} catch (IOException e) {
return "UNKNOWN";
}
}
/**
* Creates a default ExecutorService for running user-defined operations. Uses a cached thread pool with daemon
* threads by default.
*
* <p>This executor is used exclusively for user operations. Internal SDK coordination uses the common ForkJoinPool.
*
* @return Default ExecutorService instance
*/
private static ExecutorService createDefaultExecutor() {
logger.debug("Creating default ExecutorService");
return Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setName("durable-exec-" + t.getId());
t.setDaemon(true);
return t;
});
}
/** Builder for DurableConfig. Provides fluent API for configuring SDK components. */
public static final class Builder {
private DurableExecutionClient durableExecutionClient;
private SerDes serDes;
private ExecutorService executorService;
private LoggerConfig loggerConfig;
private PollingStrategy pollingStrategy;
private Duration checkpointDelay;
private Builder() {}
/**
* Sets a custom LambdaClient for production use. Use this method to customize the AWS SDK client with specific
* regions, credentials, timeouts, or retry policies.
*
* <p>Example:
*
* <pre>{@code
* LambdaClientBuilder lambdaClientBuilder = LambdaClient.builder()
* .region(Region.US_WEST_2)
* .credentialsProvider(ProfileCredentialsProvider.create("my-profile"));
*
* DurableConfig.builder()
* .withLambdaClientBuilder(lambdaClientBuilder)
* .build();
* }</pre>
*
* @param lambdaClientBuilder Custom LambdaClientBuilder instance
* @return This builder
* @throws NullPointerException if lambdaClient is null
*/
public Builder withLambdaClientBuilder(LambdaClientBuilder lambdaClientBuilder) {
Objects.requireNonNull(lambdaClientBuilder, "LambdaClient cannot be null");
this.durableExecutionClient = new LambdaDurableFunctionsClient(
addUserAgentSuffix(lambdaClientBuilder).build());
return this;
}
/**
* Sets a custom DurableExecutionClient.
*
* <p><b>Note:</b> This method is primarily intended for testing with mock clients (e.g.,
* {@code LocalMemoryExecutionClient}). For production use with a custom AWS SDK client, prefer
* {@link #withLambdaClientBuilder(LambdaClientBuilder)}.
*
* @param durableExecutionClient Custom DurableExecutionClient instance
* @return This builder
* @throws NullPointerException if durableExecutionClient is null
*/
public Builder withDurableExecutionClient(DurableExecutionClient durableExecutionClient) {
this.durableExecutionClient =
Objects.requireNonNull(durableExecutionClient, "DurableExecutionClient cannot be null");
return this;
}
/**
* Sets a custom SerDes implementation.
*
* @param serDes Custom SerDes instance
* @return This builder
* @throws NullPointerException if serDes is null
*/
public Builder withSerDes(SerDes serDes) {
this.serDes = Objects.requireNonNull(serDes, "SerDes cannot be null");
return this;
}
/**
* Sets a custom ExecutorService for running user-defined operations. If not set, a default cached thread pool
* will be created.
*
* <p>This executor is used exclusively for running user-defined operations. Internal SDK coordination (polling,
* checkpointing) uses the common ForkJoinPool and is not affected by this setting.
*
* @param executorService Custom ExecutorService instance
* @return This builder
*/
public Builder withExecutorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}
/**
* Sets a custom LoggerConfig. If not set, defaults to suppressing replay logs.
*
* @param loggerConfig Custom LoggerConfig instance
* @return This builder
*/
public Builder withLoggerConfig(LoggerConfig loggerConfig) {
this.loggerConfig = Objects.requireNonNull(loggerConfig, "LoggerConfig cannot be null");
return this;
}
/**
* Sets the polling strategy. If not set, defaults to 1 second with full jitter and 2x backoff.
*
* @param pollingStrategy Custom PollingStrategy instance
* @return This builder
*/
public Builder withPollingStrategy(PollingStrategy pollingStrategy) {
// No validation - polling intervals can be less than 1 second (e.g., 200ms with backoff)
this.pollingStrategy = pollingStrategy;
return this;
}
/**
* Sets how often the SDK checkpoints updates to backend. If not set, defaults to 0, which disables checkpoint
* batching.
*
* @param duration the checkpoint delay in Duration
* @return This builder
*/
public Builder withCheckpointDelay(Duration duration) {
this.checkpointDelay = duration;
return this;
}
/**
* Builds the DurableConfig instance.
*
* @return Immutable DurableConfig instance
*/
public DurableConfig build() {
return new DurableConfig(this);
}
}
}