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:

grails-app/services/example/grails/JobExamplesService.groovy
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()
    }
}

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:

src/main/groovy/example/grails/jobrunr/RetryDeliveryJobRequest.groovy
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
    }
}
src/main/groovy/example/grails/jobrunr/RetryDeliveryJobRequestHandler.groovy
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:

grails-app/controllers/example/grails/DeliveryController.groovy
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:

grails-app/controllers/example/grails/JobExamplesController.groovy
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)
    }
}
grails-app/controllers/example/grails/UrlMappings.groovy
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.

  Get the Code