Skip to content

Frequently Asked Questions ​

Can I use Ratchet without CDI? ​

Yes. The Spring Boot integration wires the scheduler with Spring beans, transactions, lifecycle events, and Boot connection properties. It does not need a CDI container. Use the SQL or MongoDB starter and follow that runtime's guide. Quarkus has its own extension. The Micronaut integration wires the scheduler with Micronaut beans, transactions, and configuration, also without a CDI container.

The Jakarta EE integration uses CDI. Manually constructing the implementation graph is not the recommended application setup; use the supported integration for your runtime.

What databases are supported? ​

Ratchet ships with five store modules:

ModuleDatabaseMinimum Version
ratchet-store-mysqlMySQL8.0+
ratchet-store-postgresqlPostgreSQL14+
ratchet-store-oracleOracle23ai+
ratchet-store-sqlserverSQL Server2022+
ratchet-store-mongodbMongoDB6.0+

The SQL modules provide DDL scripts (src/main/resources/ddl/) as plain SQL files. Apply them however you manage your schema (Flyway, Liquibase, manual scripts, etc.). The MongoDB module initializes its collections and indexes at startup.

Adding a new database: Implement the composed JobStore SPI and provide the corresponding schema/bootstrap logic for your backend. The ratchet-tck-store submodule contains contract tests that validate any store implementation against the expected behavior; passing them earns the "Ratchet Store Compatible" label.

How does retry work? ​

When a job fails, Ratchet follows this decision path:

  1. Check for @DoNotRetry: If the exception class (or any exception in its cause chain) is annotated with @DoNotRetry, the job skips all retry logic and moves directly to the DLQ.

  2. Consult the RetryPolicy SPI: The RetryPolicy.shouldRetry(attempt, cause) method is called. The default implementation (DefaultRetryPolicy) always returns true, deferring to the attempt limit.

  3. Check attempt count: If attempts <= maxRetries and the RetryPolicy says yes, the job is rescheduled for retry.

  4. Calculate backoff delay: First, RetryPolicy.getDelay(attempt) is consulted. If it returns Duration.ZERO, the job-level backoff configuration is used instead:

    • NONE: Immediate retry (0ms delay)
    • FIXED: Constant delay equal to backoffParamMs
    • EXPONENTIAL: backoffParamMs * 2^(attempt-1), capped at 24 hours
  5. Move to DLQ: If retries are exhausted or the RetryPolicy says no, the job is marked FAILED and moved to the dead letter queue.

Example configuration:

java
scheduler.enqueue(paymentService::processRefund)
    .withMaxRetries(3)
    .withBackoff(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(1))  // 1 second base
    .submit();
// Retry delays: 1s, 2s, 4s (then DLQ)

Manual retry from the DLQ:

java
// Reset a failed job back to PENDING with attempt counter cleared
scheduler.retryJob(failedJobId);

This sets the job status to PENDING, clears the error, resets attempts to 0, and sets scheduled_time to now.

Can jobs be distributed across nodes? ​

Yes. Ratchet supports multi-node deployment. Each node runs its own poller that atomically claims jobs from the shared database using optimistic locking (claimNextBatchOptimized). This ensures:

  • Claim ownership: Atomic claiming grants one current owner. Every claim advances a sequence, and owner writes must match it. Recovery can overlap job bodies, so execution remains at least once and external side effects need idempotency.
  • Automatic failover: If a node crashes, surviving nodes detect stale heartbeats. Orphan recovery returns its jobs to PENDING while they have crash budget left. Exhausted jobs fail and enter the DLQ.
  • Node identity: Each node registers in scheduler_node with a unique ID and periodic heartbeat (default every 10 seconds).

Ratchet supports worker tag affinity: tag jobs with withTags(...) and constrain which jobs a node claims by providing a NodeTagAffinityProvider (the default is DefaultNodeTagAffinityProvider, consumed by the poller). For fully custom routing or cross-node wakeups, implement the ClusterCoordinator SPI.

Multi-node settings:

VariableDefaultPurpose
RATCHET_NODE_HEARTBEAT_INTERVAL_SECONDS10How often nodes write their heartbeat
RATCHET_NODE_ORPHAN_GRACE_SECONDS60Time before a silent node's jobs are recovered
RATCHET_NODE_ORPHAN_SCAN_INTERVAL_SECONDS300Seconds between orphan scans
RATCHET_NODE_MAX_CRASH_REDELIVERIES3Crash redeliveries before a job fails to the DLQ

What happens if the server crashes mid-job? ​

