Quartz Schedulerでワンタイムジョブ発行



## 環境

アプリケーションとして形成して実行するためにSpringBootを使用しています。
サンプルジョブの主処理としてログ出力を行っていますが、ログ出力の方式は主旨とは関係ないので割愛させていただきます。

- Quartz:`2.2.1`
- SpringBoot:`1.3.5.RELEASE`


## 動作

1. WEBサーバの形式でアプリケーションを実行します。
1. `/api/job/onetime?value=foo`にPOSTリクエストすると、ジョブを登録するだけですぐさまレスポンス200が返ってきます。
1. リクエストしてから1分後、ジョブが実行されて、サーバログに「`Executiong HelloJob ! : [valueの値"foo"] | at [実行日時]`」が出力されます。

## コード

### ジョブの定義

この記述は、アプリによってもそんなにブレないと思ってます。
ポイントは次の通りです。

- org.quartz.Jobインタフェースを実装します
- Overrideしたexecute()が主処理になります
- ジョブパラメータはJobDataMapを経由して受け取ります

```java:HelloJob.java
public class HelloJob implements Job {

    private final Logger log = LoggerFactory.getLogger(HelloJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap data = context.getJobDetail().getJobDataMap();
        String value = data.getString("value");

        // ジョブの主処理
        log.info("Executiong HelloJob ! : " + value + " | at " + new Date());
    }

}
```

### スケジューラの定義・生成

これはアプリによって記述する場所やタイミングがかなり違ってきそうです。
今回は次のようにしています。

- アプリ全体で唯一にするSchedulerをBean化して、様々な場所にDIできるようにします
- Schedulerをアプリ（サーバ）起動時点で開始してしまいます（※本番システムでは、おそらくScheduler.shutdown()や再起動処理などを定義した別のクラスを用意したほうがよいでしょう）

```java:QuartzConfiguration.java
@Configuration
public class QuartzConfiguration {

    @Bean
    public Scheduler scheduler() throws SchedulerException {
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();

        sched.start();

        return sched;
    }

}
```

### HelloJobを実行するためのサンプルアプリ（RESTコントローラ）

今回はコントローラでジョブ・トリガーの登録を行っていますが、実際はサービス層に移動した方がよいでしょう。

```java:JobResource.java
@RestController
@RequestMapping("/api/job")
@Api(tags = {"ジョブテスト用API"})
public class JobResource {

    private final Logger log = LoggerFactory.getLogger(JobResource.class);

    @Autowired
    private Scheduler scheduler; // 説明：スケジューラのインスタンスをDIで取得

    /**
     * ワンタイムジョブの登録
     *
     * @param value
     * @return
     * @throws URISyntaxException
     */
    @ApiOperation(value = "ワンタイムジョブの登録", notes = "ワンタイムジョブを登録します。ジョブの登録処理だけですぐにレスポンスが返ってきます。")
    @RequestMapping(value = "/onetime",
        method = RequestMethod.POST,
        produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public ResponseEntity<String> addOnetimeJob(
        @RequestParam(value = "value", defaultValue = "lol") String value
    ) throws URISyntaxException, SchedulerException {
        log.debug("REST request to exec one time job with param : " + value);

        // ここから参考：http://www.quartz-scheduler.org/documentation/quartz-2.x/examples/Example1.html
        
        // [サーバ起動時に実施済み]SchedulerFactory sf = new StdSchedulerFactory();
        // [DIで対応]Scheduler sched = schedulerFactory.getScheduler();

        JobDetail job = newJob(HelloJob.class)
            .withIdentity("job1", "group1")
            .build();
        job.getJobDataMap().put("value", value);

        Date runTime = evenMinuteDate(new Date()); // 説明：1分後の時間をrunTimeにする

        Trigger trigger = newTrigger()
            .withIdentity("trigger1", "group1")
            .startAt(runTime)
            .build();

        scheduler.scheduleJob(job, trigger);
        // [サーバ起動時に実施済み]sched.start();

        // ここまで参考

        return ResponseEntity.ok().body("one time batch registered! : " + value);
    }
```


## 参考

http://www.quartz-scheduler.org/documentation/quartz-2.x/examples/Example1.html


## おまけ

Quartzは、30秒ごとに登録されているトリガーの有無を確認し、トリガーがあれば発火するか確認⇒ジョブ実行になるようです。

つまりジョブの実行時間も30秒単位。

それを司っているのは次のQuartzSchedulerThreadクラスのようです。

```java:QuartzSchedulerThread.java
...

    @Override
    public void run() {
        boolean lastAcquireFailed = false;

        while (!halted.get()) {

...

                    try {
                        triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
                        lastAcquireFailed = false;
                        if (log.isDebugEnabled()) 
                            log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
                    } catch (JobPersistenceException jpe) {
                            
...

                    }

                    if (triggers != null && !triggers.isEmpty()) { // 説明：トリガーが存在すれば処理開始

...


                        for (int i = 0; i < bndles.size(); i++) {

...


                            JobRunShell shell = null;
                            try {
                                shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
                                shell.initialize(qs);
                            } catch (SchedulerException se) {
                                qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                                continue;
                            }

                            if (qsRsrcs.getThreadPool().runInThread(shell) == false) { // 説明：JobRunShell.run()が、別スレッド（QuartzSchedulerResources.ThreadPoolのいずれかのスレッド）によって実行される
                                // this case should never happen, as it is indicative of the
                                // scheduler being shutdown or a bug in the thread pool or
                                // a thread pool being used concurrently - which the docs
                                // say not to do...
                                getLog().error("ThreadPool.runInThread() return false!");
                                qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                            }

...
                            
```

## おまけ続き

サーバを起動していると、URLにリクエストが発生するたびにトリガーとジョブが登録されます。

おまけの動作を見ると、スケジューラに登録されたトリガーは削除を行っているように見えますが、ジョブのインスタンスはちゃんとクリアされているのでしょうか？

確認しました。

### トリガーとジョブが登録されているとき

![quartz_1.png](https://qiita-image-store.s3.amazonaws.com/0/68978/f9a95bf6-6bd8-6e46-40f7-b453fbffdb34.png)

### トリガーが完了した後

![quartz_2.png](https://qiita-image-store.s3.amazonaws.com/0/68978/6e1ab965-69d9-3618-25f0-392fd860b29f.png)

ジョブインスタンスもちゃんと取り除かれているようです。安心しました。あとはきっとGCがうまいことやってくれるでしょう。

## 別のおまけ

最初は、ワンタイムジョブの登録と併せて、ジョブで実行される内容をアプリから同期的に実行できないかと考えていました。
しかし、次の内容を読んで、ジョブはジョブとして、アプリ機能はアプリ機能として実装しようと考えなおしました。

[java - Run Quartz job synchronous - Stack Overflow](http://stackoverflow.com/questions/33492860/run-quartz-job-synchronous)