Skip to content
Michi Oshima edited this page Mar 31, 2014 · 7 revisions

Scheduler (aka, "rollover-time" problem)

Background

Last year I was tasked with extending the CronTrigger of Quartz Scheduler. I did this originally in .NET (hence against Quartz.NET). I thought it'd be a good exercise to recreate the code in Clojure. But there was one main function I couldn't figure out how to write. I ended up asking for help at SNH Clojure Group.

More Background

If you were to look at Quartz's CronTrigger source code, you'd notice the CronTrigger is pretty hard to extend. So I actually did a rewrite of CronTrigger. And while at it, I changed the way the schedule is written: from Cron expression to JSON, like this:

Schedule: "Everyday at 11:30am"

  1. Cron: 0 30 11 ? * *
  2. JSON: { Hour: 11, Minute: 30 }

JSON was the thing to do for C#/.NET. Now that I'm doing this in Clojure, I wanted to write my schedules like this:

  • { :hour 11, :minute 30 }

Nice, better, yes, yes?

Next-Time Function

Given a schedule, like "Everyday at 11:30am", and a particular time, a Quartz trigger's main task is to calculate when the next scheduled time is.

  • next-time function: F(schedule, time_t) ==> next_time

This is what I had difficulty writing in Clojure. (I eventually did, but the code was ugly.)

Below is a pseudo-code to get this done in a Java-like language. Note functions like nextSecond(), nextMinute(), etc., aren't defined in the pseudo code, and I'll describe them later on:

GregCalendar nextTime(GregCalendar time_t, Schedule sched) {

    GregCalendar t = time_t;
    boolean rollOver;

    while (true) {

        [t, rollOver] = nextSecond(t, sched);
        if (rollOver)
            continue;
        [t, rollOver] = nextMinute(t, sched);
        if (rollOver)
            continue;
        [t, rollOver] = nextHour(t, sched);
        if (rollOver)
            continue;
        [t, rollOver] = nextDay(t, sched);
        if (rollOver)
            continue;
        [t, rollOver] = nextMonth(t, sched);
        if (rollOver)
            continue;
        [t, rollOver] = nextYear(t, sched);
        if (rollOver)
            continue;

        break;
    }

    return t;
}

Above looks simple enough, but I need to explain what nextSecond(), nextMinute() and the likes do. I'll try to do that with a couple of examples

Next-Time Example #1

Given a schedule and time_t:

  • Schedule: { :minute [0 15 30 45], :second [15 45] } (Can somebody help me write this schedule in plain English?)
  • time_t: 2014/03/26, 20:27:11

The next-time function would calculate:

  • next-time: 2014/03/26, 20:30:15

How does it do that?

  1. We first take the "second" value of time_t, which is 11. Then we look at the schedule for seconds, which is [15 45], and ask what the next scheduled time after 11 is. It's 15. So, we update time_t to:
    • updated time_t: 2014/03/26, 20:27:15
  2. We then take the "minute" value of time_t, which is 27. Then we look at the schedule for minutes, which is [0 15 30 45]. The next scheduled minute is 30.
    • updated time_t: 2014/03/26, 20:30:15
  3. Done.

The operation is pretty simple. If a schedule specifies hours, days, months, and years, we'd repeat pretty much the same operation on those time units. So this is what the likes of nextSecond() and nextMinute() do. The variable t is the updated time_t these functions return.

There is just one complication, however. It's rollover. On to the next example.

Next-Time Example 2

  • Schedule: { :minute [0 15 30 45], :second [15 45] }
  • time_t: 2014/03/26, 20:46:28

Next-time would calculate:

  • next-time: 2014/03/26, 21:00:15

Time rolls over to the next hour. Let's see how we figure this:

  1. Second for time_t is 28. The schedule specifies [15 45] for seconds. So, the next second is 45.
    • updated time_t: 2014/03/26, 20:46:45
  2. Minute for time_t is 46. The schedule specifies [0 15 30 45] for minutes, so the next minute is 0 of the next hour. What we must do now is to update time_t like this:
    • updated time_t: 2014/03/26, 21:00:00 (Increment the hour, and zero out minutes and seconds.)
  3. We start over and calculate second and minute values with the updated time_t.

This roll-over action is what I do with the boolean rollOver and keyword continue in my pseudo code.

Clone this wiki locally