When a node crashes while jobs are RUNNING:

  1. The heartbeat stops. The crashed node's heartbeat_ts in scheduler_node goes stale.

  2. Orphan recovery kicks in. Surviving nodes run the OrphanRecoveryTimer (default every 5 minutes). It finds RUNNING jobs assigned to nodes whose heartbeat is older than the grace period (default 60 seconds).

  3. The crash budget is checked. Recovery returns the job to PENDING and adds one to its crash count while the count is below ratchet.node.max-crash-redeliveries (default 3). Once the limit is reached, the next crash fails the job and sends it to the DLQ. A limit of zero fails it on the first crash. Poison jobs that repeatedly kill their node stop after this limit.

  4. Resource permits are cleaned up. Any permits held by the dead node in scheduler_resource_permit are released.

  5. The stale node entry is deleted. The scheduler_node row for the crashed node is removed.

Important: The recovered job starts from scratch and does not resume from where it left off. If your job performs work that is not idempotent, design it to check for partial completion before proceeding.

The crash budget is separate from application retries. A committed crash failure runs onFailure if the payload can be loaded. A manual DLQ retry clears the crash count. Graceful shutdown releases claims without charging the budget, including when the drain times out.

Worst-case recovery time: orphanScanIntervalSeconds + orphanGraceSeconds. With defaults, this is 300 seconds + 60 seconds ≈ 6 minutes.

How do I migrate from Quartz? ​

Ratchet and Quartz have different architectures. Quartz uses trigger-based scheduling with XML or programmatic job definitions. Ratchet uses lambda serialization and a pull-based poller.

Differences to address:

Quartz ConceptRatchet Equivalent
Job interface + execute(JobExecutionContext)Any CDI bean with public methods
@DisallowConcurrentExecutionResource permits (withResource())
CronTriggerscheduleRecurring() or @Recurring annotation
SimpleTrigger with repeatscheduleRecurring() with cron expression
JobDataMapJobBuilder.withParam() / JobContext.param()
JobStore (RAM/JDBC)ratchet-store-mysql, ratchet-store-postgresql, ratchet-store-oracle, or ratchet-store-mongodb
Clustering via database locksBuilt-in atomic claim with optimistic locking
@PersistJobDataAfterExecutionJob results stored in job_result column

Migration steps:

  1. Replace Job implementations with CDI beans containing public methods
  2. Replace CronTrigger definitions with @Recurring annotations or scheduleRecurring() calls
  3. Replace JobDataMap usage with withParam() at submission and JobContext.param() at execution
  4. Replace Quartz tables with Ratchet DDL (the schemas are incompatible)
  5. Configure your ClassPolicy to allow your application packages

How do I migrate from jBeret? ​

jBeret implements the JSR 352 (Java Batch) specification with XML-based job definitions. Ratchet takes a code-first approach.

jBeret / JSR 352 ConceptRatchet Equivalent
job.xml with stepsCode-based chains and workflows
ItemReader / ItemProcessor / ItemWriterStreaming batches (streamingBatch())
@BatchPropertyJobBuilder.withParam()
JobOperator.start()scheduler.enqueue()
Partition mappingBatch builder with parallel children

Note: jBeret and Ratchet solve overlapping but different problems. jBeret focuses on ETL-style chunk processing with reader/processor/writer pipelines. Ratchet focuses on task scheduling with retry, circuit breaking, and workflow orchestration. For pure ETL workloads, jBeret may still be the better fit.

What is the overhead of the circuit breaker? ​

The built-in circuit breaker adds minimal overhead:

  • CLOSED state (normal operation): One lock acquisition per call to update the sliding window ring buffer. This is a ReentrantLock held for microseconds, negligible against any real job execution cost.
  • OPEN state: A single System.currentTimeMillis() comparison, with no lock on the fast path.
  • Memory: One int[] array per circuit breaker instance (size = window size, default 100). Each unique service name gets its own instance.

The circuit breaker is resolved per job by inspecting the @CircuitBreakerProtected annotation on the target method or class. Service names are cached after first resolution, so reflection runs only once.

Disabling it: To skip circuit breaking entirely, set RatchetOptions.builder().circuitBreaker(cb -> cb.enabled(false)). This replaces the circuit breaker with a passthrough that adds zero overhead.

Can I use virtual threads? ​

Yes, on a Jakarta EE 11 container. Virtual-thread support has two parts:

  1. Where jobs run. Ratchet runs each job on the ManagedExecutorService resolved from ratchet.worker.job-executor-jndi (default java:comp/DefaultManagedExecutorService). Point that at a virtual-thread-backed managed executor and jobs run on virtual threads, with the container's context propagation (CDI, transaction, security) intact. A hand-rolled Thread.ofVirtual() executor would lose that context.
  2. Backpressure accounting. execution.virtualCounterAccounting(true) swaps the semaphore-based concurrency limits for AtomicInteger counters, since virtual threads are cheap and a fixed pool no longer bounds concurrency. Each job type keeps a configurable limit (default 1000) to prevent unbounded growth.

Ratchet does not ship its own executor definition (resource-definition scanning of library jars is container-specific, so a bundled definition would not bind portably). Declare one in your application on EE 11 (Jakarta Concurrency 3.1) and point Ratchet at it:

