Scheduling Periodic Tasks with Quartz

preview

最近のアプリケーションでは、定期的に特定のタスクを実行する必要があることがよくあります。このガイドでは、 Quartz エクステンションを使用して定期的にクラスター化されたタスクをスケジュールする方法を学びます。

この技術は、previewと考えられています。

preview では、下位互換性やエコシステムでの存在は保証されていません。具体的な改善には設定や API の変更が必要になるかもしれませんが、 stable になるための計画は現在進行中です。フィードバックは メーリングリスト や GitHub の課題管理 で受け付けています。

とりうるステータスの完全なリストについては、 FAQの項目 を参照してください。

If you only need to run in-memory scheduler use the Scheduler extension.

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 11+ がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.8.6

  • Docker と Docker Compose、または Podman 、および Docker Compose

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

アーキテクチャ

このガイドでは、10秒ごとに実行されるQuartzジョブによって作成されたタスクのリストを可視化するために、1つのRest API tasks を公開します。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

git clone https://github.com/quarkusio/quarkus-quickstarts.git で Git リポジトリーをクローンします。または、https://github.com/quarkusio/quarkus-quickstarts/archive/main.zip[アーカイブ] をダウンロードします。

ソリューションは quartz-quickstart directory にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:quartz-quickstart \
    --stream=2.13 \
    --extension='resteasy-reactive-jackson,quartz,hibernate-orm-panache,flyway,jdbc-postgresql' \
    --no-code
cd quartz-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法や使用方法については、 Quarkus CLIガイド を参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.13.9.Final:create \
    -DplatformVersion=2.13.9.Final \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=quartz-quickstart \
    -Dextensions='resteasy-reactive-jackson,quartz,hibernate-orm-panache,flyway,jdbc-postgresql' \
    -DnoCode
cd quartz-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

以下が生成されます:

  • Maven の構造

  • ランディングページは次の URL でアクセス可能です: http://localhost:8080

  • native と jvm の両方のモードに対応した Dockerfile ファイルの例

  • アプリケーション設定ファイル

MavenプロジェクトはQuarkus Quartzエクステンションもインポートします。

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに quartz エクステンションを追加することができます:

CLI
quarkus extension add 'quartz'
Maven
./mvnw quarkus:add-extension -Dextensions='quartz'
Gradle
./gradlew addExtension --extensions='quartz'

これにより、ビルドファイルに以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-quartz</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-quartz")

JDBCストアを使用するには、データソースのサポートを提供する quarkus-agroal エクステンションも必要です。

タスクエンティティの作成

org.acme.quartz パッケージで、以下の内容の Task クラスを作成します:

package org.acme.quartz;

import javax.persistence.Entity;
import java.time.Instant;
import javax.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 javax.enterprise.context.ApplicationScoped;

import javax.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 application スコープでの Bean の宣言
2 @Scheduled アノテーションを使用し、このメソッドを10秒ごとに実行し、識別子を付与するようにQuarkusに指示します。
3 現在の開始時刻で新しい Task を作成します。
4 Panacheを使ってタスクをデータベースに永続化

プログラムによるジョブのスケジューリング

Quartz APIを直接利用することも可能です。どのBeanにも、基礎となる org.quartz.Scheduler を注入することができます。

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 Invoke the TaskBean#performTask() method from the job. Jobs are also container-managed beans if they belong to a bean archive.
By default, the scheduler is not started unless a @Scheduled business method is found. You may need to force the start of the scheduler for "pure" programmatic scheduling. See also Quartz設定リファレンス.

アプリケーション設定ファイルの更新

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 javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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モードで実行します。

次のコマンドでアプリケーションを実行します。

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

数秒後、別のターミナルを開き、 curl localhost:8080/tasks を実行して、少なくとも 1 つのタスクが作成されていることを確認します。

いつものように、アプリケーションは以下の方法でパッケージ化されます。

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

そして次のコマンドで実行できます。 java -jar target/quarkus-app/quarkus-run.jar.

次のようにネイティブ実行可能ファイルを生成することもできます。

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

アプリケーションのパッケージ化と複数のインスタンスの実行

アプリケーションは以下でパッケージ化することができます:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

ビルドに成功したら、以下のコマンドを実行します:

docker-compose up --scale tasks=2 --scale nginx=1 (1)
1 アプリケーションとロードバランサーの2つのインスタンスを起動

数秒後、別のターミナルで curl localhost:8080/tasks を実行し、タスクが異なるタイミングで、10秒の間隔でのみ作成されていることを確認します。

