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..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,30 +2,62 @@ 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) { - SpringApplication.run(SchedulerApplication.class, args); + System.exit(SpringApplication.exit(SpringApplication.run(SchedulerApplication.class, 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:}