Search This Blog

Testing Quartz Cron expressions

Declaring complex Cron expressions is still giving me some headaches, especially when some more advanced constructs are used. After all, can you tell when the following trigger will fire "0 0 17 L-3W 6-9 ? *"? Since triggers are often meant to run far in the future, it's desired to test them beforehand and make sure they will actually fire when we think they will.

Quartz scheduler (I'm testing version 2.1.6) doesn't provide direct support for that, but it's easy to craft some simple function based on existing APIs, namely CronExpression.getNextValidTimeAfter() method. Our goal is to define a method that will return next N scheduled executions for a given Cron expression. We cannot request all since some triggers (including the one above) do not have end date, repeating infinitely. We can only depend on aforementioned getNextValidTimeAfter() which takes a date as an argument and returns nearest fire time T1 after that date. So if we want to find second scheduled execution, we must ask about next execution after the first one (T1). And so on. Let's put that into code:

Hope the meaning of our complex Cron expression is now clearer: closest week day (W) three days before the end of month (L-3) between June and September (6-9) at 17:00:00 (0 0 17). Now I started experimenting a little bit with different implementations to find the most elegant and suitable for this quite simple problem. First I noticed that the problem is not iterative, but recursive: finding next 100 execution times is equivalent to finding first execution and finding 99 remaining executions after the first one:

Seems like the implementation is much simpler: no matches - return empty list (Nil). Match found - return it prepended to next matches, unless we already collected enough dates. There is one problem with this implementation though, it's not tail-recursive. Very often this can be changed by introducing second function and accumulating the intermediate results in arguments:

A little bit more complex, but at least StackOverflowError won't wake us up in the middle of night. BTW I just noticed IntelliJ IDEA not only shows icons identifying recursion (see next to line number), but also uses different icons when tail-call optimization is employed (!):

So I thought that's best what I can get when another idea came to me. First of all, the artificial max limit (defaulting to 100) seemed awkward. Also why accumulate all the results if we can compute them on the fly, one after another? This is when I realized that I don't need Seq or List, I need an Iterator[Date]!

I've spent some time trying to reduce the if true-branch into one-liner and avoid intermediate toReturn variable. It's possible, but for clarity (and to spare your eyes) I won't reveal it*. But why an iterator, known to be less flexible and pleasant to use? Well, first of all it allows us to lazily generate next trigger times, so we don't pay for what we don't use. Also intermediate results aren't stored anywhere, so we can save memory as well. And because everything that works for sequences works for iterators as well, we can easily work with iterators in Scala, e.g. printing (taking) first 10 dates:

new TimeIterator(expr) take 10 foreach println

It's tempting to do a little benchmark comparing different implementations (here using caliper):

Seems like the implementation changes have negligible impact on time since most of the CPU is presumably burnt inside getNextValidTimeAfter().

What have we learnt today?

don't think too much about performance unless you really have a problem. Strive for best design and simplest implementation.

think a lot about data structures you want to use to represent your problem and solution. In this (trivial on first sight) problem Iterator (lazily evaluated, possibly infinite stream of items) turned out to be the best approach