jobScheduler.enqueue((JobLambda) (() -> processDelivery(deliveryId)))
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:
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:
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:
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.