package example.grails
import example.grails.jobrunr.ProcessDeliveryJobRequest
import example.grails.jobrunr.RetryDeliveryJobRequest
import grails.compiler.GrailsCompileStatic
import java.time.Instant
import org.jobrunr.scheduling.JobRequestScheduler
import org.springframework.beans.factory.annotation.Autowired
@GrailsCompileStatic
class JobExamplesService {
@Autowired
JobRequestScheduler jobRequestScheduler
String enqueueImmediately(Long deliveryId) {
jobRequestScheduler.enqueue(new ProcessDeliveryJobRequest(deliveryId)).asUUID().toString()
}
String enqueueForLater(Long deliveryId) {
jobRequestScheduler.schedule(Instant.now().plusSeconds(30), new ProcessDeliveryJobRequest(deliveryId)).asUUID().toString()
}
String registerRecurringDelivery(Long deliveryId) {
jobRequestScheduler.scheduleRecurrently(
"delivery-${deliveryId}",
'0 3 * * *',
new ProcessDeliveryJobRequest(deliveryId)
)
}
String enqueueRetryDemo(Long deliveryId) {
jobRequestScheduler.enqueue(new RetryDeliveryJobRequest(deliveryId)).asUUID().toString()
}
}
6 Schedule and Track Jobs
JobRequestScheduler supports immediate enqueueing, delayed work, recurring work, and retries in OSS. The sample keeps each operation on the explicit request type:
enqueue returns a one-off job ID. schedule stores a job for an Instant in the future. scheduleRecurrently uses a stable recurring ID and a cron expression, so a repeated registration updates the same recurring job rather than creating an unbounded set. The handler’s @Job(retries = 3) controls its retry count; the application-level default in application.yml supplies the fallback.
Progress belongs inside the handler, where the ThreadLocalJobContext is available. DeliveryJobRequestHandler creates a three-step progress bar and increments it as it writes the visible progress value. Progress is operational feedback, not a substitute for an idempotent business state transition.
The retry endpoint deliberately schedules a handler that fails. This makes retry state visible in the dashboard without disguising a failure as successful work:
package example.grails.jobrunr
import groovy.transform.CompileStatic
import org.jobrunr.jobs.lambdas.JobRequest
@CompileStatic
class RetryDeliveryJobRequest implements JobRequest {
Long deliveryId
RetryDeliveryJobRequest() {
}
RetryDeliveryJobRequest(Long deliveryId) {
this.deliveryId = deliveryId
}
@Override
Class<RetryDeliveryJobRequestHandler> getJobRequestHandler() {
RetryDeliveryJobRequestHandler
}
}
package example.grails.jobrunr
import grails.compiler.GrailsCompileStatic
import org.jobrunr.jobs.annotations.Job
import org.jobrunr.jobs.lambdas.JobRequestHandler
import org.springframework.stereotype.Component
@Component
@GrailsCompileStatic
class RetryDeliveryJobRequestHandler implements JobRequestHandler<RetryDeliveryJobRequest> {
@Override
@Job(name = 'Retry delivery example', retries = 2, labels = ['delivery', 'retry'])
void run(RetryDeliveryJobRequest request) {
throw new IllegalStateException("Retry example for delivery ${request.deliveryId}")
}
}
DeliveryController exposes the delivery creation and lookup API, returning 201 Created, validation errors, or 404 Not Found as appropriate:
package example.grails
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class DeliveryController {
static allowedMethods = [create: 'POST', show: 'GET']
static responseFormats = ['json']
DeliveryService deliveryService
Object create(String reference) {
Delivery delivery = deliveryService.create(reference)
if (delivery.hasErrors()) {
respond(delivery.errors, status: 422)
return
}
respond(delivery, status: 201)
}
Object show(Long id) {
Delivery delivery = Delivery.get(id)
if (delivery == null) {
render status: 404
return
}
respond(delivery)
}
}
JobExamplesController and the URL mappings separately expose the asynchronous examples as 202 Accepted operations:
package example.grails
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class JobExamplesController {
static allowedMethods = [immediate: 'POST', delayed: 'POST', recurring: 'POST', retry: 'POST']
static responseFormats = ['json']
JobExamplesService jobExamplesService
Object immediate(Long id) {
respond([jobId: jobExamplesService.enqueueImmediately(id)], status: 202)
}
Object delayed(Long id) {
respond([jobId: jobExamplesService.enqueueForLater(id)], status: 202)
}
Object recurring(Long id) {
respond([recurringJobId: jobExamplesService.registerRecurringDelivery(id)], status: 202)
}
Object retry(Long id) {
respond([jobId: jobExamplesService.enqueueRetryDemo(id)], status: 202)
}
}
package example.grails
class UrlMappings {
static mappings = {
"/deliveries"(controller: 'delivery', action: 'create', method: 'POST')
"/deliveries/$id"(controller: 'delivery', action: 'show', method: 'GET')
"/jobs/immediate/$id"(controller: 'jobExamples', action: 'immediate', method: 'POST')
"/jobs/delayed/$id"(controller: 'jobExamples', action: 'delayed', method: 'POST')
"/jobs/recurring/$id"(controller: 'jobExamples', action: 'recurring', method: 'POST')
"/jobs/retry/$id"(controller: 'jobExamples', action: 'retry', method: 'POST')
}
}
JobRunr’s background-method documentation distinguishes these OSS operations from Pro-only workflows. Batches, chains or continuations, replacement, and custom retry policies are Pro features and do not appear as runnable code here.