The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Scheduling Periodic Tasks with Quartz

Modern applications often need to run specific tasks periodically. In this guide, you learn how to schedule periodic clustered tasks using the Quartz extension.

这项技术被认为是preview。

preview(预览) 中,不保证向后兼容和在生态系统中的存在。具体的改进可能需要改变配置或API,并且正在计划变得 稳定 。欢迎在我们的 邮件列表 中提供反馈,或在我们的 GitHub问题列表 中提出问题。

For a full list of possible statuses, check our FAQ entry.

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

先决条件

完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • JDK 17+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.9.6

  • Docker and Docker Compose or Podman, and Docker Compose

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

应用结构

In this guide, we are going to expose one Rest API tasks to visualise the list of tasks created by a Quartz job running every 10 seconds.

解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或下载一个 存档

The solution is located in the quartz-quickstart directory.

创建Maven项目

首先,我们需要一个新的工程项目。用以下命令创建一个新项目:

CLI
quarkus create app org.acme:quartz-quickstart \
    --extension='rest-jackson,quartz,hibernate-orm-panache,flyway,jdbc-postgresql' \
    --no-code
cd quartz-quickstart

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

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

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=quartz-quickstart"

It generates:

  • Maven的结构

  • a landing page accessible on http://localhost:8080

  • example Dockerfile files for both native and jvm modes

  • 应用程序的配置文件

The Maven project also imports the Quarkus Quartz extension.

If you already have your Quarkus project configured, you can add the quartz extension to your project by running the following command in your project base directory:

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")

To use a JDBC store, the quarkus-agroal extension, which provides the datasource support, is also required.

Creating the Task Entity

In the org.acme.quartz package, create the Task class, with the following content:

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 Declare the entity using Panache

创建一个计划作业

In the org.acme.quartz package, create the TaskBean class, with the following content:

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 Declare the bean in the application scope
2 Use the @Scheduled annotation to instruct Quarkus to run this method every 10 seconds and set the unique identifier for this job.
3 Create a new Task with the current start time.
4 Persist the task in database using Panache.

Scheduling Jobs Programmatically

An injected io.quarkus.scheduler.Scheduler can be used to schedule a job programmatically. However, it is also possible to leverage the Quartz API directly. You can inject the underlying org.quartz.Scheduler in any 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 Inject the underlying org.quartz.Scheduler instance.
2 Schedule a new job using the 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 Configuration Reference.

更新该应用程序的配置文件

Edit the application.properties file and add the below configuration:

# 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 Indicate that the scheduler will be run in clustered mode
2 Use the database store to persist job related information so that they can be shared between nodes
3 The misfire policy can be configured for each job. task-job is the identity of the job.

Valid misfire policy for cron jobs are: smart-policy, ignore-misfire-policy, fire-now and cron-trigger-do-nothing. Valid misfire policy for interval jobs are: 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 and simple-trigger-reschedule-next-with-remaining-count.

Creating a REST resource and a test

Create the org.acme.quartz.TaskResource class with the following content:

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 Retrieve the list of created tasks from the database

You also have the option to create a org.acme.quartz.TaskResourceTest test with the following content:

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 Ensure that we have a 200 response and at least one task created

Creating Quartz Tables

Add a SQL migration file named src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql with the content copied from file with the content from V2.0.0__QuarkusQuartzTasks.sql.

Configuring the load balancer

In the root directory, create a nginx.conf file with the following content:

user  nginx;

events {
    worker_connections   1000;
}

http {
        server {
              listen 8080;
              location / {
                proxy_pass http://tasks:8080; (1)
              }
        }
}
1 Route all traffic to our tasks application

Setting Application Deployment

In the root directory, create a docker-compose.yml file with the following content:

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 Define the tasks service
2 Define the nginx load balancer to route incoming traffic to an appropriate node
3 Define the configuration to run the database

Running the database

In a separate terminal, run the below command:

docker-compose up postgres (1)
1 Start the database instance using the configuration options supplied in the docker-compose.yml file

Run the application in Dev Mode

使用以下命令运行该应用程序:

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

After a few seconds, open another terminal and run curl localhost:8080/tasks to verify that we have at least one task created.

像往常一样,该应用程序可以用以下方式打包:

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

Packaging the application and run several instances

The application can be packaged using:

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

Once the build is successful, run the below command:

docker-compose up --scale tasks=2 --scale nginx=1 (1)
1 Start two instances of the application and a load balancer

After a few seconds, in another terminal, run curl localhost:8080/tasks to verify that tasks were only created at different instants and in an interval of 10 seconds.

你也可以通过以下命令生成本地可执行文件:

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.

Configuring the Instance ID

By default, the scheduler is configured with a simple instance ID generator using the machine hostname and the current timestamp, so you don’t need to worry about setting a appropriate instance-id for each node when running in clustered mode. However, you can define a specific instance-id by yourself setting a configuration property reference or using other generators.

