Chrontab for Sitecore

Posted 12/20/2013 by mschultz

One requirement that always seems to come up in Sitecore implementations is to run a scheduled task at a specific time, so that the processing can happen outside of normal business hours. Sitecore does have a scheduling mechanism, however you can't specify that your job run at a specific time, only at a given frequency. There are at least a few ways to quickly create scheduled tasks that run in Sitecore at a specific time.

One way I've used is to create a web service that will run the task that you need done and then just use a windows scheduled task to call the web service at the right time. Another solution is to create a sitecore agent that wakes up every so often and checks if it's time to run the task again or go back to sleep.

Both of these methods get the job done, but with a little bit of extra of work we can create a framework that will let us schedule tasks for specific times, manage them within Sitecore and reuse our commands from Sitecore's existing task infrastructure.

Let's create the items that we need to handle chron scheduling. First I need a template that holds the attributes of a given ChronJob. Lets call it ChronJob.

  • Command - droplink with /sitecore/system/tasks/commands as the datasource
  • Schedule - single line text where you can put the schedule to follow
  • Last Run - this field will be used to track whether it's time to run the Command again.

Now we need a place to store our ChronJobs. For this, I've created a new folder Chron under /sitecore/system/tasks and added ChronJob to the insert options.

Now that we have the data model specified, let's tackle scheduling. If you're familiar with Unix then you've at least heard of Chron, a utility that schedules tasks in the unix environment. Chron has a compact format for specifying a recurring schedule that we can leverage for our module. Luckily, someone has already written the code to parse and check a schedule in Chron format. NChrontab, written by Atif Aziz, will parse a Chron expression and tell you the next occurrence time in that schedule based on a date. If you're unfamiliar with how a Chron expression works you find more info here.

I've created a utility class using NChrontab to deal with schedules below. It has two functions. First to parse an expression into a Schedule and second, to tell me if it's time to run the associated command. I've also extended the parsing to allow multiple schedules in the same definition by separating them with a | character. This allows maximum flexibility in defining schedules.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NCrontab;

namespace NTTData.ChronTab
{
    public class Schedule
    {
        private List<CrontabSchedule> scheduleList;

        private Schedule() 
        {
            scheduleList = new List<CrontabSchedule>();
        }
        
        public static Schedule Parse(string schedule)
        {
            Schedule sched = new Schedule();

            if (!string.IsNullOrEmpty(schedule))
            {
                string[] cronExpressions = schedule.Split(new string[] { "\r\n","\t","\n", "|" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string expr in cronExpressions)
                {
                    ValueOrError<CrontabSchedule> cs = CrontabSchedule.TryParse(expr.Trim());
                    if (!cs.IsError)
                    {
                        sched.scheduleList.Add(cs.Value);
                    }
                    else 
                    {
                        if (cs.Error != null)
                        {
                            Sitecore.Diagnostics.Log.Error("error parsing crontab expression: " + expr, cs.Error, typeof(Schedule));
                        }
                        else 
                        {
                            Sitecore.Diagnostics.Log.Error("error parsing crontab expression: " + expr, typeof(Schedule));                            
                        }
                    }
                }
            }

            return sched;
        }

        public DateTime GetNextOccurrence(DateTime baseTime) 
        {
            return GetNextOccurrence(baseTime, DateTime.MaxValue);
        }

        public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) 
        {
            DateTime dt = DateTime.MaxValue;

            foreach (CrontabSchedule cs in scheduleList)
            {
                DateTime next = cs.GetNextOccurrence(baseTime, endTime);
                if (next < dt) { dt = next; }
            }
            return dt;            
        }

        public IEnumerable<DateTime> GetNextOccurrences(DateTime baseTime, DateTime endTime) 
        {
            List<DateTime> occurenceList = new List<DateTime>();
            foreach (CrontabSchedule cs in scheduleList)
            {
                occurenceList.AddRange(cs.GetNextOccurrences(baseTime, endTime));
            }            
            occurenceList.Sort();
            return occurenceList;
        }
    }
}

