본문 바로가기
Back-end/JAVA

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

by 허도치 2020. 11. 11.
서론

  지난 포스트에서는 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를 활용한 예제를 다루어보도록 하겠다.

 

댓글