= Quartz Clustering with JDBC-JobStore

== JDBC-JobStore DB 초기화
quartz db schema (배포된 sdl-appendix 내 참조) 를 사용하는 DBMS에 맞는 SQL을 실행해 JDBC-JobStore를 사용하기 위한 Table을 생성한다.

CAUTION: Tibero DB는 미지원

== application.properties
properties 파일 내용 중 Quartz JDBC-JobStore 관련 설정이다.
[source, properties]
----
# Spring Quartz 설정
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=always

spring.quartz.datasource.driver-class-name=org.postgresql.Driver
spring.quartz.datasource.jdbcUrl=jdbc:postgresql://10.40.87.189:5445/SDL
spring.quartz.datasource.username=sdl5
spring.quartz.datasource.password=dlatl#123

spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate <1>
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=15000
spring.quartz.properties.org.quartz.scheduler.instanceName=sdl-prod <2>
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO <3>
spring.quartz.properties.org.quartz.scheduler.rmi.export=false
spring.quartz.properties.org.quartz.scheduler.rmi.proxy=false
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=10 <4>
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
spring.quartz.properties.org.quartz.scheduler-name=QuartzScheduler
----

<1> DB 에 따라 Delegate 를 변경한다.
+
org.quartz.impl.jdbcjobstore.oracle.OracleDelegate, +
org.quartz.impl.jdbcjobstore.MSSQLDelegate, +
org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
<2> Quartz Scheduler 를 구별하는 Property 값으로 Clustering 할 서버에서는 같은 값을 세팅한다.
<3> Unique 한 값이어야 하며 AUTO 일 경우 자동 생성된다.
<4> Job 을 실행하기 위한 Thread 수

기타 자세한 Property 값은 http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigJDBCJobStoreClustering.html[Quartz 메뉴얼, window=_blank]을 참고 한다.

== QuartzClusteringConfig

QuartzClusteringConfig 파일은 Spring 에 등록된 CronTriggerFactoryBean 을 실행 하도록 SchedulerFactoryBean이 정의되어 있다.

[source, java]
----
@Bean
public SchedulerFactoryBean setSchedulerFactoryBean(ApplicationContext applicationContext) {
    SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

    AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory();
    jobFactory.setApplicationContext(applicationContext);

    schedulerFactoryBean.setJobFactory(jobFactory); <1>
    schedulerFactoryBean.setDataSource(quartzDataSource()); <2>
    schedulerFactoryBean.setApplicationContext(applicationContext);
    schedulerFactoryBean.setTransactionManager(quartzTransactionManager()); <3>
    schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true);
    schedulerFactoryBean.setStartupDelay(60);
    schedulerFactoryBean.setOverwriteExistingJobs(true);

    Properties properties = new Properties();
    properties.putAll(quartzProperties.getProperties()); <4>
    schedulerFactoryBean.setQuartzProperties(properties);

    schedulerFactoryBean.setTriggers(
            knoxSyncBatchConfig.knoxApprovalSyncTrigger().getObject(),
            userBatchConfig.batchUserLongTermCheckTrigger().getObject(),
            userBatchConfig.batchUserAuthExpiredTrigger().getObject(),
            sysUseLogBatchConfig.batchSysUseLogTrigger().getObject(),
            sysUseLogBatchConfig.batchMenuUseHistoryTrigger().getObject()
    ); <5>

    return schedulerFactoryBean;
}