java
@ManagedExecutorDefinition(name = "java:app/concurrent/MyVirtualExecutor", virtual = true)
@ApplicationScoped
public class VirtualExecutorConfig {}
bash
export RATCHET_WORKER_DEFAULT_THREADING_MODE=virtual
export RATCHET_WORKER_JOB_EXECUTOR_JNDI=java:app/concurrent/MyVirtualExecutor
export RATCHET_WORKER_VIRTUAL_COUNTER_ACCOUNTING=true
# Optional: adjust per-type limits (default 1000)
export RATCHET_VIRTUAL_THREAD_LIMIT_SINGLE=500
export RATCHET_VIRTUAL_THREAD_LIMIT_BATCH_CHILD=2000

Or programmatically:

java
RatchetOptions.builder()
    .execution(e -> e
        .defaultThreadingMode(RatchetOptions.ThreadingMode.VIRTUAL)
        .virtualCounterAccounting(true)
        .jobExecutorJndi("java:app/concurrent/MyVirtualExecutor"));

Requirements:

  • A Jakarta EE 11 container whose Jakarta Concurrency 3.1 implementation honors virtual = true, on Java 21+. Verified on Eclipse GlassFish 8 (the EE 11 reference implementation). Note: WildFly 41.0.1.Final accepts the definition and runs jobs on the configured executor, but does not yet create virtual threads for managed executors; jobs run on platform threads there until a later release implements it.
  • Jakarta EE 10 (Jakarta Concurrency 3.0) has no standard virtual = true attribute on @ManagedExecutorDefinition, so an application cannot portably declare a virtual-thread executor. virtualCounterAccounting(true) still switches the backpressure model, but jobs run on virtual threads only if you point the JNDI name at an executor the container itself configures as virtual through a vendor-specific mechanism.
  • Your jobs must not hold long synchronized blocks or call native methods that pin the carrier thread. Prefer ReentrantLock.

When to use virtual threads: They are most useful when jobs spend most of their time waiting on I/O (database queries, HTTP calls, file operations). For CPU-bound workloads, platform threads with appropriate pool sizes are usually sufficient.

How does job priority work? ​

Ratchet supports five priority levels:

PriorityEnum ValueNumeric Value
LOWESTJobPriority.LOWEST0
LOWJobPriority.LOW1
NORMALJobPriority.NORMAL2 (default)
HIGHJobPriority.HIGH3
CRITICALJobPriority.CRITICAL4

The poller claims jobs ordered by effective priority descending, then due time. Effective priority starts with the numeric priority and adds floor(wait_minutes / priorityBoostIntervalMinutes).

With the default 15-minute interval, a long-waiting low-priority job can overtake newer high-priority work. This boost is computed during claim ordering; it does not rewrite the stored priority. Set RatchetOptions.builder().store(s -> s.priorityBoostIntervalMinutes(0)) to disable boosting.

How are job results stored? ​

When a job method returns a non-null value, Ratchet serializes it to JSON and stores it:

sql
SELECT job_result, result_type
FROM scheduler_job
WHERE job_id = '01902c4e-c4f3-7b8a-9d3e-fedcba987654';
  • job_result contains the JSON representation
  • result_type contains the fully qualified class name of the return value

If serialization fails (e.g., the return type is not JSON-serializable), a warning is logged but the job still counts as succeeded. The result is best-effort metadata, not a critical path.

What is the maximum payload size? ​

The default maximum payload size is 100 KB, controlled by RatchetOptions.builder().payload(p -> p.maxPayloadKb(...)). Each unit is 1024 bytes. Ratchet measures the UTF-8 bytes returned by the active PayloadSerializer before encryption framing or database-specific overhead. The payload includes the serialized lambda descriptor (target class, method name, method descriptor, arguments); task, callback, branch, batch-child, and recurring payloads are checked independently.

Submission fails with PayloadTooLargeException before the oversized payload is written.

If you need to pass large data to a job, pass a reference (e.g., a database ID or S3 key) rather than the data itself:

java
// Do this
scheduler.enqueue(() -> importService.processFile(fileId));

// Don't do this
scheduler.enqueue(() -> importService.processData(hugeByteArray));

How does job archiving work? ​

Completed jobs are automatically archived based on retention settings:

VariableDefaultPurpose
RATCHET_JOB_ARCHIVE_ENABLEDtrueEnable/disable archiving
RATCHET_JOB_RETENTION_DAYS90Days before completed jobs are archived
RATCHET_JOB_ARCHIVE_CRON0 0 1 * * ?When the archiver runs (1 AM daily)
RATCHET_JOB_ARCHIVE_BATCH_SIZE1000Jobs archived per run

The archiver moves jobs from scheduler_job to scheduler_job_archive, preserving all metadata. This keeps the active job table small for poller performance while retaining history for auditing.

Job logs have separate retention controlled by RATCHET_LOG_RETENTION_DAYS (default 30 days).