db-scheduler
repository·master·Indexed 23 days ago
https://github.com/kagkarlsson/db-schedulerA lightweight, persistent, and cluster-friendly task scheduler for Java applications. It uses a relational database to manage task state, ensuring exactly-once execution in distributed environments. It supports recurring and one-time tasks, high throughput (2k-10k executions per second), and is compatible with PostgreSQL, SQL Server, MySQL, MariaDB, Oracle, SQLite, and HSQLDB. Requires Java 17 or higher since v16.x.
What's inside db-scheduler
- db-scheduler is a task-scheduler for Java designed to be a simpler, cluster-friendly alternative to Quartz. It is built to be embedded in existing applications and uses a single relational database table for task persistence. It is designed for high throughput, capable of handling 2k - 10k executions per second, and guarantees that each task is executed by exactly one scheduler instance in a cluster.
How one-time and custom tasks work
masterdb-scheduler supports tasks that do not follow a recurring pattern:
- One-time Tasks (
Tasks.oneTime(..)): Tasks with a single execution time. You can encode data into theinstanceIdor store arbitrary binary data in a separate field (defaulting to Java serialization). - Custom Tasks (
Tasks.custom(..)): Provides full control over task behavior. Use this if you need to decide whether to reschedule or remove a task based on the result of its execution.
- One-time Tasks (
Compare db-scheduler with Quartz
masterIf you are deciding between
db-schedulerandQuartz, consider the following design goals:- Simplicity and Non-invasiveness:
db-scheduleraims to be simple to use and non-invasive while still solving persistence and cluster coordination. - Schema Footprint: Unlike heavier alternatives,
db-scheduleris designed for applications that want to avoid adding a large number of tables (e.g., 11 tables) to their existing database schema. - Persistence Model:
db-scheduleruses your existing RDBMS for both persistence and coordination, following the KISS (Keep It Simple, Stupid) principle by leveraging the most common type of shared state available to most applications.
- Simplicity and Non-invasiveness:
Handling dead and unresolved tasks
masterThe scheduler manages task health through heartbeats and registration checks:
- Dead Executions: If a task is marked as 'executing' but stops updating its heartbeat (e.g., due to a JVM crash), it is considered a 'dead execution'. A
RecurringTaskis typically rescheduled tonow()when this happens. - Unresolved Tasks: Occur when a task instance exists in the database but its definition is not registered in the current service (common during rolling updates).
- They are excluded from polling.
- They remain in the DB so other instances can pick them up.
- They are automatically removed after the configured
deleteUnresolvedAfterperiod.
- Dead Executions: If a task is marked as 'executing' but stops updating its heartbeat (e.g., due to a JVM crash), it is considered a 'dead execution'. A
Enable and use task priorities
masterYou can define a priority for executions to control the order in which due tasks are fetched from the database (
order by priority desc, execution_time asc).Important Requirements:
- This feature is opt-in: you must enable it via
.enablePriority()in the Scheduler builder. - You must update your database schema to include the
prioritycolumn (defined asSMALLINT). - For large datasets, consider adding an index on
(execution_time asc, priority desc). - MySQL/MariaDB Note: Not recommended for versions below 8.x as they lack support for descending indexes.
Setting Priority
Per Instance: Use the
TaskInstance.Builderwhen scheduling a specific execution.Per Task Type: Set a default priority for all instances of a specific task using
.defaultPriority(Priority)on the task builder.// Set priority for a specific instance scheduler.schedule( MY_TASK .instance("1") .priority(100) .scheduledTo(Instant.now())); // Set default priority for a task type Tasks.recurring("my-task", FixedDelay.ofSeconds(5)) .defaultPriority(Priority.LOW) .execute(...);- This feature is opt-in: you must enable it via
How recurring tasks work
masterA recurring task runs regularly according to a
Schedule. When an execution finishes, the scheduler consults theScheduleto determine the next execution time and creates a new task-execution record.There are two types of recurring tasks:
Static Recurring Tasks (
Tasks.recurring(..)):- The
Scheduleis defined in code. - The scheduler automatically starts instances if they are missing.
- If the
Scheduleis updated in code, the scheduler updates the next execution time.
- The
Dynamic Recurring Tasks (
Tasks.recurringWithPersistentSchedule(..)):- The
Scheduleis persisted in the database (task_datafield). - Allows multiple instances of the same task type to have different schedules.
- The scheduler does not automatically schedule these; you must use
SchedulerClientto create or update instances.
- The
Compare polling strategies: fetch vs lock-and-fetch
masterdb-scheduler offers two primary polling strategies that affect how executions are retrieved from the database and how many SQL statements are executed per batch.
fetch(Default)This is the original strategy. It works by selecting a batch of due executions and then attempting to update each one to mark it as
picked=truefor the current scheduler instance. This can result in 'misses' if competing schedulers have already picked the same execution.- SQL overhead per batch: 1
select+ (2 * batch-size)updates(excluding misses). - Best for: General use cases where
SKIP LOCKEDsupport might be unavailable.
lock-and-fetchThis strategy utilizes
SELECT FOR UPDATE ... SKIP LOCKEDto fetch executions that are already pre-locked by the scheduler instance. This eliminates the need for a separate update step to 'pick' the execution and prevents misses from competing schedulers.- SQL overhead per batch: 1
select for update .. skip locked+ (1 * batch-size)updates(no misses). - Best for: High-throughput scenarios where the database supports
SKIP LOCKED. - Database Support: Currently implemented for Postgres (single-statement mode), SQL Server, and MySQL v8+ (generic mode).
- SQL overhead per batch: 1
Configure polling strategies for high throughput
masterThe scheduler uses different strategies to fetch tasks from the database. The default is
fetch.Polling Strategies
fetch(Default): Uses the.pollUsingFetch(double, double)method. Fetched executions are not locked; the scheduler competes with other instances for the lock when executing. Use this for normal usage.lock-and-fetch: UsesSELECT FOR UPDATE ... SKIP LOCKEDfor lower overhead and higher throughput. Use this if you are running >1000 executions/s. It is configured via.pollUsingLockAndFetch(double, double). Fetched executions are already locked for the current scheduler instance, saving anUPDATEstatement.
Supported Databases for
lock-and-fetch: PostgreSQL, SQL Server, and MySQL v8+.Tuning Parameters
Both methods use two
doubleparameters representing fractions of threads:- Lower limit fraction: Triggers a new fetch when the number of remaining executions is $\le$
lowerLimitFractionOfThreads * nr-of-threads. - Upper limit fraction: Determines how many executions to fetch.
Recommended settings:
- Normal usage:
.pollUsingLockAndFetch(0.5, 1.0) - High throughput:
.pollUsingLockAndFetch(1.0, 4.0)
Important considerations and gotchas
masterWhen using db-scheduler, keep the following in mind:
- Schedule Gaps: There is no guarantee that every single instant in a
Schedulewill be executed. The scheduler picks the nearest future time after the previous task finishes. - Transaction Management:
SchedulerClientmethods (schedule,cancel,reschedule) use a newConnectionfrom the providedDataSource. To ensure these actions are part of an existing transaction, provide aTransactionAwareDataSourceProxy(e.g., from Spring). - Polling Precision: The precision of task execution depends on the
pollingInterval(default 10s). You can trigger an immediate check usingscheduler.triggerCheckForDueExecutions()or enableimmediateExecution()on theBuilder.
- Schedule Gaps: There is no guarantee that every single instant in a
Quickstart: Create and start a recurring task
masterTo get started, define a
RecurringTaskusingTasks.recurring, then instantiate theSchedulerusing aDataSource, register the task, and callstart(). The task will be automatically scheduled on startup if it does not already exist in the database.RecurringTask<Void> hourlyTask = Tasks.recurring("my-hourly-task", FixedDelay.ofHours(1)) .execute((inst, ctx) -> { System.out.println("Executed!"); }); final Scheduler scheduler = Scheduler .create(dataSource) .startTasks(hourlyTask) .build(); // hourlyTask is automatically scheduled on startup if not already started (i.e. exists in the db) scheduler.start();Use db-scheduler with Spring Boot
masterFor Spring Boot applications, use the
db-scheduler-spring-boot-starterto simplify wiring.Prerequisites
- An existing Spring Boot application.
- A working
DataSourcewith the schema initialized.
Setup Steps
- Add Dependency:
- For Spring Boot 3.x, use
db-scheduler-spring-boot-starter. - For Spring Boot 2.x, use
db-scheduler-spring-boot-4-starter(version16.11.0).
- For Spring Boot 3.x, use
- Expose Tasks: Define your
Taskinstances as Spring beans. Recurring tasks will be automatically picked up and started by the scheduler. - Health Indicators (Optional): To expose scheduler state in Spring Boot Actuator health information, enable the
db-schedulerhealth indicator. - Run the application.
<!-- Maven dependency for Spring Boot 2.x --> <dependency> <groupId>com.github.kagkarlsson</groupId> <artifactId>db-scheduler-spring-boot-4-starter</artifactId> <version>16.11.0</version> </dependency>Requirements for db-scheduler
masterTo use db-scheduler, you must meet the following requirements:
- Java Version: Java 17 or higher (required since v16.x).
- Database: A relational database with a single
scheduled_taskstable created in your schema.