From 3f1173b3bd50dc81641685e17e7b0404cd20dd59 Mon Sep 17 00:00:00 2001 From: mkuchenbecker Date: Wed, 27 May 2026 14:35:32 -0700 Subject: [PATCH 1/2] refactor(scheduler): exit JVM with Spring exit code for clean shutdown Wrap SpringApplication.run in SpringApplication.exit + System.exit so the context is closed (PreDestroy hooks, JPA pool drain, etc.) and the JVM returns a deterministic exit code after the CommandLineRunner completes. Matches the standard Spring Boot batch-style entry point and is what k8s cron jobs need. Analyzer left unchanged for now; will be applied separately. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../openhouse/optimizer/scheduler/SchedulerApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java b/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java index a22524894..d14c3da78 100644 --- a/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java +++ b/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java @@ -16,7 +16,7 @@ public class SchedulerApplication { public static void main(String[] args) { - SpringApplication.run(SchedulerApplication.class, args); + System.exit(SpringApplication.exit(SpringApplication.run(SchedulerApplication.class, args))); } /** From 10a5ddaa36877a38281716d6c85e9c7ba95667c5 Mon Sep 17 00:00:00 2001 From: mkuchenbecker Date: Wed, 27 May 2026 14:52:21 -0700 Subject: [PATCH 2/2] refactor(scheduler): adopt full ExitCodeGenerator pattern from review Apply the reviewer's full suggestion (#534): SchedulerApplication implements CommandLineRunner + ExitCodeGenerator directly, wraps the work in try/catch, tracks exitCode, and reports it via getExitCode(). SpringApplication.exit propagates that to System.exit so the k8s CronJob pod status reflects batch outcome. Removes the prior @Bean CommandLineRunner. Also adds spring.main.banner-mode=off per the suggestion. Verified with the boot jar: - empty H2 schema (runner throws) -> caught, JPA pool drained, exit 1 - schema preloaded, no PENDING ops -> exit 0 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scheduler/SchedulerApplication.java | 48 +++++++++++++++---- .../src/main/resources/application.properties | 1 + 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java b/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java index d14c3da78..d83db7524 100644 --- a/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java +++ b/apps/optimizer/schedulerapp/src/main/java/com/linkedin/openhouse/optimizer/scheduler/SchedulerApplication.java @@ -2,18 +2,38 @@ import com.linkedin.openhouse.optimizer.model.OperationTypeDto; import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -/** Entry point for the Optimizer Scheduler application. */ +/** + * Entry point for the Optimizer Scheduler application. + * + *

Spring Batch–style: implements {@link CommandLineRunner} so the work runs after context + * startup, and {@link ExitCodeGenerator} so the JVM exit code reflects batch outcome. {@code + * SpringApplication.exit(...)} closes the context (triggers {@code @PreDestroy} hooks, drains the + * JPA pool, etc.) so the k8s CronJob pod terminates cleanly with a status reflecting reality. + */ +@Slf4j @SpringBootApplication @EntityScan(basePackages = "com.linkedin.openhouse.optimizer.db") @EnableJpaRepositories(basePackages = "com.linkedin.openhouse.optimizer.repository") -public class SchedulerApplication { +public class SchedulerApplication implements CommandLineRunner, ExitCodeGenerator { + + private final SchedulerRunner runner; + private final Map binPackers; + private int exitCode = 0; + + @Autowired + public SchedulerApplication(SchedulerRunner runner, Map binPackers) { + this.runner = runner; + this.binPackers = binPackers; + } public static void main(String[] args) { System.exit(SpringApplication.exit(SpringApplication.run(SchedulerApplication.class, args))); @@ -21,11 +41,23 @@ public static void main(String[] args) { /** * Runs the scheduler once per registered {@link BinPacker} per process invocation. Each call is - * scoped to one operation type. + * scoped to one operation type. Any thrown exception is logged and surfaces as a non-zero exit + * code via {@link #getExitCode()} after the context is shut down cleanly. */ - @Bean - public CommandLineRunner run( - SchedulerRunner runner, Map binPackers) { - return args -> binPackers.keySet().forEach(runner::schedule); + @Override + public void run(String... args) { + try { + log.info("Scheduler starting; operation types: {}", binPackers.keySet()); + binPackers.keySet().forEach(runner::schedule); + log.info("Scheduler completed successfully"); + } catch (Exception e) { + log.error("Scheduler failed", e); + exitCode = 1; + } + } + + @Override + public int getExitCode() { + return exitCode; } } diff --git a/apps/optimizer/schedulerapp/src/main/resources/application.properties b/apps/optimizer/schedulerapp/src/main/resources/application.properties index 00b8c7a5e..5184cf1bc 100644 --- a/apps/optimizer/schedulerapp/src/main/resources/application.properties +++ b/apps/optimizer/schedulerapp/src/main/resources/application.properties @@ -1,5 +1,6 @@ spring.application.name=openhouse-optimizer-scheduler spring.main.web-application-type=none +spring.main.banner-mode=off spring.datasource.url=${OPTIMIZER_DB_URL:jdbc:h2:mem:schedulerdb;DB_CLOSE_DELAY=-1;MODE=MySQL} spring.datasource.username=${OPTIMIZER_DB_USER:sa} spring.datasource.password=${OPTIMIZER_DB_PASSWORD:}