Tuesday, May 4, 2010

Quartz and Grails: A Quick-Start Guide

Terracotta's Quartz scheduler has always played a key role in Grails development. Originally it was built into the framework; now it is a core plugin. Quartz allows us to have code executed at regular intervals. This a great way to have batch processes or system checks performed at off hours.


The Quartz plugin provides three different mechanisms (triggers) to determine the timing of job execution: Simple, Cron, and Custom. With a simple trigger, we can set the amount of time to wait before initial job execution, an interval to wait between repeated executions, and the number of times the job should be executed. The cron trigger also allows us to set a start delay. Then it takes a cron expression, which we can use to set a wide range of schedules. With a custom trigger – well, you can use your imagination.


In our TekDays application, we have lists of tasks that need to be done to organize a technical event. These tasks have due dates, but we currently have no way of reminding event organizers and volunteers when they have tasks that are overdue. That's where Quartz comes in. In this post, we'll create a Quartz job to check for overdue tasks and send a reminder email to the person assigned to that task.


First, we'll install the Quartz plugin:



> grails install-plugin quartz

The plugin provides us with a new Grails script, create-job. We'll use this script to create our TaskReminderJob.



> grails create-job TaskReminder

The create-job script will create a stubbed out TaskReminderJob.groovy that looks like this:



class TaskReminderJob {
def timeout = 5000l // execute job once in 5 seconds

def execute() {
// execute task
}
}

We'll replace the timeout property with a triggers closure in a moment, but first let's look at the execute method. This method will be called at the intervals that we determine with the triggers. We can put any code we want in this method, but the most common practice, and the one we'll follow, is to call a method of a service class.


For triggering the call to our service method we'll use the cron expression: "0 0 2 ? * MON-FRI", which will execute every weekday at 2:00AM. This is how our new TaskReminderJob looks:



class TaskReminderJob {
def taskService
static triggers = {
cron name: 'cronTrigger', cronExpression: "0 0 2 ? * MON-FRI"
}

def execute() {
taskService.sendTaskReminders()
log.info("Task reminders sent on ${new Date()}")
}
}

Now we need to add the sendTaskReminders() method to our TaskService. This method will use the Mail plugin (which you can find more about at http://grails.org/plugin/mail), so we'll add that to TaskService too. Something like this:



class TaskService {
def mailService

//...

def sendTaskReminders(){
def tasks = Task.findAllByDueDateLessThan(new Date())
tasks.each{task ->
def recipient
if (task.assignedTo)
recipient task.assignedTo.email
else
task.event.organizer.email

mailService.sendMail {
to recipient
from "admin@tekdays.com"
subject "Task Reminder"
body """The following task is overdue:
${task.title}"""
}
}
}
}

That's all there is to it. We now have the confidence of knowing that event organizers and volunteers will be kept informed of the tasks they need to do. And based on the experience of some people I know who recently put on a tech conference, this is important.


Now, something that in days past might have required us developers to climb tall mountains to make supplication to sysadmins wearing robes and conical hats has been made almost trivial by Quartz and the Quartz plugin. I, for one, am impressed and grateful. (I'm getting way too old for mountain climbing.)


But wait (as they say) – there's more. As TekDays gets more and more traffic (which we know will happen with all the new and exciting technologies coming out these days), we will want to take advantage of the power of Terracotta and cluster our Quartz jobs. To do this, we just need to create the property file: grails-app/conf/quartz.properties. Then enter the following values in this file:



org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore
org.quartz.jobStore.tcConfigUrl = localhost:9510
org.quartz.scheduler.instanceName = TekDaysScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.jmx.export = true

Finally, we'll copy quartz-terracotta-1.1.0.jar in our lib folder. The quartz-terracotta jar comes in the Terracotta download, which can be found at http://www.terracotta.org/dl/oss-download-catalog.


Now, if we've clustered our TekDays application as described in an earlier post, our scheduled jobs will be spread out across all the nodes. Not too shabby.


There is still some room for improvement in the Grails / Terracotta integration story, but I continue to be amazed at just how low the barrier of entry is to these powerful products.

4 comments:

Unknown said...

Great and yet simple example!

I know Quartz is supposed to be small, but there are a few issues that are common to anyone running multiple and long running tasks as whether one task might start when it has already started. If some task groups could run in parallel or not and so on... I still miss some of similar features in Quartz...

But as for the Grails integration, great!

Unknown said...

hi, i bought the book, but by creating an entry
i am getting an error "....ids for this class must be manually assigned before calling save()..."

what did i wrong ?

Unknown said...

Thank you Dave, great topic!
I posted this q on gquick, but I think here is more appropriate.sorry for violating DRY.

I'm a little confused with grails-terracotta integration:
I'm using distributed cache, as well as quartz.

for the cache, I've just installed distributed-cache plugin, used the default configuration, and was able to run same app on 2 machines and have a communication between them (beautiful). I guess there is some kind of "embedded" cache, since I'm not running anything else.
for quartz, on the other hand, I see that I need to run an external terracotta server.
is it tru that the dist cache runs inside the container? (I don't see how else it could work)
and if so, is there a way to use this cache for quartz, rather than start a different instance?

chain said...

When using cron expression, sometimes quartz invokes the job twice at the same time. maybe 1 second apart