29 June, 2012

Quartz.net task scheduling


Enterprise business apps often require that some code is executed on regular basis as scheduled task (e.g. notifications, state polling,..). There are some common ways how to achieve that:
- writing Windows Service application with a timer
- writing external .exe application run by Windows Task Scheduler
- plumbing some timer specific code
- ...
- using Quartz.net

Quartz.net 2.0 is open source job scheduling framework that can be used from smallest apps to large scale enterprise systems. Quartz.net is a pure .NET library written in C#. It is ported from popular open source Java scheduling framework Quartz.

Below is a simple recipe how to add support for scheduled jobs under ASP.NET (MVC) app. I use this approach for polling MSMQ queue on specific intervals to receive new data packets. It is worth noting that the Quartz.net framework is very powerful and we use only basic features.

1. Quartz.net configuration can be done either programmatically or using external XML  config file. The main components of Quartz that need to be configured are:
- ThreadPool: provides a set of threads to use when executing jobs;
- JobStore: is responsible for keeping track of all work data that is given to the scheduler (jobs, triggers, calendars). There are two job store implementations (custom stores can be added by implementing IJobStore interface): RAMJobStore, AdoJobStore.
- Scheduler: scheduler itself by a given name and handled instances of a JobStore and ThreadPool.

We add the following lines in web.config (app.config) file (reference to Quartz.dll is required):

<configSections>
  <section name="quartz" type="System.Configuration.NameValueSectionHandler" />
</configSections>
  
<quartz>
  <!-- configure Thread Pool -->
  <add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz" />
  <add key="quartz.threadPool.threadCount" value="10" />
  <add key="quartz.threadPool.threadPriority" value="Normal" />
  <!-- configure Job Store -->
  <add key="quartz.jobStore.misfireThreshold" value="60000" />
  <add key="quartz.jobStore.type" value="Quartz.Simpl.RAMJobStore, Quartz" />
  <!-- configure scheduler -->
  <add key="quartz.scheduler.instanceName" value="MSMQScheduler" />
  <!-- configure jobs and triggers definition -->
  <add key="quartz.plugin.xml.type" value="Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz" />
  <add key="quartz.plugin.xml.fileNames" value="~/quartzjobs.config" />
  <add key="quartz.plugin.xml.scanInterval" value="10" />
</quartz>

2. Definition of jobs & triggers can be also done programmatically or using external XML config file (quartzjobs.config). About jobs and triggers:
- Job: we make .NET class executable by the scheduler simply by making it implement the IJob interface that has only one method Execute(...). It is executed by the scheduler.

Here is our implementation of the MessageQueueJob:

namespace EE.SmsFramework.Dispatcher.Scheduler {
  
  public class MessageQueueJobIJob {

    public void Execute(IJobExecutionContext context) {
      try {
        Logger.Info("QUARTZ: Scheduled Job started");
        // packet dispatcher
        PacketDispatcher dispatcher = new PacketDispatcher();
        dispatcher.ProcessMessages();
        Logger.Info("QUARTZ: Scheduled Job ended");
      } catch (Exception ex) {
        Logger.Error(" --> PacketDispatcher Execute Error:", ex);
      }
    }

  }
}

- Trigger: it is used to fire the execution of jobs.Most commonly used types are SimpleTrigger (single execution of a job at a given time) and CronTrigger (calendar-like schedules with CRON expressions).

Here is our definition of a single job (MSMQJob) and a single trigger (MSMQJobTrigger) inside quartzjobs.config:

<?xml version="1.0"?>
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">

  <processing-directives>
    <overwrite-existing-data>true</overwrite-existing-data>
  </processing-directives>

  <schedule>
    <job>
      <name>MSMQJob</name>
      <group>MSMQJobs</group>
      <description>Dispatches messages from MSMQ and sends them to SMS Postar.</description>
      <job-type>EE.SmsFramework.Dispatcher.Scheduler.MessageQueueJob, SmsFramework.Dispatcher</job-type>
      <durable>true</durable>
      <recover>false</recover>
    </job>
    
    <trigger>
      <cron>
        <name>MSMQJobTrigger</name>
        <group>MSMQJobs</group>
        <description>Triggers MSMQ dispatcher job</description>
        <job-name>MSMQJob</job-name>
        <job-group>MSMQJobs</job-group>
        <!-- every 15 minutes -->
        <cron-expression>0 0/15 * * * ?</cron-expression>
      </cron>
    </trigger>
  </schedule>

</job-scheduling-data>

3. Instantiation of a Quartz.net Scheduler instance is maintained by StdSchedulerFactory which implements ISchedulerFactory. It uses a set of properties (NameValueCollection) to create and initialize a Quartz Scheduler. The properties are generally stored in and loaded from a file, but can also be created by your program and handed directly to the factory. Simply calling GetScheduler() on the factory will produce the scheduler, initialize it (and its ThreadPool and JobStore), and return a handle to its public interface.

We instantiate Scheduler instance inside the HttpApplication OnStart event (Global.asax). Web application instance is required to be always alive (IIS application pool must not be stopped). This is simply done by executing void web request inside OnEnd event.

 public class MvcApplication : HttpApplication {

    private IScheduler scheduler = null;

    protected void Application_Start(object sender, EventArgs e) {
      ...
      // quartz scheduler
        ISchedulerFactory factory = new StdSchedulerFactory();
      this.scheduler = factory.GetScheduler();
      this.scheduler.Start();
      ...
    }

    protected void Application_End(object sender, EventArgs e) {
      ...
      this.PingServer();
    }

    /// <summary>
    /// Pings the server - startup the IIS application pool.
    /// </summary>
    private void PingServer() {
      try {
        WebClient client = new WebClient();
        client.DownloadString(string.Format("http://{0}{1}"Environment.MachineName, VirtualPathUtility.ToAbsolute("~/")));
      } catch (WebException webex) {
        // possible 'Unauthorized' - do nothing
      } catch (Exception ex) {
        Logger.Error(" PingServer error:", ex);
      }
    }
    ...
  }
 
The job MSMQJob is triggered in its own thread every 15 minutes:
...
13:45:00.0133 [Thread:Worker-2] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job started
13:45:01.0133 [Thread:Worker-2] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job ended
14:00:00.0133 [Thread:Worker-1] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job started
14:00:01.0133 [Thread:Worker-1] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job ended
14:15:00.0133 [Thread:Worker-4] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job started
14:15:01.0133 [Thread:Worker-4] EE.SmsFramework.Common.Logger[INFO]: QUARTZ: Scheduled Job ended
...


2 comments:

Unknown said...

Thanks a lot.

Unknown said...
This comment has been removed by the author.