Quarkus Db Scheduler
A Quarkus extension that integrates db-scheduler with the Quarkus scheduler API. It provides persistent, cluster-friendly scheduling backed by a single database table.
Features
-
Full integration with
@Scheduledannotations fromquarkus-scheduler -
Persistent task storage using a single database table
-
Cluster-safe execution — only one node picks up a given task
-
Supports both cron expressions and fixed-interval schedules
-
Configurable polling interval, thread pool, and heartbeat
-
Pause and resume individual jobs or the entire scheduler
-
Access the underlying
com.github.kagkarlsson.scheduler.Schedulerfor advanced use cases -
Run side by side with the in-memory Quarkus scheduler for jobs that should not be clustered
Installation
Add the extension dependency to your project.
You also need a JDBC driver extension (e.g. quarkus-jdbc-postgresql) and a configured datasource.
<dependency>
<groupId>io.quarkiverse.db-scheduler</groupId>
<artifactId>quarkus-db-scheduler</artifactId>
<version>0.0.2</version>
</dependency>
Database Setup
db-scheduler requires a single table in your database. Create it before starting the application.
PostgreSQL
create table scheduled_tasks (
task_name text not null,
task_instance text not null,
task_data bytea,
execution_time timestamp with time zone not null,
picked boolean not null,
picked_by text,
last_success timestamp with time zone,
last_failure timestamp with time zone,
consecutive_failures int,
last_heartbeat timestamp with time zone,
version bigint not null,
primary key (task_name, task_instance)
);
MySQL / MariaDB
create table scheduled_tasks (
task_name varchar(100) not null,
task_instance varchar(100) not null,
task_data blob,
execution_time timestamp(6) not null,
picked bit(1) not null,
picked_by varchar(50),
last_success timestamp(6) null,
last_failure timestamp(6) null,
consecutive_failures int,
last_heartbeat timestamp(6) null,
version bigint not null,
primary key (task_name, task_instance)
);
For MySQL and MariaDB, set quarkus.db-scheduler.always-persist-timestamp-in-utc=true.
|
Oracle
create table scheduled_tasks (
task_name varchar2(100) not null,
task_instance varchar2(100) not null,
task_data blob,
execution_time timestamp with time zone not null,
picked number(1) not null,
picked_by varchar2(50),
last_success timestamp with time zone,
last_failure timestamp with time zone,
consecutive_failures number(10),
last_heartbeat timestamp with time zone,
version number(19) not null,
primary key (task_name, task_instance)
);
With Dev Services for Databases, you can create the table automatically in dev and test mode by putting the DDL in src/main/resources/init.sql and setting quarkus.datasource.devservices.init-script-path=init.sql.
|
If you use a different table name, set quarkus.db-scheduler.table-name to match.
DDL scripts for additional databases are available in the db-scheduler documentation.
Usage
Use @Scheduled annotations exactly as you would with the built-in Quarkus scheduler.
The extension automatically persists the schedules and ensures cluster-safe execution.
The identity of a @Scheduled method is the db-scheduler task name stored in the scheduled_tasks table.
Always set an explicit, stable identity.
If you leave it out, a name is derived from the generated invoker class name, which can change when you refactor your code and leave orphaned rows in the table.
|
Fixed-Interval Schedule
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.scheduler.Scheduled;
@ApplicationScoped
public class MyJobs {
@Scheduled(every = "10s", identity = "my-recurring-job")
void everyTenSeconds() {
// persisted and cluster-safe
}
}
Cron Schedule
@ApplicationScoped
public class MyJobs {
@Scheduled(cron = "0 0 12 * * ?", identity = "daily-noon")
void dailyAtNoon() {
// runs once per day across the cluster
}
}
Accessing Execution Metadata
import java.time.Instant;
import io.quarkus.scheduler.ScheduledExecution;
import io.quarkus.scheduler.Trigger;
@Scheduled(every = "30s", identity = "with-metadata")
void withMetadata(ScheduledExecution execution) {
Instant fireTime = execution.getFireTime();
Trigger trigger = execution.getTrigger();
// ...
}
Supported @Scheduled Attributes
| Attribute | Support |
|---|---|
|
Supported. Used as the persisted task name. Property expressions such as |
|
Supported. The cron syntax follows |
|
Supported for cron schedules. Otherwise the system default time zone is used. |
|
Supported as a fixed-rate schedule. The first execution runs as soon as the task is first registered. Set the value to |
|
Supported. |
|
Not supported and ignored. |
Execution events such as SuccessfulExecution, FailedExecution and SkippedExecution are fired just as they are with the built-in scheduler.
Advanced Usage
Injecting the db-scheduler Scheduler
For use cases not covered by @Scheduled (e.g. one-off tasks or programmatic scheduling), you can inject the underlying com.github.kagkarlsson.scheduler.Scheduler directly:
import com.github.kagkarlsson.scheduler.Scheduler;
@ApplicationScoped
public class AdvancedScheduling {
@Inject
Scheduler dbScheduler;
public void scheduleOneOff() {
// use the db-scheduler API directly
}
}
The db-scheduler Scheduler bean is only available when at least one @Scheduled method exists, or when the start mode is set to forced via quarkus.scheduler.start-mode=forced.
|
Pause and Resume
You can pause and resume individual jobs or the entire scheduler using the io.quarkus.scheduler.Scheduler API:
import io.quarkus.scheduler.Scheduler;
@ApplicationScoped
public class SchedulerControl {
@Inject
Scheduler scheduler;
public void pauseAll() {
scheduler.pause();
}
public void resumeAll() {
scheduler.resume();
}
public void pauseJob() {
scheduler.pause("my-recurring-job");
}
public void resumeJob() {
scheduler.resume("my-recurring-job");
}
}
Pause state is held in memory on the node where you call pause().
It is not stored in the database, it is not shared with other nodes in the cluster, and it is lost on restart.
The node that paused a job with pause(identity) still picks up that job, but completes it without running your method, so the next execution is scheduled normally.
|
Programmatic Scheduling
This extension does not support the io.quarkus.scheduler.Scheduler#newJob() and unscheduleJob() methods. Both throw UnsupportedOperationException.
Instead, you can:
-
Inject the db-scheduler
Schedulerand use its API for persistent, cluster-safe tasks, as shown above. -
Enable the composite scheduler and create in-memory jobs with
newJob(…).setExecuteWith(Scheduled.SIMPLE), as described in Non-Clustered Jobs.
Disabling or Halting the Scheduler
The standard quarkus-scheduler configuration properties apply:
-
quarkus.scheduler.enabled=falsedisables the scheduler entirely. -
quarkus.scheduler.start-mode=forcedstarts the scheduler even if no@Scheduledmethods exist. -
quarkus.scheduler.start-mode=haltedcreates the scheduler but does not start it. CallScheduler#resume()to start it.
Non-Clustered Jobs
Not every job needs to be persisted and cluster-safe.
Jobs such as cache refreshes, local metrics collection and other per-node housekeeping usually need to run on every node.
Quarkus supports multiple scheduler implementations side by side.
You can run those jobs on the in-memory scheduler from quarkus-scheduler while your other jobs use db-scheduler.
By default, a single scheduler implementation runs all scheduled methods.
db-scheduler has a higher priority than the in-memory SIMPLE implementation, so once you add this extension, db-scheduler runs every @Scheduled method.
To keep both implementations running, enable the composite scheduler:
quarkus.scheduler.use-composite-scheduler=true
Then choose the implementation for each method with @Scheduled#executeWith():
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.scheduler.Scheduled;
@ApplicationScoped
public class MyJobs {
// Persisted and executed on a single node (db-scheduler is the default)
@Scheduled(cron = "0 0 12 * * ?", identity = "daily-noon")
void dailyAtNoon() {
}
// In-memory, runs on every node, nothing is written to the database
@Scheduled(every = "30s", identity = "refresh-local-cache", executeWith = Scheduled.SIMPLE)
void refreshLocalCache() {
}
// Explicitly select db-scheduler
@Scheduled(every = "5m", identity = "cleanup", executeWith = "db-scheduler")
void cleanup() {
}
}
Methods that don’t set executeWith, or that set it to Scheduled.AUTO, run on the implementation with the highest priority, which is db-scheduler.
With the composite scheduler enabled, the injected io.quarkus.scheduler.Scheduler delegates to all implementations.
Calls such as pause(), resume() and getScheduledJobs() therefore cover both kinds of jobs.
You can also create in-memory jobs programmatically:
@Inject
io.quarkus.scheduler.Scheduler scheduler;
void scheduleLocalJob() {
scheduler.newJob("local-job")
.setInterval("10s")
.setExecuteWith(Scheduled.SIMPLE)
.setTask(execution -> {
// runs in memory on this node only
})
.schedule();
}
If quarkus.scheduler.use-composite-scheduler is false (the default) and a method sets executeWith = Scheduled.SIMPLE, the build fails.
|
Cluster Deployment
db-scheduler is designed for clustered environments. When multiple application instances share the same database, only one instance will execute a given task at any time.
Requirements for cluster operation:
-
All nodes must share the same database and
scheduled_taskstable -
All nodes must have reasonably synchronized clocks (NTP recommended)
-
All nodes should use the same polling interval
-
All nodes should use the same explicit
identityvalues for their@Scheduledmethods
No additional configuration is needed — cluster-safe execution is the default behavior.
Extension Configuration Reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
The name of the datasource to use. If not specified, the default datasource is used. Environment variable: |
string |
|
Number of threads used by the scheduler. Environment variable: |
int |
|
How often the scheduler checks the database for due executions. Environment variable: |
|
|
How often the scheduler updates its heartbeat timestamp in the database. Environment variable: |
|
|
Maximum time to wait for running tasks to complete during shutdown. Environment variable: |
|
|
If set to Environment variable: |
boolean |
|
Always persist timestamps in UTC. Recommended for MySQL and MariaDB. Environment variable: |
boolean |
|
The name of the database table used by db-scheduler. Environment variable: |
string |
|
|
About the Duration format
To write duration values, use the standard You can also use a simplified format, starting with a number:
In other cases, the simplified format is translated to the
|