Custom Recurring Scheduled Tasks in Sitefinity with Cron Jobs

by Vesselin Vassilev


Last Updated On Apr 2, 2018


CustomTaskProgress
Sometimes your Sitefinity web site needs to run a scheduled task on the server, and sometimes this scheduled task needs to run repeatedly, e.g. every day or every hour, or even every first Thursday of the month. 

Goal
This blog post will show you how to do that and also provide you with a nice little UI to monitor the progress of the running task.

Implementation 
Sitefinity provides an infrastructure for creating and managing custom scheduled tasks. 
This is done by the Scheduling Service for starting and managing the task and the abstract ScheduledTask class which provides basic properties like ExecuteTime, IsRunning and the abstract ExecuteTask() method. Normally, implementing the ScheduledTask class will give you the opportunity to run your task once - at ExecuteTime. 
So, how to make the task recurring? 

Enter Cron Jobs - "'Cron' is a time-based job scheduler in Unix-like operating systems (Linux, FreeBSD, Mac OS etc...). And these jobs or tasks are referred to as "Cron Jobs"."
(Source). 

Fortunately, Sitefinity support Cron jobs. To make a scheduled tab recurring, you need to:
- set its ScheduleSpecType property to "crontab"
- set its ScheduleSpec property to a cron expression. Cron expression determines how often and when the task is going to run. 

It consists of five parts:
- minute
- hour
- day of month
- month
- day of week

Example: 
50 11 * * * - means every day at 11:50AM
0 4 1,10 * * - means every 1st and 10th day of the month, at 4:00AM
20 4 * * SUN - means every Sunday at 4:20AM

A great tool to use for creating/testing cron expressions is https://crontab.guru

Now, let's have a look at the code.

First, we register our custom scheduled task in Global.asax.cs:

using SitefinityWebApp.Custom.ScheduledTasks;
using System;
using Telerik.Sitefinity.Services;
 
namespace SitefinityWebApp
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            SystemManager.ApplicationStart += SystemManager_ApplicationStart;
        }
 
        private void SystemManager_ApplicationStart(object sender, EventArgs e)
        {
            CustomScheduledTaskBase.RegisterCustomScheduledTasks();
        }
    }
}

What RegisterCustomScheduledTasks() method does, is to find all classes that implement the CustomScheduledTaskBase abstract class and add them if they do not already exist. This is useful if you have more than 1 implementation of the class.

public static void RegisterCustomScheduledTasks()
{
    using (SchedulingManager manager = SchedulingManager.GetManager())
    {
        List<ScheduledTaskData> allTasks = manager.GetTaskData().ToList();
 
        // get all defined custom Scheduled tasks in this assembly
        var scootTasks = typeof(CustomScheduledTaskBase).Assembly
                                            .GetTypes()
                                            .Where(t => t.IsSubclassOf(typeof(CustomScheduledTaskBase)) && !t.IsAbstract)
                                            .Select(t => (CustomScheduledTaskBase)Activator.CreateInstance(t));
 
        foreach (var task in scootTasks)
        {
            var taskData = allTasks.Where(t => t.TaskName == task.TaskName).ToList();
 
            if (taskData.Count == 0)
            {
                task.ScheduleCrontabTask();
            }
            else
            {
                foreach (var td in taskData)
                {
                    // delete failed or stopped tasks and those that have been running for
                    // more than 3 hours
                    if (
                        td.Status == TaskStatus.Failed || td.Status == TaskStatus.Stopped ||
                        (td.IsRunning && td.LastModified.AddHours(3) < DateTime.UtcNow)
                        )
                    {
                        manager.DeleteTaskData(td);
                        manager.SaveChanges();
                    }
                }
            }
        }
    }
}

public void ScheduleCrontabTask()
{
    using (var manager = SchedulingManager.GetManager())
    {
        var task = (CustomScheduledTaskBase)Activator.CreateInstance(this.GetType());
        task.Id = Guid.NewGuid();
        task.ScheduleSpecType = "crontab";
        task.Title = task.TaskName;
        task.ScheduleSpec = CrontabExpression;
        task.ExecuteTime = CrontabHelper.GetExecuteTime(CrontabExpression, scheduleSpecType);
        manager.AddTask(task);
        manager.SaveChanges();
    }
}

You can see here, that each task is registered with its CrontabExpression which will define when the task will run. 

But, CrontabExpression itself is not enough initially to determine the right ExecuteTime of the task, .e.g. if you task is to run at 11:50AM but at the time of registering the task it is already 11:55AM - then the ExecuteTime will be tomorrow at 11:50.
Similarly, if it was 11:40AM, then the ExecuteTime would be today at 11:50.

