JobsAutoConfiguration.java

package edu.ucsb.cs156.jobs.config;

import edu.ucsb.cs156.jobs.controllers.JobsController;
import edu.ucsb.cs156.jobs.repositories.JobLogRepository;
import edu.ucsb.cs156.jobs.services.JobContextFactory;
import edu.ucsb.cs156.jobs.services.JobRateLimit;
import edu.ucsb.cs156.jobs.services.JobService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;

/**
 * Auto-configuration for the jobs system; activates when the library is on the classpath.
 *
 * <p>{@code @AutoConfigurationPackage} adds {@code edu.ucsb.cs156.jobs} to the app's
 * auto-configuration packages, so Spring Boot's default entity and Spring Data repository scanning
 * picks up {@code Job} and {@code JobsRepository} *in addition to* the app's own classes. (Using
 * {@code @EntityScan}/{@code @EnableJpaRepositories} here instead would disable that default
 * scanning and break the consuming app's own entities and repositories.)
 *
 * <p>Every bean is {@code @ConditionalOnMissingBean} so an app can override any piece.
 *
 * <p>The {@code jobsExecutor} defaults to a single thread with an unbounded FIFO queue: jobs
 * execute strictly one at a time, in submission order, which prevents two concurrent jobs from
 * racing each other (e.g. double-launching a sync inserting the same rows). Known limitation: the
 * queue is in-memory, so jobs still queued when the app shuts down are lost and their Job rows
 * remain in "running" status. Apps that want concurrency set {@code app.jobs.core-pool-size} /
 * {@code app.jobs.max-pool-size} / {@code app.jobs.queue-capacity}.
 */
@AutoConfiguration
@AutoConfigurationPackage(basePackages = "edu.ucsb.cs156.jobs")
@EnableAsync
@EnableScheduling
public class JobsAutoConfiguration {

  /*
   * Bean method names are prefixed with libJobs so the resulting bean NAMES
   * cannot collide with beans the consuming apps define themselves: every app
   * keeps a launch controller class named JobsController (bean name
   * "jobsController"), and @ConditionalOnMissingBean matches by type, not
   * name, so a name collision would fail startup with
   * BeanDefinitionOverrideException.
   */

  @Bean
  @ConditionalOnMissingBean
  public JobContextFactory libJobsContextFactory(
      JobLogRepository jobLogRepository, PlatformTransactionManager transactionManager) {
    return new JobContextFactory(jobLogRepository, transactionManager);
  }

  @Bean
  @ConditionalOnMissingBean
  public JobService libJobsService() {
    return new JobService();
  }

  @Bean
  @ConditionalOnMissingBean
  public JobsController libJobsController() {
    return new JobsController();
  }

  @Bean
  @ConditionalOnMissingBean
  public JobRateLimit libJobsRateLimit(
      @Value("${app.jobs.rate-limit-ms:200}") String rateLimitMsString) {
    return new JobRateLimit(rateLimitMsString);
  }

  @Bean(name = "jobsExecutor")
  @ConditionalOnMissingBean(name = "jobsExecutor")
  public AsyncTaskExecutor jobsExecutor(
      @Value("${app.jobs.core-pool-size:1}") int corePoolSize,
      @Value("${app.jobs.max-pool-size:1}") int maxPoolSize,
      @Value("${app.jobs.queue-capacity:2147483647}") int queueCapacity) {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(corePoolSize);
    executor.setMaxPoolSize(maxPoolSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setThreadNamePrefix("jobsExecutor-");
    executor.initialize();
    // propagate the launching admin's security context onto the job thread
    return new DelegatingSecurityContextAsyncTaskExecutor(executor);
  }
}