@Bean
@ConfigurationProperties(prefix = "spring.quartz.datasource")
@QuartzDataSource <2>
public DataSource quartzDataSource() {
    return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

@Bean
@QuartzTransactionManager <3>
public DataSourceTransactionManager quartzTransactionManager() {
    return new DataSourceTransactionManager(quartzDataSource());
}
----

<1> Spring Bean을 AutoWiring
<2> Job 정보를 조회, 저회 저장할 때 사용하는 Datasource (resources-dev, resources-prod/application.properties 참조)
<3> Quartz 전용 TransactionManager
<4> Quartz 설정 파일 세팅
<5> SchedulerFactoryBean 에서 실행 할 Trigger

== JobDetail 및 Trigger Bean 등록
아래는 KnoxSyncBatchConfig에 수행할 Job 클래스를 설정한 JobDetail Bean과 Trigger Bean을 등록한 예이다.

.KnoxSyncBatchConfig.java
[source, java]
----
@Value("${knox.approval.sync.cron}")
private String knoxApprovalSyncCron;

@Bean
public JobDetail knoxApprovalSyncJobDetail() {
    return JobBuilder
            .newJob(KnoxSyncBatchExecutor.class) <1>
            .withIdentity("knoxApprovalSyncJob")
            .withDescription("Knox Approval Sync Batch")
            .storeDurably(true)
            .build();
}

@Bean
public CronTriggerFactoryBean knoxApprovalSyncTrigger() {
    CronTriggerFactoryBean approvalTrigger = new CronTriggerFactoryBean();
    approvalTrigger.setJobDetail(knoxApprovalSyncJobDetail());
    approvalTrigger.setCronExpression(knoxApprovalSyncCron);
    return approvalTrigger;
}
----

<1> 배치잡 실행시 QuartzJobBean을 상속 받은 KnoxSyncBatchExecutor의  executeInternal 메소드가 실행된다.

== Job 구현
QuartzJobBean은 Spring Bean을 DI Job의 구현체에서 사용할 수 있도록 Job을 구현한 추상 클래스이며,
QuartzJobBean을 상속 받아 executeInternal 메소드를 오버라이드하여 실행할 배치 Service의 메소드를 호출한다.

.KnoxSyncBatchExecutor.java
[source,java]
----
@Component
@Log4j2
public class KnoxSyncBatchExecutor extends QuartzJobBean {

    @Value("${node-id}")
    private String nodeId;

    @Autowired
    private KnoxApprovalSyncService knoxApprovalSyncService;

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        log.info(new StringBuilder()
                .append("nodeId : ").append(nodeId).append(", ")
                .append("jobName : ").append(jobDetail.getKey().getName()).append(", ")
                .append("description : ").append(jobDetail.getDescription())
        );
        knoxApprovalSyncService.synchronizeKnox();
    }
}
----

CAUTION: UserBatchExecutor의 경우 JobKey(JobBuilder.withIdentity 설정 값)를 이용해 분기 처리 하고 있으므로 Job 등록시 Name 명 지정에 주의하도록 한다.

.UserBatchConfig.java
[source, java]
----
/**
 * 장기 미사용자 관리 Job
 */
@Bean
public JobDetail batchUserLongTermCheckJob() {
     return JobBuilder
            .newJob(UserBatchExecutor.class)
            .withIdentity("batchUserLongTermCheck")
            .withDescription("User LongTerm Check Batch")
            .storeDurably(true)
            .build();
}

/**
 * 만료 권한 삭제 Job
 */
@Bean
public JobDetail batchUserAuthExpiredJob() {
    return JobBuilder
            .newJob(UserBatchExecutor.class)
            .withIdentity("batchUserAuthExpired")
            .withDescription("User Auth Expired Batch")
            .storeDurably(true)
            .build();
}
----

.UserBatchExecutor.java
[source, java]
----
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) {
    JobDetail jobDetail = jobExecutionContext.getJobDetail();
    String jobName = jobDetail.getKey().getName();
    log.info(new StringBuilder()
            .append("nodeId : ").append(nodeId).append(", ")
            .append("jobName : ").append(jobName).append(", ")
            .append("description : ").append(jobDetail.getDescription())
    );
    switch (jobName) {
        case "batchUserLongTermCheck" -> userBatchService.execUserLongTermCheck();
        case "batchUserAuthExpired" -> userBatchService.execUserAuthValidCheck();
        case "batchUserAuthExpiredMailing" -> {
            try {
                userBatchService.execUserAuthValidAlarmMail();
            } catch (IOException | MessagingException e) {
                log.error(e.getMessage(), e);
            }
        }
        default -> {
        }
    }
}
----
