Task Scheduler
Pattern
Introduction
Sitefinity comes with a background scheduler service which is really handy for peridoic processes. A design I have regular used is to actually create two tasks for every one job. Sound silly? Let me tell you what I do.
When I first started using scheduled tasks I would occasionally run into an issue where it stopped running. The issue was that the task had run and for some reason, it had errored and because of that error it needed to be cleared and rescheduled. (True, it may have been a fault in my implementation of the scheduled task.)
I also came across another issue. I had a task that would loop through a data set of which each data item would not always succeed. (External source issues) I didn't want to ignore those failures as they needed to be rerun, but I also didn't want to rerun the whole set, (a 1-hour job), just the failures.
And so
What I did and now do for all scheduled jobs is to create two. One worker job which does the work and is not scheduled. The second is a scheduled job and all it does is call and execute the worker job. This meant that if my worker job failed I had a record
of it stored in the table and it never interfered with my scheduler job that would always run again at eth required time.
In my example above, I created one job that was scheduled to run once a day. It would call and get a list of all the data item ids and in a loop schedule the worker job passing in the id of the item it was to process. The scheduled job is so simple it is highly unlikely to fail and was quick to run. The worker jobs scheduled would work independently and allow me to query if they succeeded or not, (allowing me to know which ones needed to be rerun) and also allowed the server to restart during this time as the jobs would just carry on processing after the restart. The code samples below will hopefully make sense of all this.
I still do this for simpler jobs. It might be over the top but I like to stick to consistancy.
Make sure you check out the Sitefinity documentation first as I don't present the entire classes code here for berevity sake.
At the top I create some static items that I can reuse.
public readonly static String TaskKey = "7DFCF662-DBE9-42EF-A268-6FDBE2C05474"; public static String TaskTitle = "Scheduled Task Name";
Next is a method to schedule my task. This is registered in my application startup code. One thing I can do here is check the environment and change the schedule if I like or not run it at all in that environment. In the sample here I have the cron expression hardcoded but you can move this to configuration. I do this and along with the Unschedule method allow the user to manage the actual scheduling of the job for them selves by providing a backend page with buttons that call these methods.
Take note of the GetNextOrrurance() method. This ensures the job is correctly scheduled for the next run time when resheduling happens.
public static void ScheduleTask() { SchedulingManager schedulingManager = SchedulingManager.GetManager(); UnScheduleTask(); var schedule = NCrontab.CrontabSchedule.TryParse("1 * * * *"); if(!schedule.IsError && schedule.HasValue) { DateTime nextOccurence = schedule.Value.GetNextOccurrence(DateTime.UtcNow); ScheduleRoomUpdates newTask = new ScheduleRoomUpdates() { ExecuteTime = nextOccurence, ScheduleSpec = "1 * * * *", ScheduleSpecType = "crontab", Title = TaskTitle, Description = "Scheduled Update" }; schedulingManager.AddTask(newTask); schedulingManager.SaveChanges(); } }
The unschedule method.
public static void UnScheduleTask() { SchedulingManager schedulingManager = SchedulingManager.GetManager(); IQueryable<ScheduledTaskData> tasks = SchedulingManager.GetManager().GetTaskData(); var task = tasks.Where(t => t.Title == TaskTitle).FirstOrDefault(); if (task != null) { Scheduler.Instance.StopTask(task.Id); schedulingManager.DeleteTaskData(task); schedulingManager.SaveChanges(); } }
And the actual work method. My data set had about 1000 items and so to avoid stressing out the database I space out each job by incrementing the execute time.
public override void ExecuteTask() { //Get Data removed for brevity Int32 count = 1; foreach (var itemId in itemIds) { WorkerJobTask.ScheduleTask(DateTime.UtcNow.AddSeconds(count * 10), itemId); count++; } }
The worker task's Scheduler() method takes a execute parameter but otherwise is straight forward.
public static void ScheduleTask(DateTime executeTime) { SchedulingManager schedulingManager = SchedulingManager.GetManager(); UpdateWork newTask = new UpdateWork() { ExecuteTime = executeTime, Title = TaskTitle, Description = "Update Work Task" }; schedulingManager.AddTask(newTask); schedulingManager.SaveChanges(); }
Client Extra
Above I mentioned the ability to check and rerun failed jobs as well as allowing the client to stop and reschedule the jobs themseleves.
I created a backend page for the client and provided a few inputs and buttons allowing them to pass in item ids and thus schedule and execute the indivdual worker jobs for them seleves. So that they could know of any failures I also provide a table that called a API to query the Sitefinity task table and report the status back to them. Here is some code I used to get and return a JSON result back.
List<Schedule> results = new List<Schedule>(); String conn = System.Configuration.ConfigurationManager.ConnectionStrings["Sitefinity"].ConnectionString; using (SqlConnection connection = new SqlConnection(conn.Replace("Backend=mssql", String.Empty))) { connection.Open(); using (SqlCommand command = new SqlCommand("SELECT [title],[execute_time],LEFT([status_message], 150) AS status_message,[progress] FROM [dbo].[sf_scheduled_tasks] WHERE [title] LIKE '%MyTitles%' ORDER BY [execute_time]", connection)) { using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { results.Add(new Schedule() { Title = reader["title"] != DBNull.Value ? (String)reader["title"] : String.Empty, ExecuteTime = ((DateTime)reader["execute_time"]).ToString("ddd MMM dd hh:mm:ss tt"), Status = reader["status_message"] != DBNull.Value ? (String)reader["status_message"] : String.Empty, Progress = (Int32)reader["progress"] }); } } } }
On my backend client widget I had a piece of JavaScript that polled this method every 15 seconds and updated a HTML table with the results. This allowed them to see all currently scheduled jobs as well as spot any that had errored.
If you notice, there is a progress field. When you are running the Excute() method in your scheduled task you can call the UpdateProgress() method passing in a Int32 of between 0 and 100 to indicate how far along the task is. This was really helpful for long running tasks. My client was able to see that the job was doing something and progressing along. Though it isn't real time it was great visual feedback.
UpdateProgress(5, "Starting");
Thanks for reading and feel free to comment - Darrin Robertson
If I was really helpful and you would buy me a coffee if you could, yay! You can.
Make a Comment