これは、なにをしたくて書いたもの?
QuartzのJobの設定をデータベースに保存してみようかなということで。
あと、Misfire(本来起動すべき時間に実行できなかったJobの扱い)について少し調べて書いてみました。
JobやTriggerのなど定義をデータベースに保存する
ドキュメントとしては、こちらですね。JobStoreについてです。
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はこちらに配置されています。
このディレクトリ内を見ると、実際にQuartzが対応しているデータベース製品が確認できると思って良さそうです。
バージョンなどで利用するSQLに差があるものもあり、MySQLの場合はInnoDB用のものとそうでないものの2種類があります。
テーブルの名前は「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関係の設定は、こちら。
- Configuration Reference / Configure JDBC-JobStoreTX
- Configuration Reference / Configure JDBC-JobStoreCMT
また、JDBCJobStoreを使用するためには、この他にDataSourceとDriverDelegateを設定する必要があります。
DataSourceはquartz.propertiesで定義するか、アプリケーションサーバーのDataSourceを使うことになります。
設定は、こちら。
DriverDelegateは、StdJDBCDelegateまたはデータベース固有のDriverDelegateを使用します。
PostgreSQLやMicrosoft SQL Serverなど、いくつかの製品にはDriverDelegateがあるので、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.
今回、そんなに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.
Misfireに対してどのように振る舞うかは、Triggerの種類ごとに指定できる内容も変化するようです。
デフォルトでは、Triggerの種類と構成によって動的に振る舞う「smart policy」というものが適用されているそうです。
Misfireに対する指示はTriggerごとに定義があり、以下のクラス、インターフェースに定義があります。
- Trigger (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- CalendarIntervalTrigger (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- CronTrigger (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- DailyTimeIntervalTrigger (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- SimpleTrigger (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
実際には、こちらのScheduleBuilderを使って指定することになりますが。
- CalendarIntervalScheduleBuilder (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- CronScheduleBuilder (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- DailyTimeIntervalScheduleBuilder (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
- SimpleScheduleBuilder (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)
このうち、SimpleTriggerおよびSimpleScheduleBuilderに関しては、以下に例があります。
今回は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; }
こちらは次回の起動時、その次に起動していたはずの回数だけJobを実行するようにする設定のようです。
SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
それで、アプリケーション自体が停止していて、Jobが起動するはずだった実行回数を無視するような設定をして試してみようかなと
思ったのですが、SimpleTriggerではそういったものはなさそうでした。
他のTriggerを見る時に、もっと追ってみるとします…。
まとめ
今回は、QuartzのJobやTriggerなどの定義をデータベースに保存するようにしてみました。
JDBCJobStoreやDataSourceなどの設定の読み方、クラスターとしない場合の注意事項など、ちょっとドキュメントを追うのに苦労した
ところはありますが、使ってみるとそうハマるものではなかったですね。
一方で、付随して見つけたMisfireに関してはまだ挙動がよくわからないので、また別途見ていきたいなとも思います。