Skip to content
Kona

Jobs & dispatch

A job is a class (or lambda) with a handle() method. Mark it ShouldQueue so Bus pushes it instead of running it inline.

Optional JobContract methods:

  • tries() — attempts before the job is marked failed (default 3)
  • backoffSeconds() — delay before a retry (default 0)
  • displayName() — label in logs and failed-job lists

Prefer Jackson-serializable jobs (public no-arg constructor + readable properties). Each payload stores a jobBody JSON snapshot so another process can restore the job without an in-memory factory.

val bus = app.resolve(Bus::class.java)
bus.dispatch(SendInvoice())
bus.dispatch(SendInvoice(), connection = "redis", queue = "mail")
bus.later(delaySeconds = 60, job = SendInvoice())
bus.dispatchSync(SendInvoice()) // always inline
bus.dispatch(new SendInvoice(), "redis", "mail", 0L);
bus.later(60L, new SendInvoice());

later / delaySeconds sets availableAt on the payload. Array, Redis, and database drivers skip the job until the delay elapses. The sync driver always runs immediately.

Run jobs one after another. The next job is dispatched only after the previous handle() succeeds.

bus.chain(ReserveStock(), ChargeCard(), SendReceipt()).dispatch()

Workers bind a WorkerContext so the chain can continue across pops. Keep chain steps serializable if workers run in other processes.

Dispatch a set of jobs, then run callbacks when the set finishes. Workers record success/failure against the batch as each job completes.

val batch = bus.batch(JobA(), JobB(), JobC())
.name("fulfill-order")
.allowFailures() // optional: finish even if some jobs fail
.then { b -> /* all done (or allowed failures) */ }
.catch { b, err -> /* cancelled or hard failure */ }
.finally { b -> /* always */ }
.dispatch()
batch.id
batch.size()
batch.progress() // 0.0 … 100.0
batch.pendingJobs()
batch.failedJobIds()
batch.cancel() // stop tracking; runs catch/finally
Batch batch = bus.batch(new JobA(), new JobB())
.name("fulfill-order")
.allowFailures()
.then(b -> { })
.onFinally(b -> { })
.dispatch();

By default batches live in memory (InMemoryBatchRepository). For durable counts across workers, set:

queue.batch.driver=database
queue.batch.table=job_batches

QueueServiceProvider creates a job_batches table (Cadence Database required) and wires DatabaseBatchRepository. Callbacks (then / catch / finally) stay in-process on the dispatching JVM; durable rows track totals, pending, failures, and finished/cancelled flags.

Optional process-wide hooks via QueueEvents.setDispatcher:

Event When
BatchDispatched Batch stored and jobs pushed
BatchJobSucceeded / BatchJobFailed Per-job outcome
BatchFinished Batch completed or cancelled
JobQueued / JobProcessed / JobFailed Individual job lifecycle

Implement HasMiddleware and return JobMiddleware wrappers. Middleware runs outermost-first around handle().

class NotifyUsers : Job, ShouldQueue, HasMiddleware {
override fun middleware() = listOf(RateLimited(maxAttempts = 10, decaySeconds = 60, key = "notify"))
override fun handle() { /* … */ }
}

Throw ReleaseJobException(delaySeconds) to put the job back on the queue without counting a failure (used by RateLimited when over limit).

class RebuildSearchIndex : Job, ShouldQueue, ShouldBeUnique {
override fun uniqueId() = "search-index"
override fun uniqueForSeconds() = 3600L
override fun handle() { /* … */ }
}

If the unique lock is held, the worker re-queues the payload and moves on.