次のようにネイティブ実行可能ファイルを生成することもできます。

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native
It’s the responsibility of the deployer to clear/remove the previous state, i.e. stale jobs and triggers. Moreover, the applications that form the "Quartz cluster" should be identical, otherwise an unpredictable result may occur.

プラグインとリスナーの登録

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());
    }
}

Quartz設定リファレンス

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

タイプ

デフォルト

Enable cluster mode or not. If enabled make sure to set the appropriate cluster properties.

Environment variable: QUARKUS_QUARTZ_CLUSTERED

Show more

boolean

false

The frequency (in milliseconds) at which the scheduler instance checks-in with other instances of the cluster.

Environment variable: QUARKUS_QUARTZ_CLUSTER_CHECKIN_INTERVAL

Show more

long

15000

The type of store to use. When using StoreType#JDBC_CMT or StoreType#JDBC_TX configuration values make sure that you have the datasource configured. See Configuring your datasource for more information. 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: QUARKUS_QUARTZ_STORE_TYPE

Show more

ram, jdbc-tx, jdbc-cmt

ram

The name of the datasource to use. Optionally needed when using the jdbc-tx or jdbc-cmt store types. If not specified, defaults to using the default datasource.

Environment variable: QUARKUS_QUARTZ_DATASOURCE

Show more

string

The prefix for quartz job store tables. Ignored if using a ram store.

Environment variable: QUARKUS_QUARTZ_TABLE_PREFIX

Show more

string

QRTZ_

The SQL string that selects a row in the "LOCKS" table and places a lock on the row. If not set, the default value of Quartz applies, for which the "{0}" is replaced during run-time with the table-prefix, the "{1}" with the instance-name. An example SQL string SELECT * FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE

Environment variable: QUARKUS_QUARTZ_SELECT_WITH_LOCK_SQL

Show more

string

The name of the Quartz instance.

Environment variable: QUARKUS_QUARTZ_INSTANCE_NAME

Show more

string

QuarkusQuartzScheduler

The size of scheduler thread pool. This will initialize the number of worker threads in the pool.

Environment variable: QUARKUS_QUARTZ_THREAD_COUNT

Show more

int

25

Thread priority of worker threads in the pool.

Environment variable: QUARKUS_QUARTZ_THREAD_PRIORITY

Show more

int

5

Defines how late the schedulers should be to be considered misfired.

Environment variable: QUARKUS_QUARTZ_MISFIRE_THRESHOLD

Show more

Duration

60S

Scheduler can be started in different modes: normal, forced or halted. By default, the scheduler is not started unless a io.quarkus.scheduler.Scheduled business method is found. If set to "forced", scheduler will be started even if no scheduled business methods are found. This is necessary for "pure" programmatic scheduling. Additionally, setting it to "halted" will behave just like forced mode but the scheduler will not start triggering jobs until an explicit start is called from the main scheduler. This is useful to programmatically register listeners before scheduler starts performing some work.

Environment variable: QUARKUS_QUARTZ_START_MODE

Show more

normal, forced, halted

normal

Trigger listeners

タイプ

デフォルト

Class name for the configuration.

Environment variable: QUARKUS_QUARTZ_TRIGGER_LISTENERS__LISTENER_NAME__CLASS

Show more

string

required

The properties passed to the class.

Environment variable: QUARKUS_QUARTZ_TRIGGER_LISTENERS__LISTENER_NAME__PROPERTIES

Show more

Map<String,String>

Job listeners

タイプ

デフォルト

Class name for the configuration.

Environment variable: QUARKUS_QUARTZ_JOB_LISTENERS__LISTENER_NAME__CLASS

Show more

string

required

The properties passed to the class.

Environment variable: QUARKUS_QUARTZ_JOB_LISTENERS__LISTENER_NAME__PROPERTIES

Show more

Map<String,String>

Plugins

タイプ

デフォルト

Class name for the configuration.

Environment variable: QUARKUS_QUARTZ_PLUGINS__PLUGIN_NAME__CLASS

Show more

string

required

The properties passed to the class.

Environment variable: QUARKUS_QUARTZ_PLUGINS__PLUGIN_NAME__PROPERTIES

Show more

Map<String,String>

Misfire policy per job configuration

タイプ

デフォルト

The quartz misfire policy for this job.

Environment variable: QUARKUS_QUARTZ_MISFIRE_POLICY__IDENTITY_

Show more

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-remaining-count, simple-trigger-reschedule-next-with-existing-count, cron-trigger-do-nothing

smart-policy

期間フォーマットについて

期間のフォーマットは標準の java.time.Duration フォーマットを使用します。詳細は Duration#parse() javadoc を参照してください。

数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 PT が暗黙的に値の前に付加され、標準の java.time.Duration 形式が得られます。