diff --git a/java/cdk-custom-resources/src/main/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambda.java b/java/cdk-custom-resources/src/main/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambda.java index 51b17c1d013..21035804966 100644 --- a/java/cdk-custom-resources/src/main/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambda.java +++ b/java/cdk-custom-resources/src/main/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambda.java @@ -77,6 +77,7 @@ public void handleEvent( } private void stopApplication(String applicationId) throws InterruptedException { + LOGGER.info("Terminating {} running application: ", applicationId); List jobRuns = emrServerlessClient.listJobRuns(request -> request.applicationId(applicationId) @@ -91,10 +92,12 @@ private void stopApplication(String applicationId) throws InterruptedException { poll.pollUntil("all EMR Serverless jobs finished", () -> allJobsFinished(applicationId)); } - emrServerlessClient.stopApplication(request -> request.applicationId(applicationId)); + if (!isApplicationStopped(applicationId)) { + emrServerlessClient.stopApplication(request -> request.applicationId(applicationId)); - LOGGER.info("Waiting for applications to stop"); - poll.pollUntil("all EMR Serverless applications stopped", () -> isApplicationStopped(applicationId)); + LOGGER.info("Waiting for applications to stop"); + poll.pollUntil("all EMR Serverless applications stopped", () -> isApplicationStopped(applicationId)); + } } private boolean allJobsFinished(String applicationId) { diff --git a/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambdaIT.java b/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambdaIT.java index e732d1969d9..ca84fe1975d 100644 --- a/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambdaIT.java +++ b/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/AutoStopEmrServerlessApplicationLambdaIT.java @@ -21,6 +21,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.emrserverless.model.ApplicationState; import software.amazon.awssdk.services.emrserverless.model.JobRunState; import sleeper.core.util.PollWithRetries; @@ -34,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithJobRunWithState; import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithNoJobRuns; +import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithStartedApplication; import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithStoppedApplication; import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithStoppingApplication; import static sleeper.cdk.custom.WiremockEmrServerlessTestHelper.aResponseWithTerminatedApplication; @@ -64,13 +66,22 @@ void shouldTimeOutWhenDeleting(WireMockRuntimeInfo runtimeInfo) throws Exception // Given stubFor(listRunningJobsForApplicationRequest(applicationId) - .willReturn(aResponseWithNoJobRuns())); - stubFor(stopApplicationRequest(applicationId).inScenario("StopApplication") + .inScenario("ShouldTimeOut") + .willReturn(aResponseWithStartedApplication(applicationId)) + .willSetStateTo(STARTED)); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldTimeOut") + .willReturn(aResponseWithStartedApplication(applicationId)) + .whenScenarioStateIs(STARTED)); + stubFor(stopApplicationRequest(applicationId) + .inScenario("ShouldTimeOut") .willReturn(aResponse().withStatus(200)) - .whenScenarioStateIs(STARTED).willSetStateTo("ApplicationStopping")); - stubFor(getApplicationRequest(applicationId).inScenario("StopApplication") + .whenScenarioStateIs(STARTED) + .willSetStateTo(ApplicationState.STOPPING.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldTimeOut") .willReturn(aResponseWithStoppingApplication(applicationId)) - .whenScenarioStateIs("ApplicationStopping")); + .whenScenarioStateIs(ApplicationState.STOPPING.toString())); // Then assertThatThrownBy(() -> lambda.handleEvent(applicationEvent(applicationId, "Delete"), null)) @@ -84,23 +95,34 @@ void shouldTrackStoppingApplication(WireMockRuntimeInfo runtimeInfo) throws Exce // Given stubFor(listRunningJobsForApplicationRequest(applicationId) - .willReturn(aResponseWithNoJobRuns())); - stubFor(stopApplicationRequest(applicationId).inScenario("StopApplication") + .inScenario("ShouldTrackStopping") + .willReturn(aResponseWithNoJobRuns()) + .willSetStateTo(STARTED)); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldTrackStopping") + .willReturn(aResponseWithStartedApplication(applicationId)) + .whenScenarioStateIs(STARTED)); + stubFor(stopApplicationRequest(applicationId) + .inScenario("ShouldTrackStopping") .willReturn(aResponse().withStatus(200)) - .whenScenarioStateIs(STARTED).willSetStateTo("ApplicationStopping")); - stubFor(getApplicationRequest(applicationId).inScenario("StopApplication") + .whenScenarioStateIs(STARTED) + .willSetStateTo(ApplicationState.STOPPING.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldTrackStopping") .willReturn(aResponseWithStoppingApplication(applicationId)) - .whenScenarioStateIs("ApplicationStopping").willSetStateTo("ApplicationStopped")); - stubFor(getApplicationRequest(applicationId).inScenario("StopApplication") + .whenScenarioStateIs(ApplicationState.STOPPING.toString()) + .willSetStateTo(ApplicationState.STOPPED.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldTrackStopping") .willReturn(aResponseWithStoppedApplication(applicationId)) - .whenScenarioStateIs("ApplicationStopped")); + .whenScenarioStateIs(ApplicationState.STOPPED.toString())); // Then lambda.handleEvent(applicationEvent(applicationId, "Delete"), null); // Then - verify(4, anyRequestedForEmrServerless()); + verify(5, anyRequestedForEmrServerless()); verify(1, stopApplicationRequested(applicationId)); - verify(2, getApplicationRequested(applicationId)); + verify(3, getApplicationRequested(applicationId)); } @@ -110,32 +132,44 @@ void shouldStopEMRServerlessWhenApplicationIsStartedWithRunningJob(WireMockRunti lambda = lambda(runtimeInfo, PollWithRetries.noRetries()); // Given - stubFor(listRunningJobsForApplicationRequest(applicationId).inScenario("StopJob") + stubFor(listRunningJobsForApplicationRequest(applicationId) + .inScenario("ShouldStopWithJobs") .willReturn(aResponseWithJobRunWithState(applicationId, jobRunId, JobRunState.RUNNING)) - .whenScenarioStateIs(STARTED)); - stubFor(cancelJobRunRequest(applicationId, jobRunId).inScenario("StopJob") + .willSetStateTo(STARTED)); + stubFor(cancelJobRunRequest(applicationId, jobRunId) + .inScenario("ShouldStopWithJobs") .willReturn(ResponseDefinitionBuilder.okForEmptyJson()) - .whenScenarioStateIs(STARTED).willSetStateTo("JobStopped")); - stubFor(listRunningOrCancellingJobsForApplicationRequest(applicationId).inScenario("StopJob") + .whenScenarioStateIs(STARTED) + .willSetStateTo(JobRunState.CANCELLING.toString())); + stubFor(listRunningOrCancellingJobsForApplicationRequest(applicationId) + .inScenario("ShouldStopWithJobs") .willReturn(aResponseWithNoJobRuns()) - .whenScenarioStateIs("JobStopped")); - stubFor(stopApplicationRequest(applicationId).inScenario("StopJob") + .whenScenarioStateIs(JobRunState.CANCELLING.toString()) + .willSetStateTo(JobRunState.CANCELLED.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldStopWithJobs") + .willReturn(aResponseWithStartedApplication(applicationId)) + .whenScenarioStateIs(JobRunState.CANCELLED.toString())); + stubFor(stopApplicationRequest(applicationId) + .inScenario("ShouldStopWithJobs") .willReturn(aResponse().withStatus(200)) - .whenScenarioStateIs("JobStopped").willSetStateTo("AppStopped")); - stubFor(getApplicationRequest(applicationId).inScenario("StopJob") + .whenScenarioStateIs(JobRunState.CANCELLED.toString()) + .willSetStateTo(ApplicationState.STOPPED.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldStopWithJobs") .willReturn(aResponseWithTerminatedApplication(applicationId)) - .whenScenarioStateIs("AppStopped")); + .whenScenarioStateIs(ApplicationState.STOPPED.toString())); // When lambda.handleEvent(applicationEvent(applicationId, "Delete"), null); // Then - verify(5, anyRequestedForEmrServerless()); + verify(6, anyRequestedForEmrServerless()); verify(1, listRunningJobsForApplicationRequested(applicationId)); verify(1, cancelJobRunRequested(applicationId, jobRunId)); verify(1, listRunningOrCancellingJobsForApplicationRequested(applicationId)); verify(1, stopApplicationRequested(applicationId)); - verify(1, getApplicationRequested(applicationId)); + verify(2, getApplicationRequested(applicationId)); } @Test @@ -145,22 +179,31 @@ void shouldStopEMRServerlessWhenApplicationIsStartedWithNoRunningJobs(WireMockRu // Given stubFor(listRunningJobsForApplicationRequest(applicationId) - .willReturn(aResponseWithNoJobRuns())); - stubFor(stopApplicationRequest(applicationId).inScenario("StopApplication") + .inScenario("ShouldStopNoJobs") + .willReturn(aResponseWithNoJobRuns()) + .willSetStateTo(STARTED)); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldStopNoJobs") + .willReturn(aResponseWithStartedApplication(applicationId)) + .whenScenarioStateIs(STARTED)); + stubFor(stopApplicationRequest(applicationId) + .inScenario("ShouldStopNoJobs") .willReturn(aResponse().withStatus(200)) - .whenScenarioStateIs(STARTED).willSetStateTo("ApplicationStopped")); - stubFor(getApplicationRequest(applicationId).inScenario("StopApplication") + .whenScenarioStateIs(STARTED) + .willSetStateTo(ApplicationState.STOPPED.toString())); + stubFor(getApplicationRequest(applicationId) + .inScenario("ShouldStopNoJobs") .willReturn(aResponseWithTerminatedApplication(applicationId)) - .whenScenarioStateIs("ApplicationStopped")); + .whenScenarioStateIs(ApplicationState.STOPPED.toString())); // When lambda.handleEvent(applicationEvent(applicationId, "Delete"), null); // Then - verify(3, anyRequestedForEmrServerless()); + verify(4, anyRequestedForEmrServerless()); verify(1, listRunningJobsForApplicationRequested(applicationId)); verify(1, stopApplicationRequested(applicationId)); - verify(1, getApplicationRequested(applicationId)); + verify(2, getApplicationRequested(applicationId)); } @Test diff --git a/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/WiremockEmrServerlessTestHelper.java b/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/WiremockEmrServerlessTestHelper.java index a15c82ff819..4dd88d28480 100644 --- a/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/WiremockEmrServerlessTestHelper.java +++ b/java/cdk-custom-resources/src/test/java/sleeper/cdk/custom/WiremockEmrServerlessTestHelper.java @@ -194,15 +194,15 @@ public static ResponseDefinitionBuilder aResponseWithTerminatedApplication(Strin } /** - * Build an EMR application response for a running application. + * Build an EMR application response for a started application. * * @param applicationId the application id * @return a HTTP response */ - public static ResponseDefinitionBuilder aResponseWithRunningApplication(String applicationId) { + public static ResponseDefinitionBuilder aResponseWithStartedApplication(String applicationId) { return aResponse().withStatus(200).withBody("{\"application\":{" + "\"applicationId\":\"" + applicationId + "\"," + - "\"state\":\"RUNNING\"" + + "\"state\":\"STARTED\"" + "}}"); } diff --git a/java/cdk/src/main/java/sleeper/cdk/SleeperCdkApp.java b/java/cdk/src/main/java/sleeper/cdk/SleeperCdkApp.java index ac0b11540d1..8412e129e02 100644 --- a/java/cdk/src/main/java/sleeper/cdk/SleeperCdkApp.java +++ b/java/cdk/src/main/java/sleeper/cdk/SleeperCdkApp.java @@ -45,6 +45,7 @@ import sleeper.cdk.stack.compaction.CompactionTrackerResources; import sleeper.cdk.stack.core.AutoDeleteS3ObjectsStack; import sleeper.cdk.stack.core.AutoStopEcsClusterTasksStack; +import sleeper.cdk.stack.core.AutoStopEmrServerlessApplicationStack; import sleeper.cdk.stack.core.ConfigBucketStack; import sleeper.cdk.stack.core.CoreStacks; import sleeper.cdk.stack.core.LoggingStack; @@ -106,6 +107,7 @@ public class SleeperCdkApp extends Stack { private QueryQueueStack queryQueueStack; private AutoDeleteS3ObjectsStack autoDeleteS3ObjectsStack; private AutoStopEcsClusterTasksStack autoStopEcsClusterTasksStack; + private AutoStopEmrServerlessApplicationStack autoStopEmrServerlessApplicationStack; private LoggingStack loggingStack; // These flags are used to control when the stacks are deployed in the SystemTest CDK app. @@ -149,6 +151,9 @@ public void create() { // Auto stop ECS cluster tasks stack autoStopEcsClusterTasksStack = new AutoStopEcsClusterTasksStack(this, "AutoStopEcsClusterTasks", instanceProperties, jars, loggingStack); + // Auto stop EMR Serverless application stack + autoStopEmrServerlessApplicationStack = new AutoStopEmrServerlessApplicationStack(this, "AutoStopEmrServerlessApplication", instanceProperties, jars, loggingStack); + // Stacks for tables ManagedPoliciesStack policiesStack = new ManagedPoliciesStack(this, "Policies", instanceProperties); TableDataStack dataStack = new TableDataStack(this, "TableData", instanceProperties, loggingStack, policiesStack, autoDeleteS3ObjectsStack, jars); @@ -198,6 +203,7 @@ public void create() { topicStack.getTopic(), bulkImportBucketStack, coreStacks, + autoStopEmrServerlessApplicationStack, errorMetrics); // Stack to created EMR studio to be used to access EMR Serverless diff --git a/java/cdk/src/main/java/sleeper/cdk/stack/bulkimport/EmrServerlessBulkImportStack.java b/java/cdk/src/main/java/sleeper/cdk/stack/bulkimport/EmrServerlessBulkImportStack.java index 1c943920677..2362de90cf5 100644 --- a/java/cdk/src/main/java/sleeper/cdk/stack/bulkimport/EmrServerlessBulkImportStack.java +++ b/java/cdk/src/main/java/sleeper/cdk/stack/bulkimport/EmrServerlessBulkImportStack.java @@ -47,6 +47,7 @@ import sleeper.bulkimport.core.configuration.BulkImportPlatform; import sleeper.cdk.jars.BuiltJars; import sleeper.cdk.jars.LambdaCode; +import sleeper.cdk.stack.core.AutoStopEmrServerlessApplicationStack; import sleeper.cdk.stack.core.CoreStacks; import sleeper.cdk.stack.core.LoggingStack.LogGroupRef; import sleeper.cdk.util.Utils; @@ -96,11 +97,12 @@ public EmrServerlessBulkImportStack( Topic errorsTopic, BulkImportBucketStack importBucketStack, CoreStacks coreStacks, + AutoStopEmrServerlessApplicationStack autoStopEmrServerlessApplicationStack, List errorMetrics) { super(scope, id); IBucket jarsBucket = Bucket.fromBucketName(scope, "JarsBucket", instanceProperties.get(JARS_BUCKET)); LambdaCode lambdaCode = jars.lambdaCode(jarsBucket); - createEmrServerlessApplication(instanceProperties); + createEmrServerlessApplication(instanceProperties, autoStopEmrServerlessApplicationStack); IRole emrRole = createEmrServerlessRole( instanceProperties, importBucketStack, coreStacks, jarsBucket); CommonEmrBulkImportHelper commonHelper = new CommonEmrBulkImportHelper(this, @@ -135,7 +137,7 @@ private static void configureJobStarterFunction(InstanceProperties instancePrope .build()); } - public void createEmrServerlessApplication(InstanceProperties instanceProperties) { + public void createEmrServerlessApplication(InstanceProperties instanceProperties, AutoStopEmrServerlessApplicationStack autoStopEmrServerlessApplicationStack) { CfnApplication emrServerlessCluster = CfnApplication.Builder.create(this, "BulkImportEMRServerless") .name(String.join("-", "sleeper", Utils.cleanInstanceId(instanceProperties))) .releaseLabel(instanceProperties.get(BULK_IMPORT_EMR_SERVERLESS_RELEASE)) @@ -155,6 +157,8 @@ public void createEmrServerlessApplication(InstanceProperties instanceProperties emrServerlessCluster.getName()); instanceProperties.set(BULK_IMPORT_EMR_SERVERLESS_APPLICATION_ID, emrServerlessCluster.getAttrApplicationId()); + + autoStopEmrServerlessApplicationStack.addAutoStopEmrServerlessApplication(this, emrServerlessCluster); } private String createSecurityGroup(InstanceProperties instanceProperties) { diff --git a/java/cdk/src/main/java/sleeper/cdk/stack/core/AutoStopEmrServerlessApplicationStack.java b/java/cdk/src/main/java/sleeper/cdk/stack/core/AutoStopEmrServerlessApplicationStack.java new file mode 100644 index 00000000000..8e6c773cfaf --- /dev/null +++ b/java/cdk/src/main/java/sleeper/cdk/stack/core/AutoStopEmrServerlessApplicationStack.java @@ -0,0 +1,110 @@ +/* + * Copyright 2022-2025 Crown Copyright + * + * 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 sleeper.cdk.stack.core; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import software.amazon.awscdk.CustomResource; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.NestedStack; +import software.amazon.awscdk.customresources.Provider; +import software.amazon.awscdk.services.emrserverless.CfnApplication; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.lambda.IFunction; +import software.amazon.awscdk.services.s3.Bucket; +import software.amazon.awscdk.services.s3.IBucket; +import software.constructs.Construct; + +import sleeper.cdk.jars.BuiltJars; +import sleeper.cdk.jars.LambdaCode; +import sleeper.cdk.stack.core.LoggingStack.LogGroupRef; +import sleeper.cdk.util.Utils; +import sleeper.core.deploy.LambdaHandler; +import sleeper.core.properties.instance.InstanceProperties; +import sleeper.core.util.EnvironmentUtils; + +import java.util.List; +import java.util.Map; + +/** + * Stops EMR Serverless application for the CloudFormation stack. + */ +public class AutoStopEmrServerlessApplicationStack extends NestedStack { + + private IFunction lambda; + private Provider provider; + + public AutoStopEmrServerlessApplicationStack(Construct scope, String id, InstanceProperties instanceProperties, BuiltJars jars, + LoggingStack loggingStack) { + super(scope, id); + createLambda(instanceProperties, jars, loggingStack); + } + + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") + private void createLambda(InstanceProperties instanceProperties, BuiltJars jars, LoggingStack loggingStack) { + + // Jars bucket + IBucket jarsBucket = Bucket.fromBucketName(this, "JarsBucket", jars.bucketName()); + LambdaCode lambdaCode = jars.lambdaCode(jarsBucket); + + String functionName = String.join("-", "sleeper", + Utils.cleanInstanceId(instanceProperties), "auto-stop-emr-serverless-application"); + + lambda = lambdaCode.buildFunction(this, LambdaHandler.AUTO_STOP_EMR_SERVERLESS_APPLICATION, "Lambda", builder -> builder + .functionName(functionName) + .memorySize(2048) + .environment(EnvironmentUtils.createDefaultEnvironmentNoConfigBucket(instanceProperties)) + .description("Lambda for auto-stopping EMR Serverless application") + .logGroup(loggingStack.getLogGroup(LogGroupRef.AUTO_STOP_EMR_SERVERLESS_APPLICATION)) + .timeout(Duration.minutes(15))); + + // Grant this function permission to emrserverless actions + lambda.getRole().addToPrincipalPolicy(PolicyStatement.Builder + .create() + .resources(List.of("*")) + .actions(List.of("emr-serverless:ListJobRuns", "emr-serverless:CancelJobRun", "emr-serverless:StopApplication", + "emr-serverless:GetApplication")) + .build()); + + provider = Provider.Builder.create(this, "Provider") + .onEventHandler(lambda) + .logGroup(loggingStack.getLogGroup(LogGroupRef.AUTO_STOP_EMR_SERVERLESS_APPLICATION_PROVIDER)) + .build(); + + Utils.addStackTagIfSet(this, instanceProperties); + + } + + /** + * Add a custom resource to stop EMR serverless applications. + * + * @param scope the stack to add the custom resource to + * @param application the EMR serverless application + */ + public void addAutoStopEmrServerlessApplication(Construct scope, CfnApplication application) { + + String id = application.getNode().getId() + "-Autostop"; + + CustomResource customResource = CustomResource.Builder.create(scope, id) + .resourceType("Custom::AutoStopEmrServerlessApplication") + .properties(Map.of("applicationId", application.getAttrApplicationId())) + .serviceToken(provider.getServiceToken()) + .build(); + + customResource.getNode().addDependency(application); + + } + +} diff --git a/java/cdk/src/main/java/sleeper/cdk/stack/core/LoggingStack.java b/java/cdk/src/main/java/sleeper/cdk/stack/core/LoggingStack.java index e2901e41427..4540b56d412 100644 --- a/java/cdk/src/main/java/sleeper/cdk/stack/core/LoggingStack.java +++ b/java/cdk/src/main/java/sleeper/cdk/stack/core/LoggingStack.java @@ -84,6 +84,8 @@ public enum LogGroupRef { AUTO_DELETE_S3_OBJECTS_PROVIDER("auto-delete-s3-objects-provider"), AUTO_STOP_ECS_CLUSTER_TASKS("auto-stop-ecs-cluster-tasks"), AUTO_STOP_ECS_CLUSTER_TASKS_PROVIDER("auto-stop-ecs-cluster-tasks-provider"), + AUTO_STOP_EMR_SERVERLESS_APPLICATION("auto-stop-emr-serverless-application"), + AUTO_STOP_EMR_SERVERLESS_APPLICATION_PROVIDER("auto-stop-emr-serverless-application-provider"), BULK_EXPORT("bulk-export"), BULK_EXPORT_TASKS("FargateBulkExportTasks"), BULK_EXPORT_TASKS_CREATOR("bulk-export-task-creator"), diff --git a/java/core/src/main/java/sleeper/core/deploy/LambdaHandler.java b/java/core/src/main/java/sleeper/core/deploy/LambdaHandler.java index 6c28fc283d3..623c1d01aac 100644 --- a/java/core/src/main/java/sleeper/core/deploy/LambdaHandler.java +++ b/java/core/src/main/java/sleeper/core/deploy/LambdaHandler.java @@ -130,6 +130,10 @@ public class LambdaHandler { .jar(LambdaJar.CUSTOM_RESOURCES) .handler("sleeper.cdk.custom.AutoStopEcsClusterTasksLambda::handleEvent") .core().add(); + public static final LambdaHandler AUTO_STOP_EMR_SERVERLESS_APPLICATION = builder() + .jar(LambdaJar.CUSTOM_RESOURCES) + .handler("sleeper.cdk.custom.AutoStopEmrServerlessApplicationLambda::handleEvent") + .core().add(); public static final LambdaHandler PROPERTIES_WRITER = builder() .jar(LambdaJar.CUSTOM_RESOURCES) .handler("sleeper.cdk.custom.PropertiesWriterLambda::handleEvent")