Friday, October 21, 2011

CDI Events solve fancy problems elegantly (Part 2)

Problem: Does your application have a heartbeat?

I used to spread the phrase "every decent application needs a timer bean". Since JEE6, I have rephrased it to "every decent application needs a heart beat".

Whaddaya?

The @Schedule annotation improves upon the old timer service because it has zero admin effort. However, it has issues:

The configuration of @Schedule is in the static code, you cannot modify it at runtime, for example with values from a configuration file outside the deployment package.

Further, I think most timed routine jobs in an application are of the simple sort every minute, every quarter hour, every hour, at time x every day. I rarely find a reason to be more granular than this.

@Scheduled has been inspired by cron, which is flexibly configured with a text file. This is way different to compiling the static config into a class file in an EAR and not being able to modify it in the deployed application. That's very "un-cronish".

How about this idea: a heartbeat event is fired every minute and subscribers can observe it. The event has inspection methods to find out about the event time properties. The observer can be configured at runtime using data from an external config file.

The HeartbeatEvent has a bunch of inquiry methods. isTime(), isMinuteInterval, isHourInterval() take parameters, which can be fetched from a configuration, for example. Having the Calendar at hand, it is possible to invent all kinds of inquiries. Those are the ones that I find most useful.
public class HeartbeatEvent {
    private Calendar calendar;
    public HeartbeatEvent() {
        this.calendar = new GregorianCalendar();
    }
    /**
     * "at x hour and y minutes"
     * Use minute == 0 to test against a specific full hour.
     * @param hour 0-23
     * @param minute 0-59
     * @return true if match
     */
    public boolean isTime(int hour, int minute) {
        return calendar.get(Calendar.HOUR_OF_DAY) == hour && calendar.get(Calendar.MINUTE) == minute;
    }
    /**
     * "every n minutes"
     * @param minutes minute interval 0-59
     * @return true if match
     */
    public boolean isMinuteInterval(int minutes) {
        return calendar.get(Calendar.MINUTE) % minutes == 0;
    }
    /**
     * "every n hours"
     * @param hour 0-23
     * @return true if match
     */
    public boolean isHourInterval(int hour) {
        return calendar.get(Calendar.HOUR_OF_DAY) % hour == 0 && isFullHour();
    }
    public boolean isFullHour() {
        return calendar.get(Calendar.MINUTE) == 0;
    }
    public boolean isHalfHour() {
        return isMinuteInterval(30);
    }
    public boolean isQuarterHour() {
        return isMinuteInterval(15);
    }
}

The HeartbeatEmitter:
@Singleton
public class HeartbeatEmitter {
    @Inject
    private Event<HeartbeatEvent> heartbeat;

    @Schedule(hour="*", minute="*", persistent = false)
    public void emit() {
        heartbeat.fire(new HeartbeatEvent());
    }
}
Example heartbeat observer:
public void fileCleanup(@Observes HeartbeatEvent heartbeat) {
  if (heartbeat.isFullHour()) {
      // do file cleanup every full hour
  }
}

2 comments:

  1. Great blog post! A user on StackOverflow pointed at this where it caught my attention. A couple notes:

    You can declare `@Schedule` outside of code in the ejb-jar.xml file via the element.

    You can also create schedules dynamically at runtime via the `ScheduleExpression` object.

    A better description of `@Schedule` and `ScheduleExpression` here:

    http://stackoverflow.com/a/7365953/190816

    Also note there is some danger in the proposed Heartbeat solution. With a heartbeat that is scheduled to fire every minute, you could never have an event that would last longer than a minute or you will run into trouble.

    A better description of that and alternative and workarounds here:

    http://stackoverflow.com/a/12947352/190816

    ReplyDelete
  2. David, thanks for your comment. The @Singleton @Lock twist is interesting.
    You are right, in the synchronous event scenario I have proposed, a job running longer than a minute is a problem, provided that another job is waiting right behind. I guess it would also be possible to declare @Asynchronous observers to get around this limitation (thread pool exhaustion not covered). Haven't tried that.

    For the purpose I have originally designed it, that is regular short running maintenance jobs in long time intervals, this wasn't a problem. The minute precision is probably over the top for this.

    ReplyDelete