It’s been some time since I started off in the world of software development, but I have found myself looking for a good way to schedule tasks more than once, and I’m sure you have too. You know the sort. Run this report every third Friday of the month, clean out this table at the end of every day, sacrifice a goat every summer and winter solstice, and so on.
Some tasks can happily run in Cron, provided that your operating system has Cron. However, some tasks require a bit more flexibility than this old workhorse can provide. You may need to run the task in the context of your application, for instance, or your requirements might dictate that the scheduling of certain tasks must be controlled by the users. In the case of a Java application, you might not want to start and stop the JVM every time a small task needs to execute.
Enter Quartz, OpenSymphony’s enterprise job scheduler. This API allows you to schedule tasks within your own application, which means that you’re more or less free to control the scheduling your way. In this post, we’ll look at some of the most basic tools which this API brings to the table.
I wrote a short sample application for this post, which can be found here. The application can be run without arguments, in which case it will display a message every 30 seconds. If the “cron” argument is used, the application will instead display a message every 15 seconds (we’ll discuss why this is so later on). If the “excluding” argument is used instead, the application will print a few messages (or none if you have really bad timing), take about a minute’s break, and then start displaying messages again.
Jobs and Triggers
In Quartz, a task is defined by a job, which says what gets done, and a trigger, which says when. This gives us some flexibility, since we can assign the same job (even the same instance) to a number of triggers, and vice versa. These combinations are managed by a scheduler, which takes care of all the boring details like working out priorities, and calling the jobs when they’re due.
Defining a Job
To create a Job, extend the Job interface. No brainer there. We only need to implement the execute(JobExecutionContext) method, and we’re set.
The only thing to be careful with is the exception handling in a job implementation. A job should only ever throw JobExecutionExceptions, and that includes RuntimeExceptions. The suggested practice is to wrap the entire execute method in a try/catch block and wrap and re-throw anything caught there.
The JobExecutionContext provides us with information about the task, when it fired last, when it’s supposed to fire next, and so on. It also provides a data map which we can use to share information between tasks, but personally I prefer to keep my tasks independent of each other as much as possible. It’s not a commandment though, that’s just the way I like to roll.
Once we have a job implementation, we can prep it for use by creating a JobDetail instance:
final JobDetail job = new JobDetail("Memory log", diagnosticTaskGroup, MemoryLoggerJob.class);
The first argument is the name of the job, and is useful for debugging. The second argument is the name of the group to which the job belongs, while the third is the class of our implementation. JobDetail creates a new instance of this every time it needs to run the task, and then drops it again.
The sample application uses two types of trigger – SimpleTrigger and CronTrigger. SimpleTrigger executes tasks periodically, while CronTrigger executes according to a given pattern. Both can be set up very easily, and Quartz provides a handy TriggerUtils class to generate periodic (simple) triggers.
Like the Cron command, Cron triggers take a pattern which defines 6 to 7 fields, which let us define the execution time down to the second. For instance, in the sample application, we use the pattern “0,15,30,45 * * * * ?”, which means any day of the week, every month, every day of the month, every hour, every minute at 0, 15, 30 or 45 seconds (the pattern can be read this way from right to left).
This is functionally equivalent to creating a simple trigger with a 15 second interval, but the pattern can of course be far more specific. The sample application demonstrates this behaviour when running with the “cron” argument.
Getting the show on the road
Once we have both a job and a trigger, we can simply inform the scheduler and let it go about its business:
That could hardly be simpler.
The thing with repetitive tasks is that sometimes, you just don’t want them to fire. Suppose you have a scheduled task which directs your robotic minion to make you a mug of coffee and bring it to your desk at 9:45 sharp every morning. You want (need) this task to run most days, and you can use a Cron trigger to fire it on weekdays only, but what happens if you go on a two week holiday?
You don’t want your robot to keep delivering cups to your desk (you don’t want to meet representatives of the cultures that evolve in a mug of coffee left unattended for two weeks), and you don’t want to switch off the scheduler (because you have other tasks that need to run anyway, and/or you will inevitably forget to switch it back on).
Quartz saves us the hassle of creating another robotic minion to throw away our coffee for those two weeks by allowing us to provide a calendar of days off. Calendars are identified by name, so multiple triggers can share the same calendar:
00 final HolidayCalendar exclusionCalendar = new TinyCalendar();
02 final Trigger trigger =createTrigger(10);
04 scheduler.addCalendar("exclusions", exclusionCalendar, false, true);
In line 3, we’re telling our trigger to check with a calendar called “exclusions”, and in line 4, we’re adding the calendar (the last two parameters indicate whether an existing calendar with the same name should be replaced, and whether any existing triggers which use the same calendar name should be updated). This means the trigger will operate normally, but will not fire on any day which was added to the calendar via the addExcludedDate method (line 1).
TinyCalendar is an extension of the Quartz HolidayCalendar class, and is defined in the sample application. Its sole purpose is to accept excluded dates with 1 minute precision, so that the exclusion only lasts one minute rather than 1 day. I kind of felt it would be extremely rude to have people wait 24 hours just to see the sample do nothing, thereby indicating that it is working.
Using the “excluding” argument with the sample application demonstrates this behaviour.
Never hear the end of it
If you run the sample application, you will notice that the application does not stop by itself, despite the apparent lack of anything to prevent it from doing so. The reason, in fact, lies with the scheduler itself; once a scheduler starts up, it keeps the application from closing itself. I left it like this for demonstration purposes; in a production environment however, you should close the scheduler with its shutdown() method.
And that’s not all
Quartz can also be configuration driven, and the Spring framework also offers strong support for it. I’d like to explore these in more detail, so if you’ve worked with either, I’d like to know your thoughts about them. It would also be interesting to look at how Quartz works in a clustered environment; hopefully without ending knee deep in locks.