CLOVER🍀

That was when it all began.

QuartzのJobやTriggerなどの定義の保存先をデータベースにしてみる+Misfire(発火しなかったイベント)について少し

これは、なにをしたくて書いたもの?

QuartzのJobの設定をデータベースに保存してみようかなということで。

あと、Misfire(本来起動すべき時間に実行できなかったJobの扱い)について少し調べて書いてみました。

JobやTriggerのなど定義をデータベースに保存する

ドキュメントとしては、こちらですね。JobStoreについてです。

Lesson 9: Job Stores

JobStoreそのものは、QuartsのSchedulerが使用するJobやTrigger、Calenderなどにストレージメカニズムを提供するものです。

JobStore (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)

なお、JobStore自体はQuartzの利用者が直接扱うものではありません。

Never use a JobStore instance directly in your code.

Quartzの設定ファイルでどのJobStoreを使用するかは選択しますが、その操作そのものはQuartzが行います。

JobStoreの選択肢としては、以下があります。

  • RAMJobStore … データの保存先をメモリとするJobStore
  • JDBCJobStore(JobStoreTXまたはJobStoreCMT) … データの保存先をデータベースとするJobStore
  • TerracottaJobStore … データの保存先をTerracottaサーバーとするJobStore

今回のお題は、タイトルどおりJDBCJobStoreにします。

JDBCJobStoreは、Oracle、PostgreSQL、MySQL、Microsoft SQL Server、HSQLDB、H2Database、DB2など多くのデータベースに対応して
いるそうです。

使用する時にはQuartzが使用するテーブルを作成しておく必要があり、そのSQLはこちらに配置されています。

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore

このディレクトリ内を見ると、実際にQuartzが対応しているデータベース製品が確認できると思って良さそうです。

バージョンなどで利用するSQLに差があるものもあり、MySQLの場合はInnoDB用のものとそうでないものの2種類があります。

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql.sql

テーブルの名前は「QRTZ_」という接頭辞で始まりますが、これはquartz.propertiesでorg.quartz.jobStore.tablePrefixを設定することで
変更できます。

また紹介時にも書きましたが、JDBCJobStoreには2種類あり、トランザクション管理をQuartzに任せる場合は
JobStoreTX(org.quartz.impl.jdbcjobstore.JobStoreTX)を、Java EEサーバーのトランザクション管理に任せる場合は JobStoreCMT(org.quartz.impl.jdbcjobstore.JobStoreCMT)を使います。

この指定は、org.quartz.jobStore.classプロパティで行います。

JDBCJobStore関係の設定は、こちら。

また、JDBCJobStoreを使用するためには、この他にDataSourceとDriverDelegateを設定する必要があります。

DataSourceはquartz.propertiesで定義するか、アプリケーションサーバーのDataSourceを使うことになります。
設定は、こちら。

Configuration Reference

DriverDelegateは、StdJDBCDelegateまたはデータベース固有のDriverDelegateを使用します。

PostgreSQLやMicrosoft SQL Serverなど、いくつかの製品にはDriverDelegateがあるので、JDBCJobStore関係のクラスと合わせて以下を
参照してください。

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/quartz-core/src/main/java/org/quartz/impl/jdbcjobstore

では、JDBCJobStoreについてある程度見たので、実際に使ってみたいと思います。

お題

今回は、Java SE環境でアプリケーションを作成、実行しようと思うのですが、mainメソッドを持ったクラスを以下の2種類作成しようと
思います。

  • JobとTriggerを登録するだけのクラス
  • Schedulerを起動するだけのクラス

最初にJobとTriggerだけを登録して、そのアプリケーション自体は終了します。その後で起動するクラスは、Schedulerを起動するのみで
最初に登録したJobとTriggerを引き継ぐことを確認します。

デフォルトのRAMJobStoreの場合は、アプリケーションが終了するとデータが失われるはずですからね。

注意点

今回は、クラスタリングは使いません。

Lesson 11: Advanced (Enterprise) Features

