Quartz.Net Built-In Listeners

This is the fifth post in the Quartz.Net Listener Tutorial series. In this post we'll be looking at the listeners that are shipped with the Quartz.Net distribution.

Scheduler Listeners

If we search through the Quartz.Net codebase we'll find two classes that implement the ISchedulerListener interface: SchedulerListenerSupport and BroadcastSchedulerListener. Let's look at these guys in detail now.

SchedulerListenerSupport

The documentation for the SchedulerListenerSupport says that this class is

A helpful abstract base class for implementors of ISchedulerListener

The methods in this class are empty so you only need to override the subset for the ISchedulerListener events you care about.

Well, there you have it. An empty class where all the methods of the interface are implemented but left empty. I guess this can save you some typing but I'm not sure it's very useful if you want to implement a listener. The way to use this is to inherit from this class and then only implement the methods you really are interested in.

BroadcastSchedulerListener

The documentation for the BroadcastSchedulerListener says that this class

Holds a List of references to SchedulerListener instances and broadcasts all events to them (in order).

This may be more convenient than registering all of the listeners directly with the Scheduler, and provides the flexibility of easily changing which listeners get notified.

If we were to hide all of the methods that are required by the ISchedulerListener interface, then we would be left with these two methods, which are at the heart of what this listener does:

public void AddListener(ISchedulerListener listener)
{
    listeners.Add(listener);
}

public bool RemoveListener(ISchedulerListener listener)
{
    return listeners.Remove(listener);
}

The listeners variable is a generic List, so it follows the semantics of that class. In essence this listener manages a list of listeners and then forwards all of the scheduler events to all the listeners. This is probably useful if you have a large number of scheduler listeners.

Now let's go hunting for some job listeners. I think we might find some interesting things there.

Job Listeners

After searching through the codebase for classes that implement IJobListener and classes that inherit from JobListenerSupport, I was able to find 4 job listeners: JobListenerSupport, BroadcastJobListener, JobChainingJobListener and LoggingJobHistoryPlugin. Let's look at these job listeners now.

JobListenerSupport

The documentation for JobListenerSupport states that this class is

A helpful abstract base class for implementors of IJobListener.

The methods in this class are empty so you only need to override the subset for the IJobListener events you care about.

You are required to implement IJobListener.Name to return the unique name of your IJobListener.

As you can see, it's a class similar to the SchedulerListenerSupport class we reviewed earlier. It's an empty implementation, so perhaps useful if you're only interested in some of the methods and want to save some keystrokes. It also includes a logger instance in it. The JobChainingJobListener that we're about to discuss inherits from this instead of implementing the IJobListener interface directly.

BroadcastJobListener

The documentation for BroadcastJobListener states that this class

Holds a List of references to JobListener instances and broadcasts all events to them (in order).

The broadcasting behavior of this listener to delegate listeners may be more convenient than registering all of the listeners directly with the Scheduler, and provides the flexibility of easily changing which listeners get notified.

This class is also similar to the BroadcastSchedulerListener we went over. Similarly, it provides just a few methods in addition to the ones required by the interface:

public void AddListener(IJobListener listener)
{
    listeners.Add(listener);
}

public bool RemoveListener(IJobListener listener)
{
    return listeners.Remove(listener);
}

public bool RemoveListener(string listenerName)
{
    IJobListener listener = listeners.Find(x => x.Name == listenerName);
    if (listener != null)
    {
        listeners.Remove(listener);
        return true;
    }
    return false;
}

Like its scheduler counterpart, this broadcast listener forwards all of the job events to all the registered job listeners. If you have lots of job listeners, this could be useful.

JobChainingJobListener

The documentation for JobChainingJobListener states that this class

Keeps a collection of mappings of which Job to trigger after the completion of a given job. If this listener is notified of a job completing that has a mapping, then it will then attempt to trigger the follow-up job. This achieves "job chaining", or a "poor man's workflow".

Generally an instance of this listener would be registered as a global job listener, rather than being registered directly to a given job.

If for some reason there is a failure creating the trigger for the follow-up job (which would generally only be caused by a rare serious failure in the system, or the non-existence of the follow-up job), an error messsage is logged, but no other action is taken. If you need more rigorous handling of the error, consider scheduling the triggering of the flow-up job within your job itself.

Kind of cool no? A simple way to chain jobs is provided in the box for you. It's not a fully developed workflow engine but for sequential jobs it probably does the trick. Let's look at the two methods of interest in this job listener. Here they are:

public void AddJobChainLink(JobKey firstJob, JobKey secondJob)
{
    if (firstJob == null || secondJob == null)
    {
        throw new ArgumentException("Key cannot be null!");
    }
    if (firstJob.Name == null || secondJob.Name == null)
    {
        throw new ArgumentException("Key cannot have a null name!");
    }

    chainLinks.Add(firstJob, secondJob);
}

public override void JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException)
{
    JobKey sj;
    chainLinks.TryGetValue(context.JobDetail.Key, out sj);

    if (sj == null)
    {
        return;
    }

    Log.Info(string.Format(CultureInfo.InvariantCulture, "Job '{0}' will now chain to Job '{1}'", context.JobDetail.Key, sj));
    try
    {
        context.Scheduler.TriggerJob(sj);
    }
    catch (SchedulerException se)
    {
        Log.Error(string.Format(CultureInfo.InvariantCulture, "Error encountered during chaining to Job '{0}'", sj), se);
    }
}

