4 Use Job Requests From Groovy

Do not enqueue Groovy closures

JobRunr needs a durable description of the method to execute. An ordinary Groovy closure does not provide one. More subtly, this tempting cast does not make the job safe:

jobScheduler.enqueue((JobLambda) (() -> processDelivery(deliveryId)))

The cast chooses a Java functional-interface overload, but it does not produce a javac-style JobRunr target. Executed checks against Groovy 4.0.33 and against Grails 8.0.0-M5 with Groovy 5.0.8 both persisted a generated Closure#doCall target. The jobs failed before the intended method ran. Never claim this cast-arrow form is a workaround.

Use JobRunr’s explicit JobRequest and JobRequestHandler contract instead. The request carries only the durable delivery ID, has a no-argument constructor for deserialization, and maps itself to a Spring handler:

src/main/groovy/example/grails/jobrunr/ProcessDeliveryJobRequest.groovy
package example.grails.jobrunr

import groovy.transform.CompileStatic
import org.jobrunr.jobs.lambdas.JobRequest

@CompileStatic
class ProcessDeliveryJobRequest implements JobRequest {
    Long deliveryId

    ProcessDeliveryJobRequest() {
    }

    ProcessDeliveryJobRequest(Long deliveryId) {
        this.deliveryId = deliveryId
    }

    @Override
    Class<DeliveryJobRequestHandler> getJobRequestHandler() {
        DeliveryJobRequestHandler
    }
}

The handler is a Spring component, statically compiled, and transactional. It reloads current state rather than serializing a Delivery instance. It returns for a missing or previously completed delivery, reports progress, and preserves interruption by restoring the interrupt flag before throwing:

src/main/groovy/example/grails/jobrunr/DeliveryJobRequestHandler.groovy
package example.grails.jobrunr

import example.grails.Delivery
import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional
import org.jobrunr.jobs.annotations.Job
import org.jobrunr.jobs.lambdas.JobRequestHandler
import org.jobrunr.server.runner.ThreadLocalJobContext
import org.springframework.stereotype.Component

@Component
@GrailsCompileStatic
@Transactional
class DeliveryJobRequestHandler implements JobRequestHandler<ProcessDeliveryJobRequest> {
    @Override
    @Job(name = 'Process delivery', retries = 3, labels = ['delivery'])
    void run(ProcessDeliveryJobRequest request) throws Exception {
        try {
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedException('Delivery worker was interrupted')
            }

            if (request.deliveryId == null) {
                return
            }

            Delivery delivery = Delivery.get(request.deliveryId)
            if (delivery == null || delivery.status == 'SUCCEEDED') {
                return
            }

            def context = ThreadLocalJobContext.getJobContext()
            def progressBar = context.progressBar(3)
            for (int step = 1; step <= 3; step++) {
                delivery.progress = step * 33
                progressBar.incrementSucceeded()
            }
            delivery.progress = 100
            delivery.status = 'SUCCEEDED'
            delivery.completedAt = new Date()
            delivery.save(failOnError: true, flush: true)
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt()
            throw exception
        }
    }
}

The SUCCEEDED guard makes retries and repeated delivery harmless for this state transition. Real handlers need the same kind of idempotency around every externally visible effect.

  Get the Code