こちらを参照して、クラスタリングされていない状態で複数のスケジューラーを使います。

How-To: Using Multiple (Non-Clustered) Schedulers

実際の説明は、ソースコードと一緒にすることにしましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-126-generic", arch: "amd64", family: "unix"

データベースには、MySQLを使うことにします。

$ mysql --version
mysql  Ver 8.0.30 for Linux on x86_64 (MySQL Community Server - GPL)


mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.30    |
+-----------+
1 row in set (0.00 sec)

MySQLは172.17.0.2で動作しているものとします。データベースはpractice、アカウントはkazuhira/passwordで作成しているものとします。

準備

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
    </dependencies>

MySQLを使用するので、Connector/Jを依存関係に加えてあります。

SLF4Jが使うロギングライブラリはLogbackとし、設定は最低限で作成。

src/main/resources/logback.xml

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

MySQLにQuartzのテーブルを作成する

最初に、MySQLにQuartzで必要なテーブルを作成しておきましょう。

SQLファイルをcurlで取得して、そのままmysqlコマンドで実行することにしました。

$ curl -sL https://raw.githubusercontent.com/quartz-scheduler/quartz/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql | mysql -ukazuhira -p practice

出力される情報。

Note (Code 1051): Unknown table 'practice.QRTZ_FIRED_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_PAUSED_TRIGGER_GRPS'
Note (Code 1051): Unknown table 'practice.QRTZ_SCHEDULER_STATE'
Note (Code 1051): Unknown table 'practice.QRTZ_LOCKS'
Note (Code 1051): Unknown table 'practice.QRTZ_SIMPLE_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_SIMPROP_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_CRON_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_BLOB_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_TRIGGERS'
Note (Code 1051): Unknown table 'practice.QRTZ_JOB_DETAILS'
Note (Code 1051): Unknown table 'practice.QRTZ_CALENDARS'
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.
Warning (Code 1681): Integer display width is deprecated and will be removed in a future release.

SQLファイルもかなり古いからか、いろいろ警告が出ていますが…。

テーブルはこれだけ作成されました。

mysql> show tables;
+--------------------------+
| Tables_in_practice       |
+--------------------------+
| QRTZ_BLOB_TRIGGERS       |
| QRTZ_CALENDARS           |
| QRTZ_CRON_TRIGGERS       |
| QRTZ_FIRED_TRIGGERS      |
| QRTZ_JOB_DETAILS         |
| QRTZ_LOCKS               |
| QRTZ_PAUSED_TRIGGER_GRPS |
| QRTZ_SCHEDULER_STATE     |
| QRTZ_SIMPLE_TRIGGERS     |
| QRTZ_SIMPROP_TRIGGERS    |
| QRTZ_TRIGGERS            |
+--------------------------+
11 rows in set (0.00 sec)

これで、QuartzのデータをMySQLに保存する準備ができました。

アプリケーションを作成する

では、ソースコードを作成していきます。

まずはJobの作成。

src/main/java/org/littlewings/quartz/PrintMessageJob.java

package org.littlewings.quartz;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintMessageJob implements Job {
    Logger logger = LoggerFactory.getLogger(PrintMessageJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail jobDetail = context.getJobDetail();

        JobKey jobKey = jobDetail.getKey();
        String name = jobKey.getName();
        String group = jobKey.getGroup();

        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        String message = jobDataMap.getString("message");

        logger.info("[{} / {}] execute job, messege = [{}]", name, group, message);
    }
}

せっかくなので、JobDataMapも使うようにしました。

次に、mainメソッドを持ったクラスを作成していきます。が、今回は以下の2つのmainクラスを作成することにしたわけですが。

  • JobとTriggerを登録するだけのクラス
  • Schedulerを起動するだけのクラス

設定ファイルは同じものを使うか、別々にするかちょっと考えました。

結果としてはCookbookの(非クラスター環境で)複数のSchedulerを扱う方法を書いていたので、こちらに習いました。
同じ名前のスケジューラーを使うなら、複数のインスタンスでScheduler#startを呼び出さないこと、というのがポイントのようです。

