Quartz JDBC storeを使ってJOB管理(クラスタリングもやってみました）


# はじめに

12/26追記
quartz APIについてまとめてみました。合わせてどうぞ。
http://qiita.com/uzresk/items/21cb62e7ee38c92a7aca

ジョブのスケジュールをプログラムから動的に登録し、それが動くような仕組みってないのかな。と思って見つけたのがquartzでした。
quartzの良さはcronを登録するセマフォにDBが利用できること、またそれを利用したクラスタリングが組めることです。
つまりジョブの実行サーバをスケールアウトすることができるのが最大の魅力です。

他にもcron4jなどありますが、こちらはファイルしかできなかったので落選しました。
ただどちらもAPIはすごくシンプルなので使いやすいFrameworkだと思います。

web上を漁って見たのですがDBを利用している情報があまりないので試してみました。

流行ってないのかな・・・。

# データベースを用意

今回はPostgreSQLを利用しました。

1.早速DBを作ります。

```
postgres=# create database quartz;
CREATE DATABASE
```

2.作ったDBにつないでユーザを作ります。

```
quartz=# CREATE USER job WITH PASSWORD 'job';
CREATE ROLE
quartz=# CREATE SCHEMA job AUTHORIZATION job;
CREATE SCHEMA
```

3.スクリプトを流します。添付されている/docs/dbtables/tables_postgres.sqlを流します。

```
quartz=# \c - job
データベース "quartz" にユーザ"job"として接続しました。


quartz=> \i tables_postgres.sql
psql:tables_postgres.sql:6: ERROR:  table "qrtz_fired_triggers" does not exist
psql:tables_postgres.sql:7: ERROR:  table "qrtz_paused_trigger_grps" does not
exist
psql:tables_postgres.sql:8: ERROR:  table "qrtz_scheduler_state" does not exist
psql:tables_postgres.sql:9: ERROR:  table "qrtz_locks" does not exist
psql:tables_postgres.sql:10: ERROR:  table "qrtz_simple_triggers" does not exist
psql:tables_postgres.sql:11: ERROR:  table "qrtz_cron_triggers" does not exist
psql:tables_postgres.sql:12: ERROR:  table "qrtz_simprop_triggers" does not
exist
psql:tables_postgres.sql:13: ERROR:  table "qrtz_blob_triggers" does not exist
psql:tables_postgres.sql:14: ERROR:  table "qrtz_triggers" does not exist
psql:tables_postgres.sql:15: ERROR:  table "qrtz_job_details" does not exist
psql:tables_postgres.sql:16: ERROR:  table "qrtz_calendars" does not exist
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
psql:tables_postgres.sql:187: WARNING:  there is no transaction in progress
```

4.テーブルができていることを確認します。

```
quartz=> \d
                   リレーションの一覧
 スキーマ |           名前           |    型    | 所有者
----------+--------------------------+----------+--------
 job      | qrtz_blob_triggers       | テーブル | job
 job      | qrtz_calendars           | テーブル | job
 job      | qrtz_cron_triggers       | テーブル | job
 job      | qrtz_fired_triggers      | テーブル | job
 job      | qrtz_job_details         | テーブル | job
 job      | qrtz_locks               | テーブル | job
 job      | qrtz_paused_trigger_grps | テーブル | job
 job      | qrtz_scheduler_state     | テーブル | job
 job      | qrtz_simple_triggers     | テーブル | job
 job      | qrtz_simprop_triggers    | テーブル | job
 job      | qrtz_triggers            | テーブル | job
(11 行)
```

# JOBを登録

1.まずは動かしたいJOBを用意
今回はただ日付を出力するJOBを用意しました。JOBをImplementsしてあげれば良いだけですのでなんでも書けます。

```HelloJob.java
package quartz.jdbc.simple;

import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {

    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("hello " + new Date() + " :" + context.getJobDetail());

    }
}
```

2.JOBを登録するためのクラスを用意します。

1で用意したHelloJobを5分間隔で動かしてみようと思います。

```RegistHelloJob.java
package quartz.jdbc.simple;

import java.util.TimeZone;

import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class RegistHelloJob {

    public static void main(String[] args) throws Exception {

        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("helloJob", "jobGroup1")
                .storeDurably()
                .build();

        CronTrigger trigger = TriggerBuilder
                .newTrigger()
                .withIdentity("cronTrigger1", "triggerGroup1")
                .withSchedule(
                        CronScheduleBuilder.cronSchedule("*/5 * * * * ?")
                                .inTimeZone(TimeZone.getTimeZone("Asia/Tokyo"))).startNow().build();

        scheduler.scheduleJob(job, trigger);

        System.out.println("JOBの登録が完了しました。");
    }
}
```

3.DBに接続するためのプロパティファイルを編集します。

ベースはexamples13のものを持ってくると良いでしょう。名前は何でも構いません。
ポイントはPostgreSQLDelegateを使えるようにコメントアウトを解除してあげることと、DBの接続設定を変更してあげることです。

```quartz.properties

#============================================================================
# Configure Main Scheduler Properties  
#============================================================================

org.quartz.scheduler.instanceName: TestScheduler
org.quartz.scheduler.instanceId: instance_one

org.quartz.scheduler.skipUpdateCheck: true

#============================================================================
# Configure ThreadPool  
#============================================================================

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 5
org.quartz.threadPool.threadPriority: 5

#============================================================================
# Configure JobStore  
#============================================================================

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true

#============================================================================
# Other Example Delegates
#============================================================================
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DB2v6Delegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DB2v7Delegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DriverDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.HSQLDBDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.MSSQLDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PointbaseDelegate
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.WebLogicDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.WebLogicOracleDelegate

#============================================================================
# Configure Datasources  
#============================================================================

org.quartz.dataSource.myDS.driver: org.postgresql.Driver
org.quartz.dataSource.myDS.URL: jdbc:postgresql://25.32.65.23:5432/quartz
org.quartz.dataSource.myDS.user: job
org.quartz.dataSource.myDS.password: job
org.quartz.dataSource.myDS.maxConnections: 5
org.quartz.dataSource.myDS.validationQuery: select 0

#============================================================================
# Configure Plugins 
#============================================================================

#org.quartz.plugin.shutdownHook.class: org.quartz.plugins.management.ShutdownHookPlugin
#org.quartz.plugin.shutdownHook.cleanShutdown: true


#org.quartz.plugin.triggHistory.class: org.quartz.plugins.history.LoggingJobHistoryPlugin

```

実行するときには、VMのオプションに-Dorg.quartz.properties=foo/bar/quartz.propertiesを付けてあげます。

# 起動

JOBが登録できたので今度は実行してみます。

```HelloJobStarter.java
package quartz.jdbc.simple;

import org.quartz.Scheduler;
import org.quartz.impl.StdSchedulerFactory;

public class HelloJobStarter {

    public static void main(String[] args) throws Exception {

        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();

        try {
            Thread.sleep(60000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        scheduler.shutdown();
    }
}

```

# クラスタリング

今度はクラスタリングで動かしてみます。HelloJobStarterが複数台で動いているイメージになります。
ドキュメントにも記載されていますが、NTPで時間同期が必須です。時間同期できない場合は同一のノードで動かすようにしてください。

設定は、quartz.propertiesの下記が設定されていることを確認します。

```quartz.properties
org.quartz.scheduler.instanceId: AUTO
org.quartz.jobStore.isClustered=true
```

ここではAUTOとしましたが、複数台あるサーバそれぞれに別のプロパティファイルを用意してinstance1,instance2と記載しても大丈夫です。

早速動かしてみます。
実行コマンド

```
java -classpath quartz-sample-0.0.1-SNAPSHOT.jar:/root/uzresk/quartz-examples/lib/* -Dorg.quartz.properties=/root/uzresk/quartz-examples/target/quartzauto.properties quartz.jdbc.simple.HelloJobStarter
```

結果は貼り付けませんが、下記を試しました。

その１
1つのcronを登録して、1号機2号機でStarterを起動する。
1号機でcronが動いたことを確認した後、1号機を停止。
→２号機で続きのジョブが動くことを確認しました。

その２
4つのcronを登録して、1号機2号機でStarterを起動する。
→1号機2号機でJOBが重複なく動くことを確認

# まとめ

実行すると5秒おきに実行できることが確認できると思います。思ったより簡単ですね。

ちなみに、DBを利用する場合と利用しない場合でJavaのプログラム自体は何も変わりません。プロパティファイル(quartz.properties)にパスが通っているか否かで内部で実装が切り替わりますのでその点も非常にいい感じでした。

クラスタリングに関しては簡単なプロパティの変更で実現できてしまうのにびっくりしました。自分でこんな仕組みを作るのは非常に大変なので積極的に活用していきたいですね。

# 参考
http://quartz-scheduler.org/generated/2.2.1/html/qs-all/#page/Quartz_Scheduler_Documentation_Set%2Fto-quartz_scheduler_online_documentation_library.html