db-scheduler

repository·master·Indexed 23 days ago

https://github.com/kagkarlsson/db-scheduler

A 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.

Tokens
8.8K
Snippets
18
Records
46
Agent score
81%

What's inside db-scheduler

  1. Overview of db-scheduler

    master
    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.
  2. How one-time and custom tasks work

    master

    db-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 the instanceId or 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.
  3. Compare db-scheduler with Quartz

    master

    If you are deciding between db-scheduler and Quartz, consider the following design goals:

    • Simplicity and Non-invasiveness: db-scheduler aims to be simple to use and non-invasive while still solving persistence and cluster coordination.
    • Schema Footprint: Unlike heavier alternatives, db-scheduler is 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-scheduler uses 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.
  4. Handling dead and unresolved tasks

    master

    The 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 RecurringTask is typically rescheduled to now() 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 deleteUnresolvedAfter period.
  5. Enable and use task priorities

    master

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

    1. This feature is opt-in: you must enable it via .enablePriority() in the Scheduler builder.
    2. You must update your database schema to include the priority column (defined as SMALLINT).
    3. For large datasets, consider adding an index on (execution_time asc, priority desc).
    4. MySQL/MariaDB Note: Not recommended for versions below 8.x as they lack support for descending indexes.

    Setting Priority

    Per Instance: Use the TaskInstance.Builder when 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(...);
  6. How recurring tasks work

    master

    A recurring task runs regularly according to a Schedule. When an execution finishes, the scheduler consults the Schedule to determine the next execution time and creates a new task-execution record.

    There are two types of recurring tasks:

    1. Static Recurring Tasks (Tasks.recurring(..)):

      • The Schedule is defined in code.
      • The scheduler automatically starts instances if they are missing.
      • If the Schedule is updated in code, the scheduler updates the next execution time.
    2. Dynamic Recurring Tasks (Tasks.recurringWithPersistentSchedule(..)):

      • The Schedule is persisted in the database (task_data field).
      • Allows multiple instances of the same task type to have different schedules.
      • The scheduler does not automatically schedule these; you must use SchedulerClient to create or update instances.
  7. Compare polling strategies: fetch vs lock-and-fetch

    master

    db-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=true for 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 LOCKED support might be unavailable.

    lock-and-fetch

    This strategy utilizes SELECT FOR UPDATE ... SKIP LOCKED to 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).
  8. Configure polling strategies for high throughput

    master

    The scheduler uses different strategies to fetch tasks from the database. The default is fetch.

    Polling Strategies

    1. 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.

    2. lock-and-fetch: Uses SELECT FOR UPDATE ... SKIP LOCKED for 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 an UPDATE statement.

    Supported Databases for lock-and-fetch: PostgreSQL, SQL Server, and MySQL v8+.

    Tuning Parameters

    Both methods use two double parameters 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)
  9. Important considerations and gotchas

    master

    When using db-scheduler, keep the following in mind:

    • Schedule Gaps: There is no guarantee that every single instant in a Schedule will be executed. The scheduler picks the nearest future time after the previous task finishes.
    • Transaction Management: SchedulerClient methods (schedule, cancel, reschedule) use a new Connection from the provided DataSource. To ensure these actions are part of an existing transaction, provide a TransactionAwareDataSourceProxy (e.g., from Spring).
    • Polling Precision: The precision of task execution depends on the pollingInterval (default 10s). You can trigger an immediate check using scheduler.triggerCheckForDueExecutions() or enable immediateExecution() on the Builder.
  10. Quickstart: Create and start a recurring task

    master

    To get started, define a RecurringTask using Tasks.recurring, then instantiate the Scheduler using a DataSource, register the task, and call start(). 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();
  11. Use db-scheduler with Spring Boot

    master

    For Spring Boot applications, use the db-scheduler-spring-boot-starter to simplify wiring.

    Prerequisites

    • An existing Spring Boot application.
    • A working DataSource with the schema initialized.

    Setup Steps

    1. 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 (version 16.11.0).
    2. Expose Tasks: Define your Task instances as Spring beans. Recurring tasks will be automatically picked up and started by the scheduler.
    3. Health Indicators (Optional): To expose scheduler state in Spring Boot Actuator health information, enable the db-scheduler health indicator.
    4. 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>