Never start (scheduler.start()) a non-clustered instance against the same set of database tables that any other instance with the same scheduler name is running (start()ed) against. You may get serious data corruption, and will definitely experience erratic behavior.

How-To: Using Multiple (Non-Clustered) Schedulers

こちらの記載に習い、JobやTriggerを登録するアプリケーションではScheduler#startは呼び出さず、Schedulerを起動するクラスのみで
Scheduler#startを呼び出すことにします。

  • In “App A” create “Scheduler A” (with config that points it at database tables prefixed with “A”), and invoke start() on “Scheduler A”. Now “Scheduler A” in “App A” will execute jobs scheduled by “Scheduler A” in “App A”
  • In “App A” create “Scheduler B” (with config that points it at database tables prefixed with “B”), and DO NOT invoke start() on “Scheduler B”. Now “Scheduler B” in “App A” can schedule jobs to be ran where “Scheduler B” is started.
  • In “App B” create “Scheduler B” (with config that points it at database tables prefixed with “B”), and invoke start() on “Scheduler B”. Now “Scheduler B” in “App B” will execute jobs scheduled by “Scheduler B” in “App A”.

JobとTriggerを登録するだけのクラス。

src/main/java/org/littlewings/quartz/JobDefinitionRegister.java

package org.littlewings.quartz;

import java.util.Set;

import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class JobDefinitionRegister {
    public static void main(String... args) {
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            JobDetail jobDetail =
                    JobBuilder
                            .newJob(PrintMessageJob.class)
                            .withIdentity("job1", "job-group1")
                            .usingJobData("message", "Hello JDBC Store Job")
                            .build();
            Trigger trigger =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("trigger1", "trigger-group1")
                            .withSchedule(
                                    SimpleScheduleBuilder
                                            .simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .repeatForever()
                            )
                            .build();


            scheduler.scheduleJob(jobDetail, Set.of(trigger), true);

            //scheduler.start();  // 呼ばないこと

            scheduler.shutdown(true);
        } catch (SchedulerException e) {
            e.printStackTrace();
        } finally {
            AbandonedConnectionCleanupThread.checkedShutdown();
        }
    }
}

こちらは、JobDetailとTriggerの定義とSchedulerへの登録だけ行い、Scheduler#startは呼び出さないことがポイントです。

Job自体は、3秒に1回実行します。

続いて、Schedulerを起動するだけのクラス。

src/main/java/org/littlewings/quartz/ScheduledRunner.java

package org.littlewings.quartz;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;

public class ScheduledRunner {
    public static void main(String... args) {
        Scheduler scheduler = null;

        try {
            scheduler = StdSchedulerFactory.getDefaultScheduler();
            scheduler.start();

            waitFor(Duration.ofSeconds(20L));
        } catch (SchedulerException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) {

                try {
                    scheduler.shutdown(true);
                } catch (SchedulerException e) {
                    e.printStackTrace();
                }
            }

            AbandonedConnectionCleanupThread.checkedShutdown();
        }
    }

    static void waitFor(Duration waitTime) {
        LocalDateTime startTime = LocalDateTime.now();
        while (ChronoUnit.SECONDS.between(startTime, LocalDateTime.now()) < waitTime.toSeconds()) {
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                // no-op
            }
        }
    }
}

こちらは、本当にSchedulerを作成してScheduler#startを呼び出すだけです。

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

20秒間実行したら、終了することにはしています。

Quartzの設定ファイルは、こちら。

src/main/resources/quartz.properties

org.quartz.scheduler.instanceName=JdbcJobStoreExampleScheduler

org.quartz.threadPool.threadCount=5

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.dataSource=mysqlds

org.quartz.dataSource.mysqlds.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
org.quartz.dataSource.mysqlds.user=kazuhira
org.quartz.dataSource.mysqlds.password=password
org.quartz.dataSource.mysqlds.maxConnections=7