The next step is to actually check the schedule and run a command on the specified schedule. In order to do this, I've created a Sitecore agent that will run a specific interval and check all the scheduled tasks in the Chron folder. We can reuse some of the code that Sitecore uses to run ScheduledTasks. Here's the source for the agent.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Reflection;
using Sitecore.Tasks;
using Sitecore.Jobs;

namespace NTTData.ChronTab
{
    public class ChronTabAgent
    {
        public const string ChronJobID = "{77C9D7FD-7C84-467F-B932-EFA60EE7F4F2}";

        Database Master;

        Item ChronFolder;

        public ChronTabAgent() 
        {
            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                Master = Factory.GetDatabase("master");

                ChronFolder = Master.GetItem(new ID("{D02BB5DC-FDA6-47B2-B56A-489D3FAA740C}"));
            }
        }

        public void Run() 
        {
            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                Sitecore.Diagnostics.Log.Info("ChronTab Agent checking tasks...",this);
                ProcessChildren(ChronFolder);
                Sitecore.Diagnostics.Log.Info("ChronTab Agent finished...",this);
            }
        }

        private void ProcessChildren(Item chronJob)
        {
            if (chronJob.TemplateID.ToString() == ChronJobID)
            {
                Schedule sched = Schedule.Parse(chronJob["Schedule"]);
                DateTime lastRun = Sitecore.DateUtil.ParseDateTime(chronJob["Last Run"], DateTime.MinValue);
                DateTime nextRun = sched.GetNextOccurrence(lastRun);
                if (nextRun < DateTime.Now)
                {
                    CommandItem ci = GetCommmandItem(chronJob);

                    if (ci != null)
                    {
                        JobOptions options = new JobOptions("Chron Job " + ci.ID.ToString(), "crontask", "scheduler", ci, "Execute", new object[] { new Item [] {}, new ScheduleItem(chronJob) });
                        options.AtomicExecution = true;
                        JobManager.Start(options);
                        using (new EditContext(chronJob))
                        {
                            chronJob["Last Run"] = Sitecore.DateUtil.ToIsoDate(DateTime.Now);
                        }
                    }
                }
            }
            else
            {
                foreach (Item c in chronJob.Children) 
                {
                    ProcessChildren(c);
                }
            }
        }

        private void ExecuteCommand(CommandItem ci)
        {
            throw new NotImplementedException();
        }

        private CommandItem GetCommmandItem(Item chronJob)
        {
            CommandItem ci = null;
            if (!string.IsNullOrEmpty(chronJob["Command"]) && ID.IsID(chronJob["Command"])) 
            {
                Item baseItem = Master.GetItem(new ID(chronJob["Command"]));
                if (baseItem != null) 
                {
                    ci = new CommandItem(baseItem);
                }
            }

            return ci;
        }

    }


    public class TestCommand 
    {
        public void TestCommand1()
        {
            Sitecore.Diagnostics.Log.Info("Running TestCommand", this);
            for (int i = 0; i < 60; i++)
            {
                System.Threading.Thread.Sleep(1000);
            }
        }            
    }
}

The code above is fairly simple. Each time the agent wakes up it iterates through each ChronJob item, checks it's schedule and executes it as a job if it's due. I should note that this code isn't terribly efficient and there are lots of possible improvements. However, for a reasonably small number of tasks that are not too close together it should work well.

The last thing that needs to be done to get this running is to configure the agent. Here's the code for this below as a sitecore web.config include.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">

  <sitecore>

    <scheduling>
    
      <agent type="NTTData.ChronTab.ChronTabAgent,NTTData.ChronTab" method="Run" interval="00:01:00"></agent>
  
    </scheduling>
  
  </sitecore>

</configuration>

And that's it. If you have questions or comments I can be reached at matthew_dot_schultz_at_nttdata_dot_com.

Share:

Archive

Syndication