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.
Essa tecnologia é considerada preview. In preview, backward compatibility and presence in the ecosystem is not guaranteed. Specific improvements might require changing configuration or APIs, and plans to become stable are under way. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker. Para obter uma lista completa de possíveis status, consulte nosso FAQ. |
If you only need to run in-memory scheduler use the Scheduler extension. |
Pré-requisitos
Para concluir este guia, você precisa:
-
Cerca de 15 minutos
-
Um IDE
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
Docker e Docker Compose ou Podman e Docker Compose
-
Opcionalmente, o Quarkus CLI se você quiser usá-lo
-
Opcionalmente, Mandrel ou GraalVM instalado e configurado apropriadamente se você quiser criar um executável nativo (ou Docker se você usar uma compilação de contêiner nativo)
Arquitetura
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.
Solução
Recomendamos que siga as instruções nas seções seguintes e crie a aplicação passo a passo. No entanto, você pode ir diretamente para o exemplo completo.
Clone o repositório Git: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, ou baixe um arquivo.
The solution is located in the quartz-quickstart
directory.
Criar o projeto Maven
Primeiro, precisamos de um novo projeto. Crie um novo projeto com o seguinte comando:
Para usuários do Windows:
-
Se estiver usando cmd, (não use barra invertida '\' e coloque tudo na mesma linha)
-
Se estiver usando o Powershell, envolva os parâmetros '-D' entre aspas duplas, por exemplo, '"-DprojectArtifactId=quartz-quickstart"'
It generates:
-
a estrutura Maven
-
uma página acessível em
http://localhost:8080
-
exemplo de arquivos
Dockerfile
para os modosnativo
ejvm
-
o arquivo de configuração da aplicação
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:
quarkus extension add quartz
./mvnw quarkus:add-extension -Dextensions='quartz'
./gradlew addExtension --extensions='quartz'
Isto irá adicionar o seguinte trecho no seu arquivo de build:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-quartz</artifactId>
</dependency>
implementation("io.quarkus:quarkus-quartz")
To use a JDBC store, the |
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 |
Criando um trabalho agendado
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 o bean no escopo da aplicação |
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.
|
Atualizando o arquivo de configuração da aplicação
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
Execute a aplicação com:
quarkus dev
./mvnw quarkus:dev
./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.
Como de costume, a aplicação pode ser empacotada utilizando:
quarkus build
./mvnw install
./gradlew build
and executed with java -jar target/quarkus-app/quarkus-run.jar
.
Também é possível gerar o executável nativo com:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
Packaging the application and run several instances
The application can be packaged using:
quarkus build
./mvnw install
./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.
Também é possível gerar o executável nativo com:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
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
Propriedade de Configuração Fixa no Momento da Compilação - Todas as outras propriedades de configuração podem ser sobrepostas em tempo de execução.
Configuration property |
Tipo |
Padrão |
---|---|---|
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 |
long |
|
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 |
|
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 |
long |
|
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 |
|
|
When set to When this option is enabled, blocking scheduled methods do not run on a Environment variable: Show more |
boolean |
|
Tipo |
Padrão |
|
The quartz misfire policy for this job. Environment variable: Show more |
|
|
Tipo |
Padrão |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
Tipo |
Padrão |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
Tipo |
Padrão |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
|
Tipo |
Padrão |
|
Class name for the configuration. Environment variable: Show more |
string |
required |
The properties passed to the class. Environment variable: Show more |
Map<String,String> |
About the Duration format
To write duration values, use the standard Você também pode usar um formato simplificado, começando com um número:
Em outros casos, o formato simplificado é traduzido para o formato 'java.time.Duration' para análise:
|