org.quartz.jobStore.〜の項目がJobStoreの設定で、JDBCJobStore(JobStoreTX)を使うことを定義しています。

org.quartz.dataSource.[データソース名].〜は、Quartzが使用するHikariCPによるデータソースの定義になっています。

JDBCJobStoreとデータソースの紐付けを行うのが、org.quartz.jobStore.dataSourceですね。

再掲ですが、このあたりの設定はこちらです。

Jobを実行するスレッド数とデータソースのプールサイズの関係ですが、常にスレッドプールのサイズと同じ数のJobを実行している場合は、
データソースのプールサイズはスレッド数+2くらいにしておくと良いそうです。

If your Scheduler is busy (i.e. nearly always executing the same number of jobs as the size of the thread pool, then you should probably set the number of connections in the DataSource to be the about the size of the thread pool + 2.

Lesson 9: Job Stores

今回、そんなにJobを実行しないのですが、とりあえず合わせておきました。

これで、アプリケーションの準備は完了です。

実行してみる

用意はできたので、アプリケーションを実行してみます。

まずはJobとTriggerの定義を行いましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.quartz.JobDefinitionRegister
2022-10-14 01:24:00.130 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
2022-10-14 01:24:00.131 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Using thread monitor-based data access locking (synchronization).
2022-10-14 01:24:00.132 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - JobStoreTX initialized.
2022-10-14 01:24:00.133 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'JdbcJobStoreExampleScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 5 threads.
  Using job-store 'org.quartz.impl.jdbcjobstore.JobStoreTX' - which supports persistence. and is not clustered.

2022-10-14 01:24:00.133 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'JdbcJobStoreExampleScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
2022-10-14 01:24:00.133 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
2022-10-14 01:24:00.161 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  c.m.v.c.i.AbstractPoolBackedDataSource - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> z8kfsxar1mb873f1ynw73x|5f075acb, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.cj.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> z8kfsxar1mb873f1ynw73x|5f075acb, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 7, maxStatements -> 0, maxStatementsPerConnection -> 120, minPoolSize -> 1, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {password=******, user=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2022-10-14 01:24:00.751 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutting down.
2022-10-14 01:24:00.751 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED paused.
2022-10-14 01:24:01.124 [org.littlewings.quartz.JobDefinitionRegister.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutdown complete.

この時点で、以下のテーブルにデータが入っていました。

mysql> select * from QRTZ_JOB_DETAILS;
+------------------------------+----------+------------+-------------+----------------------------------------+------------+------------------+----------------+-------------------+------------------------------------------------------------------------------------------------------------------------------+
| SCHED_NAME                   | JOB_NAME | JOB_GROUP  | DESCRIPTION | JOB_CLASS_NAME                         | IS_DURABLE | IS_NONCONCURRENT | IS_UPDATE_DATA | REQUESTS_RECOVERY | JOB_DATA                                                                                                                     |
+------------------------------+----------+------------+-------------+----------------------------------------+------------+------------------+----------------+-------------------+------------------------------------------------------------------------------------------------------------------------------+
| JdbcJobStoreExampleScheduler | job1     | job-group1 | NULL        | org.littlewings.quartz.PrintMessageJob | 0          | 0                | 0              | 0                 | 0x230A23467269204F63742031342030313A32343A3030204A535420323032320A6D6573736167653D48656C6C6F204A4442432053746F7265204A6F620A |
+------------------------------+----------+------------+-------------+----------------------------------------+------------+------------------+----------------+-------------------+------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from QRTZ_SIMPLE_TRIGGERS;
+------------------------------+--------------+----------------+--------------+-----------------+-----------------+
| SCHED_NAME                   | TRIGGER_NAME | TRIGGER_GROUP  | REPEAT_COUNT | REPEAT_INTERVAL | TIMES_TRIGGERED |
+------------------------------+--------------+----------------+--------------+-----------------+-----------------+
| JdbcJobStoreExampleScheduler | trigger1     | trigger-group1 |           -1 |            3000 |               0 |
+------------------------------+--------------+----------------+--------------+-----------------+-----------------+
1 row in set (0.00 sec)

mysql> select * from QRTZ_TRIGGERS;
+------------------------------+--------------+----------------+----------+------------+-------------+----------------+----------------+----------+---------------+--------------+---------------+----------+---------------+---------------+--------------------+
| SCHED_NAME                   | TRIGGER_NAME | TRIGGER_GROUP  | JOB_NAME | JOB_GROUP  | DESCRIPTION | NEXT_FIRE_TIME | PREV_FIRE_TIME | PRIORITY | TRIGGER_STATE | TRIGGER_TYPE | START_TIME    | END_TIME | CALENDAR_NAME | MISFIRE_INSTR | JOB_DATA           |
+------------------------------+--------------+----------------+----------+------------+-------------+----------------+----------------+----------+---------------+--------------+---------------+----------+---------------+---------------+--------------------+
| JdbcJobStoreExampleScheduler | trigger1     | trigger-group1 | job1     | job-group1 | NULL        |  1665678240135 |             -1 |        5 | WAITING       | SIMPLE       | 1665678240135 |        0 | NULL          |             0 | 0x                 |
+------------------------------+--------------+----------------+----------+------------+-------------+----------------+----------------+----------+---------------+--------------+---------------+----------+---------------+---------------+--------------------+
1 row in set (0.00 sec)

次に、Schedulerを起動する側のプログラムを実行します。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.quartz.ScheduledRunner

Jobが起動し、3秒おきに実行されます。

2022-10-14 01:25:00.007 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
2022-10-14 01:25:00.031 [org.littlewings.quartz.ScheduledRunner.main()] INFO  c.m.v.c.i.AbstractPoolBackedDataSource - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> z8kfsxar1mb9har148z9s0|1c053ab6, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.cj.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> z8kfsxar1mb9har148z9s0|1c053ab6, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 7, maxStatements -> 0, maxStatementsPerConnection -> 120, minPoolSize -> 1, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {password=******, user=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2022-10-14 01:25:00.499 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Freed 0 triggers from 'acquired' / 'blocked' state.
2022-10-14 01:25:00.513 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Handling 1 trigger(s) that missed their scheduled fire-time.
2022-10-14 01:25:00.546 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Recovering 0 jobs that were in-progress at the time of the last shut-down.
2022-10-14 01:25:00.546 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Recovery complete.
2022-10-14 01:25:00.548 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Removed 0 'complete' triggers.
2022-10-14 01:25:00.549 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Removed 0 stale fired job entries.
2022-10-14 01:25:00.616 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED started.
2022-10-14 01:25:03.236 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:06.208 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:09.184 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:12.205 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:15.446 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:18.167 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:21.167 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:21.624 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutting down.
2022-10-14 01:25:21.624 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED paused.
2022-10-14 01:25:22.241 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutdown complete.

JobDataMapから値も取得できていますね。

少し時間を空けて、もう1度動かしてみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.quartz.ScheduledRunner

すると、動くには動いたのですが、実行回数が妙に多いです。

2022-10-14 01:25:52.013 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
2022-10-14 01:25:52.046 [org.littlewings.quartz.ScheduledRunner.main()] INFO  c.m.v.c.i.AbstractPoolBackedDataSource - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> z8kfsxar1mbaleo1u18s6j|1c053ab6, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.cj.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> z8kfsxar1mbaleo1u18s6j|1c053ab6, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 7, maxStatements -> 0, maxStatementsPerConnection -> 120, minPoolSize -> 1, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {password=******, user=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2022-10-14 01:25:52.566 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Freed 0 triggers from 'acquired' / 'blocked' state.
2022-10-14 01:25:52.583 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Recovering 0 jobs that were in-progress at the time of the last shut-down.
2022-10-14 01:25:52.583 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Recovery complete.
2022-10-14 01:25:52.585 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Removed 0 'complete' triggers.
2022-10-14 01:25:52.587 [org.littlewings.quartz.ScheduledRunner.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Removed 0 stale fired job entries.
2022-10-14 01:25:52.589 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED started.
2022-10-14 01:25:52.699 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.777 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.853 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.949 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.019 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.085 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.177 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.253 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.343 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.411 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:54.190 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:57.165 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:00.170 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:03.171 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:06.173 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:09.224 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:12.161 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:26:13.597 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutting down.
2022-10-14 01:26:13.597 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED paused.
2022-10-14 01:26:13.748 [org.littlewings.quartz.ScheduledRunner.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler JdbcJobStoreExampleScheduler_$_NON_CLUSTERED shutdown complete.

止まっていた時間で動いているはずだった分が、動いている感じがします。

2022-10-14 01:25:52.699 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.777 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.853 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:52.949 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.019 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.085 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.177 [JdbcJobStoreExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.253 [JdbcJobStoreExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.343 [JdbcJobStoreExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:53.411 [JdbcJobStoreExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]
2022-10-14 01:25:54.190 [JdbcJobStoreExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [job1 / job-group1] execute job, messege = [Hello JDBC Store Job]

これは、JobとTriggerを登録してからSchedulerを実行するまでの間、本来起動すべきだったJob…Misfire(発火しなかったイベント)に
対して再実行しているからのようですね。

Misfire(発火しなかったイベント)について

Misfire(発火しなかったイベント)については、こちらに記載があります。

Another important property of a Trigger is its “misfire instruction”. A misfire occurs if a persistent trigger “misses” its firing time because of the scheduler being shutdown, or because there are no available threads in Quartz’s thread pool for executing the job. The different trigger types have different misfire instructions available to them. By default they use a ‘smart policy’ instruction - which has dynamic behavior based on trigger type and configuration. When the scheduler starts, it searches for any persistent triggers that have misfired, and it then updates each of them based on their individually configured misfire instructions.

Lesson 4: More About Triggers

Misfireに対してどのように振る舞うかは、Triggerの種類ごとに指定できる内容も変化するようです。

デフォルトでは、Triggerの種類と構成によって動的に振る舞う「smart policy」というものが適用されているそうです。

Misfireに対する指示はTriggerごとに定義があり、以下のクラス、インターフェースに定義があります。

実際には、こちらのScheduleBuilderを使って指定することになりますが。

このうち、SimpleTriggerおよびSimpleScheduleBuilderに関しては、以下に例があります。

Example 5 - Job Misfires

今回はSimpleTriggerを使い、永遠に実行するようにしている(REPEAT_INDEFINITELY)ので、
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNTが選択されているようです。

        if (instr == Trigger.MISFIRE_INSTRUCTION_SMART_POLICY) {
            if (getRepeatCount() == 0) {
                instr = MISFIRE_INSTRUCTION_FIRE_NOW;
            } else if (getRepeatCount() == REPEAT_INDEFINITELY) {
                instr = MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;
            } else {
                // if (getRepeatCount() > 0)
                instr = MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;
            }

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/quartz-core/src/main/java/org/quartz/impl/triggers/SimpleTriggerImpl.java#L473-L481

こちらは次回の起動時、その次に起動していたはずの回数だけJobを実行するようにする設定のようです。

SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT

それで、アプリケーション自体が停止していて、Jobが起動するはずだった実行回数を無視するような設定をして試してみようかなと
思ったのですが、SimpleTriggerではそういったものはなさそうでした。

他のTriggerを見る時に、もっと追ってみるとします…。

まとめ

今回は、QuartzのJobやTriggerなどの定義をデータベースに保存するようにしてみました。

JDBCJobStoreやDataSourceなどの設定の読み方、クラスターとしない場合の注意事項など、ちょっとドキュメントを追うのに苦労した
ところはありますが、使ってみるとそうハマるものではなかったですね。

一方で、付随して見つけたMisfireに関してはまだ挙動がよくわからないので、また別途見ていきたいなとも思います。