[JAVA] Quartz 스케줄러 만들기 (3) - Job 강제 종료

2020. 11. 11. 16:44Back-end/JAVA

서론

  지난 포스트에서는 Listener를 적용해보았다. 어떤 순서로 돌아가는지, 오류가 발생했을 때는 어떻게 되는지 궁금해서 수행중인 Job을 강제로 중단시키거나 오류를 발생시키고 싶어졌다.
  
  그러나, Quartz에서 스케줄을 종료할 때 Scheduler.shutdown() 함수를 사용하는데, 이 함수는 Trigger를 종료시키는 명령으로써 이미 수행중인 Job에는 영향을 주지않는다. 따라서, 이미 진행되고 있는 Job은 멈출 수 없으므로 별도의 기능을 구현해야한다.
  
  그래서 이번 포스트에서는 Job을 강제로 중단시키기는 방법에 대해서 다루어보려고한다.

 

 

 

개발환경

    - jdk-11.0.5
    - quartz-2.3.2

<!-- Scheduler -->
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.2</version>
</dependency>
<!-- Scheduler -->
<!-- Logging -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<!-- Logging -->

 

 

 

소스코드
1. MainJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.dochi.quartz.interrupt;
 
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
 
 
public class MainJob implements InterruptableJob {
    
    private Thread currentThread = null;
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 현재 Thread 저장
        this.currentThread = Thread.currentThread();
        
        String jobName = context.getJobDetail().getKey().getName();
        
        System.out.println(String.format("[%s][%s] Running..."this.getClass().getName(), jobName));
        try {
            // 강제로 종료를 지연시키기
            Thread.sleep(5*1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("[%s][%s] Finish!!!"this.getClass().getName(), jobName));
    }
 
    @Override
    public void interrupt() throws UnableToInterruptJobException {
        // interrupt 설정
        //   - 강제종료
        ifthis.currentThread != null ) {
            this.currentThread.interrupt();
        }
    }
}
 
cs

 

 

 

2. TriggerLogListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.dochi.quartz.interrupt;
 
import java.util.Date;
 
import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.Trigger.CompletedExecutionInstruction;
import org.quartz.TriggerListener;
 
public class TriggerLogListener implements TriggerListener {
 
    @Override
    public String getName() {
        return this.getClass().getName();
    }
 
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][triggerFired]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][vetoJobExecution]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
        return false;
    }
 
    @Override
    public void triggerMisfired(Trigger trigger) {
        System.out.println(String.format("[%s][%s][triggerMisfired]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            CompletedExecutionInstruction triggerInstructionCode) {
        System.out.println(String.format("[%s][%s][triggerComplete]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
}
 
cs

 

 

 

3. JobLogListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.dochi.quartz.interrupt;
 
import java.util.Date;
 
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
 
public class JobLogListener implements JobListener {
    
    @Override
    public String getName() {
        return this.getClass().getName();
    }
 
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][triggerComplete]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][jobExecutionVetoed]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println(String.format("[%s][%s][jobWasExecuted]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
}
 
cs

 

 

 

4. JobLauncher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.dochi.quartz.interrupt;
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Set;
 
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.UnableToInterruptJobException;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
 
public class JobLauncher {
    
    // 상수 설정
    //   - Prefix 설정
    public static final String PREFIX_STEP_JOB_NAME = "job_";
    public static final String PREFIX_STEP_TRIGGER_NAME = "trigger_";
    public static final String PREFIX_NEXT_STEP_JOB_NAME = "step_job_";
    public static final String PREFIX_NEXT_STEP_TRIGGER_NAME = "step_trigger_";
    
    //   - DateFormat 설정
    public static final SimpleDateFormat TIMESTAMP_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    public static final SimpleDateFormat DATETIME_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
    // Scheduler 객체 생성
    private static SchedulerFactory factory = null;
    private static Scheduler scheduler = null;
    
    // Main 함수
    public static void main(String[] args) throws SchedulerException {
        // Scheduler 실행
        start();
        
        // Schedule 등록
        addSchedule("MainJob");
        addSchedule("MainJob2");
 
        try {
            System.out.println("아무키나 입력하면 종료됩니다...");
            System.in.read();
 
            // Scheduler 롱료
            stop();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // Scheduler 실행 함수
    public static void start() throws SchedulerException {
        // Scheduler 객체 정의
        factory = new StdSchedulerFactory();
        scheduler = factory.getScheduler();
        
        // Listener 설정
        scheduler.getListenerManager().addJobListener(new JobLogListener());
        scheduler.getListenerManager().addTriggerListener(new TriggerLogListener());
        
        // Scheduler 실행
        scheduler.start();
    }
    
    // Scheduler 종료 함수
    public static void stop() throws SchedulerException {
        try {
            System.out.println("스케줄러가 종료됩니다...");
            
            // Job Key 목록
            Set<JobKey> allJobKeys = scheduler.getJobKeys(GroupMatcher.anyGroup());
            
            // Job 강제 중단
            allJobKeys.forEach((jobKey)->{
                try {
                    scheduler.interrupt(jobKey);
                } catch (UnableToInterruptJobException e) {
                    e.printStackTrace();
                }
            });
            
            // Scheduler 중단
            //   - true : 모든 Job이  완료될 때까지 대기 후 종료
            //   - false: 즉시 종료
            scheduler.shutdown(true);
 
            System.out.println("스케줄러가 종료되었습니다.");
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
    
    // Schedule 등록 함수
    public static void addSchedule(String name) throws SchedulerException {
        // JobDetail 설정
        JobDetail jobDetail = JobBuilder.newJob(MainJob.class)
                                .withIdentity(PREFIX_STEP_JOB_NAME+name)
                                .build();
        
        // Simple Schedule 생성
        //   - 3초마다 실행, 최대 5회
        SimpleScheduleBuilder schedule = SimpleScheduleBuilder.simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .withRepeatCount(5);
        
        // Trigger 설정
        Trigger trigger = TriggerBuilder.newTrigger()
                              .withIdentity(PREFIX_STEP_TRIGGER_NAME+name)
                              .withSchedule(schedule)
                              .forJob(jobDetail)
                              .build();
        
        // Schedule 등록
        scheduler.scheduleJob(jobDetail, trigger);
    }
}
 
cs

 

 

 

실행결과

    - JobLauncher를 실행시키고, Console에서 아무키나 입력하면 Scheduler가 중단됨.

    - 이미 Job이 실행중이라면 위와같이 InterInterruptedException이 발생함.

 

 

 

마치며

  지금까지 Job을 강제로 중단하는 방법에 대해서 다루어보았다. Listener를 좀 더 알아보기위해서 만들었는데, 나중에 Crawler를 적용했을 때 요청이 오래걸리거나 잘못요청했을 때 중단하는 기능으로 활용하면 매우 좋을것 같다. 다음 포스트에서는 Listener를 활용한 예제를 다루어보도록 하겠다.