The first method, AddJobChainLink, is the method we would call to set up the links. That's all we need to do to set up a chain: tell the scheduler which job should fire (secondJob) after the first job completes (firstJob). The JobWasExecuted method is called by the scheduler once a job has finished running. If the listener finds a link for the job that was executed, it will trigger the new job immediately.

This is one of those features that not many people know about but it comes up every now and then on the mailing list.

LoggingJobHistoryPlugin

The documentation for LoggingJobHistoryPlugin states that this class

Logs a history of all job executions (and execution vetos) via common logging.

The logged message is customizable by setting one of the following message properties to a string that conforms to the syntax of string.Format(string,object).

JobToBeFiredMessage - available message data are:

Element Data Type Description
0 String The Job's Name.
1 String The Job's Group.
2 Date The current time.
3 String The Trigger's name.
4 String The Triggers's group.
5 Date The scheduled fire time.
6 Date The next scheduled fire time.
7 Integer The re-fire count from the JobExecutionContext.

The default message text is "Job {1}.{0} fired (by trigger {4}.{3}) at: {2:HH:mm:ss MM/dd/yyyy}"

JobSuccessMessage - available message data are:

Element Data Type Description
0 String The Job's Name.
1 String The Job's Group.
2 Date The current time.
3 String The Trigger's name.
4 String The Triggers's group.
5 Date The scheduled fire time.
6 Date The next scheduled fire time.
7 Integer The re-fire count from the JobExecutionContext.
8 Object The string value (toString() having been called) of the result (if any) that the Job set on the JobExecutionContext, with on it. "NULL" if no result was set.

The default message text is "Job {1}.{0} execution complete at {2:HH:mm:ss MM/dd/yyyy} and reports: {8}"

JobFailedMessage - available message data are:

Element Data Type Description
0 String The Job's Name.
1 String The Job's Group.
2 Date The current time.
3 String The Trigger's name.
4 String The Triggers's group.
5 Date The scheduled fire time.
6 Date The next scheduled fire time.
7 Integer The re-fire count from the JobExecutionContext.
8 String The message from the thrown JobExecution Exception.

The default message text is "Job {1}.{0} execution failed at {2:HH:mm:ss MM/dd/yyyy} and reports: {8}"

JobWasVetoedMessage - available message data are:

Element Data Type Description
0 String The Job's Name.
1 String The Job's Group.
2 Date The current time.
3 String The Trigger's name.
4 String The Triggers's group.
5 Date The scheduled fire time.
6 Date The next scheduled fire time.
7 Integer The re-fire count from the JobExecutionContext.

The default message text is "Job {1}.{0} was vetoed. It was to be fired (by trigger {4}.{3}) at: {2:HH:mm:ss MM/dd/yyyy}"

The documentation is pretty extensive but the bottom line is that it's a listener that logs all of the job related events to the common.logging infrastructure. It's pretty useful if you're not sure why jobs aren't firing when you expect them to and to get a general idea of what the scheduler is doing.

The one thing that might strike you as odd is that it's also a scheduler plugin. This is so that you can add the job listener through the configuration file upon scheduler startup so that you don't miss any events. There are a couple of ways of adding listeners to a scheduler, but I'll adress that in a different post.

Trigger Listeners

The last set of built-in listeners that we'll take a look at are the trigger listeners. Let's search the codebase for ITriggerListener and see what we find. I only found 2 classes that implement the ITriggerListener interface: TriggerListenerSupport and BroadcastTriggerListener. Just like we did for job and scheduler listeners, let's go a little deeper and talk about these classes a bit.

TriggerListenerSupport

The documentation for the TriggerListenerSupport says that this class is

A helpful abstract base class for implementors of ITriggerListener.

The methods in this class are empty so you only need to override the subset for the ITriggerListener events you care about.

You are required to implement ITriggerListener.Name to return the unique name of your ITriggerListener.

This is the third time we've seen a description such as this one, and I think that by now you get the idea. This is, for the most part, an empty class. There's a logger there and the VetoJobExecution will always return false.

BroadcastTriggerListener

The documentation for the BroadcastTriggerListener says that this class

Holds a List of references to TriggerListener instances and broadcasts all events to them (in order).

The broadcasting behavior of this listener to delegate listeners may be more convenient than registering all of the listeners directly with the Scheduler, and provides the flexibility of easily changing which listeners get notified.

You've seen this before, yes? It's another broadcasting implementation that forwards trigger events to trigger listeners that are registered with it.

What was that all about?

We've just gone over all of the listeners that come with the Quartz.Net distribution. Now you know that there are empty implementations of all 3 listeners that you can inherit from if you only want to implement a few of the methods required by the interface. You also know that there are 3 broadcasting listeners that you can use if you have large numbers of listeners that you want to manage a bit easier. Finally, we found a couple so interesting job listener classes that could come in handy if you wanted to get more detailed information on what was going on inside the scheduler (LoggingJobHistoryPlugin) or if you want to link together multiple jobs, so that they execute in sequence (JobChainingJobListener).

I hope this is useful information that may come in handy some day. If you wish to comment on this post, send me an email and I'll attach your comment to this post.