これは、なにをしたくて書いたもの?
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に関してはまだ挙動がよくわからないので、また別途見ていきたいなとも思います。