To get the proper execute time based on the current time and the cron expression, we use the GetExecuteTime method of the CrontabHelper. It will calculate when is the next run of the task, depending on the expression.
The full source code is included in the gist link at the end of this article.

Now, let's have a look at the concrete task implementation, which is pretty simple in this case:

using System;
using System.Threading;
 
namespace SitefinityWebApp.Custom.ScheduledTasks
{
    public class DemoScheduledTask : CustomScheduledTaskBase
    {
        public override string CrontabExpression
        {
            get
            {
                // you can read the value from a custom config section
 
                // execute the task every day at 11:50AM
                return "50 11 * * *";
            }
        }
 
        protected override void ExecuteTheTask()
        {
            for (int i = 1; i <= 100; i++)
            {
                // this is useful if you are going to provide a UI that shows the current task progress
                this.UpdateProgress(i, "Doing stuff {0}".Arrange(i));
         
                // do your stuff...
                Thread.Sleep(2000);
            }
        }
    }
}

The task is not very fun - it just loops through 1 to 100 and updates its progress and then sleeps for 2s. 

The updateProgress part is optional, but it is great if you know roughly how long your task is going to run or how many items it is going to process, etc. 

Next, let's see how we can show a progress bar of the status of the running task.

We'll create a new MVC widget - the controller will do nothing, just return a view, the view will do all the work, which is client-side. 
For our demo purposes, we will add a Start button, that will manually start the task so that we can test the progress bar (we don't want to wait for 11:50AM which is how we set our task to run.

<div class="row">
    <label>
        You can manually start the task by clicking the button below
    </label>
    <div>
        <button class="sfLinkBtn sfSave" onclick="return start();">Manual Start Task</button>
    </div>
</div>
<div class="row" style="margin-top:50px;">
    <div id="progressbar"></div>
    <div id="status" style="margin-top:30px;"></div>
</div>


<script>
    var $progress = null;
    var $status = $("#status");
    var _intervalHandle = null;
 
    $(function () {
 
        // .container is the wrapper class of the parent Layout widget
        var $editDiv = $(".container");
        if ($editDiv.parent().hasClass("sfHeader")) {
            // edit div is in the wrong place for some reason in SF 10.1
            // we need to move it out of sfHeader, to become a sibling instead of child
            $editDiv.insertAfter(".sfHeader");
        }
 
        // init kendo
        $progress = $("#progressbar").kendoProgressBar({
            type: "percent"
        }).data("kendoProgressBar");
 
        // in case user refreshes the page
        beginPolling();
 
    })
 
 
    function start() {
 
        $.ajax({
            url: window.location.pathname + "/startScheduledTask?taskName=SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask",
            type: "GET"
        })
        .done(function (data) {
            if (data == "Success") {
                alert("The task has started successfully, you can check its status below");
                beginPolling();
            }
        })
        .fail(function (jqXHR, textStatus) {
            console.log(jqXHR);
            alert(jqXHR.responseText);
        });
 
        return false;
    }
 
    function beginPolling() {
        refreshProgressBar();
 
        _intervalHandle = window.setInterval(function () {
            refreshProgressBar();
        }, 1500);
    }
 
    function _removeHandlers() {
        if (_intervalHandle) {
            window.clearInterval(_intervalHandle);
            _intervalHandle = null;
        }
    }
 
    function refreshProgressBar() {
        // get task progress from Sitefinity Scheduling Service
        $.ajax({
            url: "/Sitefinity/Services/SchedulingService.svc/taskName/SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask/progress?providerName=",
            type: "GET"
        })
        .done(function (data) {
            $progress.value(data.ProgressStatus);
 
            if (data.StatusMessage)
                $status.text(data.StatusMessage);
            else
                $status.text("");
 
            if (data.ProgressStatus == 100 || data.Status != 1) {
                _removeHandlers();
            }
        })
    }
</script>


Now, if we start the task, we will see this:

TaskProgress

To get the current status of the task we use the SchedulingService - we just pass the name of our task, like this:

/Sitefinity/Services/SchedulingService.svc/taskName/SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask/progress?providerName=
The json result provides a ProgressStatus and StatusMessage (the ones that we set via UpdateProgress server method).


Full source code is here: 
https://gist.github.com/VesselinVassilev/89b04f4ce7cef2610590643bafe089ca






Copyright © Sitefinity Development