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 @Scheduled annotations from quarkus-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.Scheduler for 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

identity

Supported. Used as the persisted task name. Property expressions such as ${my.job.id} are resolved.

cron

Supported. The cron syntax follows quarkus.scheduler.cron-type (Quartz by default). Set the value to off or disabled to disable the job.

timeZone

Supported for cron schedules. Otherwise the system default time zone is used.

every

Supported as a fixed-rate schedule. The first execution runs as soon as the task is first registered. Set the value to off or disabled to disable the job.

concurrentExecution, skipExecutionIf, executionMaxDelay, overdueGracePeriod

Supported.

delay, delayUnit, delayed

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 Scheduler and 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=false disables the scheduler entirely.

  • quarkus.scheduler.start-mode=forced starts the scheduler even if no @Scheduled methods exist.

  • quarkus.scheduler.start-mode=halted creates the scheduler but does not start it. Call Scheduler#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_tasks table

  • All nodes must have reasonably synchronized clocks (NTP recommended)

  • All nodes should use the same polling interval

  • All nodes should use the same explicit identity values for their @Scheduled methods

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: QUARKUS_DB_SCHEDULER_DATASOURCE

string

Number of threads used by the scheduler.

Environment variable: QUARKUS_DB_SCHEDULER_THREAD_COUNT

int

10

How often the scheduler checks the database for due executions.

Environment variable: QUARKUS_DB_SCHEDULER_POLLING_INTERVAL

Duration 

10S

How often the scheduler updates its heartbeat timestamp in the database.

Environment variable: QUARKUS_DB_SCHEDULER_HEARTBEAT_INTERVAL

Duration 

5M

Maximum time to wait for running tasks to complete during shutdown.

Environment variable: QUARKUS_DB_SCHEDULER_SHUTDOWN_MAX_WAIT

Duration 

10S

If set to true, the scheduler will trigger an immediate check for due executions when a task is scheduled.

Environment variable: QUARKUS_DB_SCHEDULER_ENABLE_IMMEDIATE_EXECUTION

boolean

false

Always persist timestamps in UTC. Recommended for MySQL and MariaDB.

Environment variable: QUARKUS_DB_SCHEDULER_ALWAYS_PERSIST_TIMESTAMP_IN_UTC

boolean

false

The name of the database table used by db-scheduler.

Environment variable: QUARKUS_DB_SCHEDULER_TABLE_NAME

string

scheduled_tasks

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.