quarkus.quartz.instance-id=${HOST:AUTO} (1)
1 This will expand the HOST environment variable and use AUTO as the default value if HOST is not set.

The example below configure the generator org.quartz.simpl.HostnameInstanceIdGenerator named as hostname, so you can use its name as instance-id to be used. That generator uses just the machine hostname and can be appropriate in environments providing unique names for the nodes.

quarkus.quartz.instance-id=hostname
quarkus.quartz.instance-id-generators.hostname.class=org.quartz.simpl.HostnameInstanceIdGenerator
It’s the responsibility of the deployer to define appropriate instance identifiers. Moreover, the applications that form the "Quartz cluster" should contain unique instance identifiers, otherwise an unpredictable result may occur. It’s recommended to use an appropriate instance ID generator rather than specifying explicit identifiers.

Registering Plugin and Listeners

You can register plugins, job-listeners and trigger-listeners through Quarkus configuration.

The example below registers the plugin org.quartz.plugins.history.LoggingJobHistoryPlugin named as jobHistory with the property jobSuccessMessage defined as 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}

You can also register a listener programmatically with an injected 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());
    }
}

Run scheduled methods on virtual threads

Methods annotated with @Scheduled can also be annotated with @RunOnVirtualThread. In this case, the method is invoked on a virtual thread.

The method must return void and your Java runtime must provide support for virtual threads. Read the virtual thread guide for more details.

This feature cannot be combined with the run-blocking-method-on-quartz-thread option. If run-blocking-method-on-quartz-thread is set, the scheduled method runs on a (platform) thread managed by Quartz.

Quartz Configuration Reference

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.

Ignored if using a ram store i.e StoreType#RAM.

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.

Ignored if using a ram store i.e StoreType#RAM.

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 i.e StoreType#RAM

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.

Ignored if using a ram store i.e StoreType#RAM.

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

Instructs JDBCJobStore to serialize JobDataMaps in the BLOB column.

Ignored if using a ram store i.e StoreType#RAM.

If this is set to true, the JDBCJobStore will store the JobDataMaps in their serialize form in the BLOB Column. This is useful when you want to store complex JobData objects other than String. This is equivalent of setting org.quartz.jobStore.useProperties to false. NOTE: When this option is set to true, all the non-String classes used in JobDataMaps have to be registered for serialization when building a native image

If this is set to false (the default), the values can be stored as name-value pairs rather than storing more complex objects in their serialized form in the BLOB column. This can be handy, as you avoid the class versioning issues that can arise from serializing your non-String classes into a BLOB. This is equivalent of setting org.quartz.jobStore.useProperties to true.

Environment variable: QUARKUS_QUARTZ_SERIALIZE_JOB_DATA

Show more

boolean

false

The name of the Quartz instance.

Environment variable: QUARKUS_QUARTZ_INSTANCE_NAME

Show more

string

QuarkusQuartzScheduler

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 AUTO or some of the configured instance ID generators if you wish the identifier to be generated for you.

Environment variable: QUARKUS_QUARTZ_INSTANCE_ID

Show more

string

AUTO

The amount of time in milliseconds that a trigger is allowed to be acquired and fired ahead of its scheduled fire time.

Environment variable: QUARKUS_QUARTZ_BATCH_TRIGGER_ACQUISITION_FIRE_AHEAD_TIME_WINDOW

Show more

long

0

The maximum number of triggers that a scheduler node is allowed to acquire (for firing) at once.

Environment variable: QUARKUS_QUARTZ_BATCH_TRIGGER_ACQUISITION_MAX_COUNT

Show more

int

1

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

The maximum amount of time Quarkus will wait for currently running jobs to finish. If the value is 0, then Quarkus will not wait at all for these jobs to finish - it will call org.quartz.Scheduler.shutdown(false) in this case.

Environment variable: QUARKUS_QUARTZ_SHUTDOWN_WAIT_TIME

Show more

Duration

10S

The quartz misfire policy for this job.

Environment variable: QUARKUS_QUARTZ_SIMPLE_TRIGGER_MISFIRE_POLICY

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

The quartz misfire policy for this job.

Environment variable: QUARKUS_QUARTZ_CRON_TRIGGER_MISFIRE_POLICY

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

When set to true, blocking scheduled methods are invoked on a thread managed by Quartz instead of a thread from the regular Quarkus thread pool (default).

When this option is enabled, blocking scheduled methods do not run on a duplicated context.

Environment variable: QUARKUS_QUARTZ_RUN_BLOCKING_SCHEDULED_METHOD_ON_QUARTZ_THREAD

Show more

boolean

false

Instance ID generators

类型

默认

Class name for the configuration.

Environment variable: QUARKUS_QUARTZ_INSTANCE_ID_GENERATORS__GENERATOR_NAME__CLASS

Show more

string

required

The properties passed to the class.

Environment variable: QUARKUS_QUARTZ_INSTANCE_ID_GENERATORS__GENERATOR_NAME__PROPERTIES

Show more

Map<String,String>

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

About the Duration format

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.

Related content