Quartzによる定期的なタスクのスケジューリング
最近のアプリケーションでは、定期的に特定のタスクを実行する必要があることがよくあります。このガイドでは、 Quartz エクステンションを使用して定期的にクラスター化されたタスクをスケジュールする方法を学びます。
この技術は、previewと考えられています。 preview では、下位互換性やエコシステムでの存在は保証されていません。具体的な改善には設定や API の変更が必要になるかもしれませんが、 stable になるための計画は現在進行中です。フィードバックは メーリングリスト や GitHub の課題管理 で受け付けています。 とりうるステータスの完全なリストについては、 FAQの項目 を参照してください。 |
インメモリーのスケジューラーを実行するだけであれば、 Schedulerエクステンションを使用してください。 |
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.9
-
Docker と Docker Compose、または Podman 、および Docker Compose
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.git
、 アーカイブ をダウンロードします。
ソリューションは quartz-quickstart
ディレクトリ にあります。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=quartz-quickstart"
以下が生成されます:
-
maven構造
-
ランディングページは次のURLでアクセス可能です:
http://localhost:8080
-
native
とjvm
の両方のモードに対応したDockerfile
ファイルの例 -
アプリケーション設定ファイル
MavenプロジェクトはQuarkus Quartzエクステンションもインポートします。
すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに quartz
エクステンションを追加することができます:
quarkus extension add quartz
./mvnw quarkus:add-extension -Dextensions='quartz'
./gradlew addExtension --extensions='quartz'
これにより、ビルドファイルに以下が追加されます:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-quartz</artifactId>
</dependency>
implementation("io.quarkus:quarkus-quartz")
JDBCストアを使用するには、データソースのサポートを提供する |
タスクエンティティの作成
org.acme.quartz
パッケージで、以下の内容の Task
クラスを作成します:
package org.acme.quartz;
import jakarta.persistence.Entity;
import java.time.Instant;
import jakarta.persistence.Table;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
@Table(name="TASKS")
public class Task extends PanacheEntity { (1)
public Instant createdAt;
public Task() {
createdAt = Instant.now();
}
public Task(Instant time) {
this.createdAt = time;
}
}
1 | Panacheを使ってエンティティを宣言 |
スケジュールされたジョブの作成
org.acme.quartz
パッケージで、以下の内容の TaskBean
クラスを作成します:
package org.acme.quartz;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import io.quarkus.scheduler.Scheduled;
@ApplicationScoped (1)
public class TaskBean {
@Transactional
@Scheduled(every = "10s", identity = "task-job") (2)
void schedule() {
Task task = new Task(); (3)
task.persist(); (4)
}
}
1 | アプリケーション スコープでBeanを宣言する |
2 | @Scheduled アノテーションを使用し、このメソッドを10秒ごとに実行し、識別子を付与するようにQuarkusに指示します。 |
3 | 現在の開始時刻で新しい Task を作成します。 |
4 | Panacheを使ってタスクをデータベースに永続化 |
プログラムによるジョブのスケジューリング
注入された io.quarkus.scheduler.Scheduler
を使用して、 プログラムでジョブをスケジュール できます。
しかし、Quartz API を直接活用することも可能です。
基礎となる org.quartz.Scheduler
を任意の Bean に注入できます:
package org.acme.quartz;
@ApplicationScoped
public class TaskBean {
@Inject
org.quartz.Scheduler quartz; (1)
void onStart(@Observes StartupEvent event) throws SchedulerException {
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "myGroup")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "myGroup")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10)
.repeatForever())
.build();
quartz.scheduleJob(job, trigger); (2)
}
@Transactional
void performTask() {
Task task = new Task();
task.persist();
}
// A new instance of MyJob is created by Quartz for every job execution
public static class MyJob implements Job {
@Inject
TaskBean taskBean;
public void execute(JobExecutionContext context) throws JobExecutionException {
taskBean.performTask(); (3)
}
}
}
1 | 基礎となる org.quartz.Scheduler インスタンスをインジェクトします。 |
2 | Quartz APIを使用して新しいジョブをスケジュールします。 |
3 | ジョブから TaskBean#performTask() メソッドを呼び出します。ジョブは、 Bean アーカイブ に属している場合、 コンテナ管理された Beanでもあります。 |
デフォルトでは、 @Scheduled ビジネスメソッドが見つからない限り、スケジューラーは起動されません。純粋なプログラムによるスケジューリングを行うには、スケジューラーを強制的に起動させる必要があるかもしれません。 Quartz-configuration-reference も参照してください。
|
アプリケーション設定ファイルの更新
application.properties
ファイルを編集し、以下の設定を追加します:
# Quartz configuration
quarkus.quartz.clustered=true (1)
quarkus.quartz.store-type=jdbc-cmt (2)
quarkus.quartz.misfire-policy.task-job=ignore-misfire-policy (3)
# Datasource configuration.
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/quarkus_test
# Hibernate configuration
quarkus.hibernate-orm.database.generation=none
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=no-file
# flyway configuration
quarkus.flyway.connect-retries=10
quarkus.flyway.table=flyway_quarkus_history
quarkus.flyway.migrate-at-start=true
quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.baseline-version=1.0
quarkus.flyway.baseline-description=Quartz
1 | スケジューラーがクラスターモードで実行されることを示します |
2 | データベースストアを使用してジョブ関連情報を永続化し、ノード間で共有できるようにします |
3 | 誤動作防止ポリシーは、ジョブごとに設定できます。 task-job はジョブの ID です |
cronジョブで有効な誤動作防止ポリシーは以下の通りです: smart-policy
, ignore-misfire-policy
, fire-now
および cron-trigger-do-nothing
. インターバルジョブで有効な誤動作防止ポリシーは次の通りです: smart-policy
ignore-misfire-policy
, fire-now
, simple-trigger-reschedule-now-with-existing-repeat-count
, simple-trigger-reschedule-now-with-remaining-repeat-count
, simple-trigger-reschedule-next-with-existing-count
および simple-trigger-reschedule-next-with-remaining-count
.
RESTリソースとテストの作成
org.acme.quartz.TaskResource
クラスを以下の内容で作成します:
package org.acme.quartz;
import java.util.List;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/tasks")
public class TaskResource {
@GET
public List<Task> listAll() {
return Task.listAll(); (1)
}
}
1 | 作成されたタスクのリストをデータベースから取得します |
また、以下の内容で org.acme.quartz.TaskResourceTest
テストを作成するオプションもあります:
package org.acme.quartz;
import io.quarkus.test.junit.QuarkusTest;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class TaskResourceTest {
@Test
public void tasks() throws InterruptedException {
Thread.sleep(1000); // wait at least a second to have the first task created
given()
.when().get("/tasks")
.then()
.statusCode(200)
.body("size()", is(greaterThanOrEqualTo(1))); (1)
}
}
1 | 200 のレスポンスがあり、少なくとも 1 つのタスクが作成されていることを確認してください |
Quartz テーブルの作成
V2.0.0.0QuarkusQuartzTasks.sql からコピーした内容でファイルからコピーした内容で src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql
という名前のSQLマイグレーションファイルを追加します。
ロードバランサーの設定
ルートディレクトリーに、以下の内容の nginx.conf
ファイルを作成します:
user nginx;
events {
worker_connections 1000;
}
http {
server {
listen 8080;
location / {
proxy_pass http://tasks:8080; (1)
}
}
}
1 | すべてのトラフィックをタスクアプリケーションにルーティング |
アプリケーションデプロイメントの設定
ルートディレクトリーに、以下の内容の docker-compose.yml
ファイルを作成します:
version: '3'
services:
tasks: (1)
image: quarkus-quickstarts/quartz:1.0
build:
context: ./
dockerfile: src/main/docker/Dockerfile.${QUARKUS_MODE:-jvm}
environment:
QUARKUS_DATASOURCE_URL: jdbc:postgresql://postgres/quarkus_test
networks:
- tasks-network
depends_on:
- postgres
nginx: (2)
image: nginx:1.17.6
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- tasks
ports:
- 8080:8080
networks:
- tasks-network
postgres: (3)
image: postgres:14.1
container_name: quarkus_test
environment:
- POSTGRES_USER=quarkus_test
- POSTGRES_PASSWORD=quarkus_test
- POSTGRES_DB=quarkus_test
ports:
- 5432:5432
networks:
- tasks-network
networks:
tasks-network:
driver: bridge
1 | タスクサービスの定義 |
2 | 受信トラフィックを適切なノードにルーティングするための nginx ロードバランサーを定義 |
3 | データベースを実行するための設定を定義 |
データベースの実行
別のターミナルで以下のコマンドを実行します:
docker-compose up postgres (1)
1 | docker-compose.yml ファイルで提供されている設定オプションを使用してデータベースインスタンスを起動します |
アプリケーションをDevモードで実行します。
アプリケーションを実行します:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
数秒後、別のターミナルを開き、 curl localhost:8080/tasks
を実行して、少なくとも 1 つのタスクが作成されていることを確認します。
いつものように、アプリケーションは以下の方法でパッケージ化されます:
quarkus build
./mvnw install
./gradlew build
そして次のコマンドで実行できます。 java -jar target/quarkus-app/quarkus-run.jar
.
ネイティブ実行可能ファイルを生成することもできます:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
アプリケーションのパッケージ化と複数のインスタンスの実行
アプリケーションは以下でパッケージ化することができます:
quarkus build
./mvnw install
./gradlew build
ビルドに成功したら、以下のコマンドを実行します:
docker-compose up --scale tasks=2 --scale nginx=1 (1)
1 | アプリケーションとロードバランサーの2つのインスタンスを起動 |
数秒後、別のターミナルで curl localhost:8080/tasks
を実行し、タスクが異なるタイミングで、10秒の間隔でのみ作成されていることを確認します。
ネイティブ実行可能ファイルを生成することもできます:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
前の状態、つまり古いジョブやトリガーをクリア/削除するのはデプロイ側の責任です。さらに、"Quartzクラスター"を形成するアプリケーションは同一でなければならず、そうでなければ予測不可能な結果が発生する可能性があります。 |
インスタンスIDの設定
デフォルトでは、スケジューラはマシンホスト名と現在のタイムスタンプを使った単純なインスタンスIDジェネレーターで設定されているので、クラスターモードで実行する場合、各ノードに対して適切な instance-id
を設定することを気にする必要はありません。しかし、自分で設定プロパティの参照を設定したり、他のジェネレーターを使ったりして、特定の instance-id
を定義することができます。
quarkus.quartz.instance-id=${HOST:AUTO} (1)
1 | これにより、環境変数 HOST が展開され、 HOST が設定されていない場合、 AUTO がデフォルト値として使用されます。 |
以下の例では、ジェネレーター org.quartz.simpl.HostnameInstanceIdGenerator
を hostname
という名前で構成しているので、その名前を instance-id
として使用することができます。このジェネレーターはマシンのホスト名だけを使用し、ノードに一意な名前を提供する環境では適切なものとなります。
quarkus.quartz.instance-id=hostname
quarkus.quartz.instance-id-generators.hostname.class=org.quartz.simpl.HostnameInstanceIdGenerator
適切なインスタンス識別子を定義するのは、deployerの責任です。さらに、"Quartzクラスター"を形成するアプリケーションは一意のインスタンス識別子を含む必要があり、そうでない場合は予測不可能な結果が発生する可能性があります。明示的な識別子を指定するのではなく、適切なインスタンスIDジェネレーターを使用することが推奨されます。 |
プラグインとリスナーの登録
Quarkusの設定から、 plugins
、 job-listeners
、 trigger-listeners
を登録することができます。
以下の例では、 jobHistory
というプラグインを org.quartz.plugins.history.LoggingJobHistoryPlugin
という名前で登録し、プロパティー jobSuccessMessage
を Job [{1}.{0}] execution complete and reports: {8}
として定義します。
quarkus.quartz.plugins.jobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
quarkus.quartz.plugins.jobHistory.properties.jobSuccessMessage=Job [{1}.{0}] execution complete and reports: {8}
インジェクトされた org.quartz.Scheduler
でリスナーをプログラムで登録することも出来ます:
public class MyListenerManager {
void onStart(@Observes StartupEvent event, org.quartz.Scheduler scheduler) throws SchedulerException {
scheduler.getListenerManager().addJobListener(new MyJogListener());
scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());
}
}
仮想スレッド上でスケジュールされたメソッドを実行
@Scheduled
でアノテーションされたメソッドは、 @RunOnVirtualThread
でアノテーションすることもできます。
この場合、メソッドは仮想スレッド上で呼び出されます。
メソッドは void
を返す必要があり、Java ランタイムは仮想スレッドのサポートを提供する必要があります。
詳細については、 仮想スレッドガイド をお読みください。
この機能は run-blocking-method-on-quartz-thread オプションと組み合わせることはできません。 run-blocking-method-on-quartz-thread が設定されている場合、スケジュールされたメソッドは Quartz が管理する (プラットフォームの) スレッドで実行されます。
|
Quartz設定リファレンス
ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能
Configuration property |
型 |
デフォルト |
---|---|---|
Enable cluster mode or not. If enabled make sure to set the appropriate cluster properties. Environment variable: Show more |
boolean |
|
The frequency (in milliseconds) at which the scheduler instance checks-in with other instances of the cluster. Ignored if using a Environment variable: Show more |
長 |
|
The type of store to use. When using To create Quartz tables, you can perform a schema migration via the Flyway extension using a SQL script matching your database picked from Quartz repository. Environment variable: Show more |
|
|
The name of the datasource to use. Ignored if using a Optionally needed when using the Environment variable: Show more |
string |
|
The prefix for quartz job store tables. Ignored if using a Environment variable: Show more |
string |
|
The SQL string that selects a row in the "LOCKS" table and places a lock on the row. Ignored if using a If not set, the default value of Quartz applies, for which the "{0}" is replaced during run-time with the An example SQL string Environment variable: Show more |
string |
|
Allows users to specify fully qualified class name for a custom JDBC driver delegate. This property is optional and leaving it empty will result in Quarkus automatically choosing appropriate default driver delegate implementation. Note that any custom implementation has to be a subclass of existing Quarkus implementation such as Environment variable: Show more |
string |
|
Instructs JDBCJobStore to serialize JobDataMaps in the BLOB column. Ignored if using a If this is set to If this is set to Environment variable: Show more |
boolean |
|
The name of the Quartz instance. Environment variable: Show more |
string |
|
The identifier of Quartz instance that must be unique for all schedulers working as if they are the same logical Scheduler within a cluster. Use the default value Environment variable: Show more |
string |
|
The amount of time in milliseconds that a trigger is allowed to be acquired and fired ahead of its scheduled fire time. Environment variable: Show more |
長 |
|
The maximum number of triggers that a scheduler node is allowed to acquire (for firing) at once. Environment variable: Show more |
int |
|
The size of scheduler thread pool. This will initialize the number of worker threads in the pool. It’s important to bear in mind that Quartz threads are not used to execute scheduled methods, instead the regular Quarkus thread pool is used by default. See also Environment variable: Show more |
int |
|
Thread priority of worker threads in the pool. Environment variable: Show more |
int |
|
Defines how late the schedulers should be to be considered misfired. Environment variable: Show more |
|
|
The maximum amount of time Quarkus will wait for currently running jobs to finish. If the value is Environment variable: Show more |
|
|
The quartz misfire policy for this job. Environment variable: Show more |
|
|
The quartz misfire policy for this job. Environment variable: Show more |
|
|
Properties that should be passed on directly to Quartz. Use the full configuration property key here, for instance Properties set here are completely unsupported: as Quarkus doesn’t generally know about these properties and their purpose, there is absolutely no guarantee that they will work correctly, and even if they do, that may change when upgrading to a newer version of Quarkus (even just a micro/patch version). Consider using a supported configuration property before falling back to unsupported ones. If none exists, make sure to file a feature request so that a supported configuration property can be added to Quarkus, and more importantly so that the configuration property is tested regularly. Environment variable: Show more |
Map<String,String> |
|
When set to When this option is enabled, blocking scheduled methods do not run on a Environment variable: Show more |
boolean |
|
型 |
デフォルト |
|
The quartz misfire policy for this job. Environment variable: Show more |
|
|
型 |
デフォルト |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
型 |
デフォルト |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
型 |
デフォルト |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
型 |
デフォルト |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
期間フォーマットについて
To write duration values, use the standard 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|