Using The Task Framework

In some projects you may encounter a need to execute some long running task. This can be challenging in a Web application where things are generally meant to be stateless and request/response oriented. Typically you will want to spin the long running task off on a new thread so it doesn't block the page from rendering. However, any thread that is executing may be killed if the web application pool is recycled for any reason so it poses a challenge when you want to be sure the task gets completed.

One example where I encountered this was in developing the newsletter feature. I was thinking about what would happen if the list size grows very large, say 10,000 or 100,000 subscribers. Sending those emails will take a long time and its very likely that the application may be recycled and kill my thread, so I needed a way to make it possible for my task to resume after the application restarts and continue sending to the rest of the list. So, part of the solution is to keep track of progress so that I know which ones I've already sent and part of the solution is having a mechanism to find and restart unfinished tasks. I also wanted to be able to re-use the solution pattern for any other similar long running tasks I may need in the future so I implemented a task framework that can be used for this type of thing.

This is kind of an advanced thing so I'm going to talk about the high level steps and point you to example code that you can study to see how to leverage the task framework for your own long running task needs.

The basic strategy is that you develop a task that implements ITaskQueueTask (see ITaskQueueTask.cs).

This interface defines a property CanResume indicating if the task can be resumed. It is up to you to implement the internals of your task, ideally you will implement it such that it can be resumed but it may not always be possible. It can only work if the task is able to know what it has already done and what is remaining to do. The interface also defines an UpdateFrequency property which indicates how frequently the task will report its status. If the task has already started but has not reported progress within its UpdateFrequency we may conclude that the task stalled or was killed either by an error or by the application being recycled and killing the thread.

A good example of a long running task to study is the LetterSendTask.cs. Once you have implemented your task the idea is that you queue it to the database by serializing the task with all of its properties into the mp_TaskQueue table. You do this in the QueueTask method defined by ITaskQueueTask. Look in the LetterEdit.aspx.cs btnSendToList_Click method for an example as shown here:

LetterSendTask letterSender = new LetterSendTask
{
	SiteGuid = siteSettings.SiteGuid,
	QueuedBy = currentUser.UserGuid,
	LetterGuid = letter.LetterGuid,
	UnsubscribeLinkText = Resource.NewsletterUnsubscribeLink,
	UnsubscribeUrl = SiteRoot + "/eletter/Unsubscribe.aspx",
	NotificationFromEmail = siteSettings.DefaultEmailFromAddress,
	NotifyOnCompletion = true,
	NotificationToEmail = currentUser.Email,
	StatusCompleteMessage = Resource.NewsletterStatusCompleteMessage,
	StatusQueuedMessage = Resource.NewsletterStatusQueuedMessage,
	StatusStartedMessage = Resource.NewsletterStatusStartedMessage,
	StatusRunningMessage = Resource.NewsletterStatusRunningMessageFormatString,
	TaskName = string.Format(CultureInfo.InvariantCulture, Resource.NewsletterTaskNameFormatString, letterInfo.Title),
	NotificationSubject = string.Format(CultureInfo.InvariantCulture, Resource.TaskQueueCompletedTaskNotificationFormatString, letterSender.TaskName),
	TaskCompleteMessage = string.Format(CultureInfo.InvariantCulture, Resource.TaskQueueCompletedTaskNotificationFormatString, letterSender.TaskName)
};

letterSender.QueueTask();

WebTaskManager.StartOrResumeTasks();

So basically, after you serialize your task into the database using .QueueTask(), you call WebTaskManager.StartOrResumeTasks();.

WebTaskManager is also actually an ITaskQueueTask, its job is to monitor the status of other tasks, start them if the need to be started, resume them if they need to be resumed, etc. It must make sure there is only one instance of WebTaskManager running at a time and it must determine if other tasks are already running or are stalled and need to be restarted. Tasks report their progress by updating their own row in the mp_TaskQueue table, so the WebTaskManager checks if they have updated within the bounds of their UpdateFrequency, if not and the task CanResume, it reconstitutes the task from its serialized state and calls ResumeTask() on it. If you want to study WebTaskManager.cs its located in Web/Components folder. WebTaskManager.StartOrResumeTasks() is also called from application_start in Global.asax.cs which is what allows resumable tasks to be started again if they were killed by an application pool recycle or anything else that killed the thread.

A very important thing to know when implementing your own tasks is that when executing on a background thread there is no HttpContext, HttpContext.Current will always be null so you can not get any information from HttpContext. So any information your task needs must be stored within the properties of your task and serialized into the database so that you have all the information needed to run your task when it gets deserialized. I think it is very advisable to use a lot of error logging inside your task because its very difficult to know what happening on a thread running in the background.

There is also a place where you can monitor the progress of tasks under Administration Menu > Task Queue

Last Modified by Elijah Fowler on Oct 13, 2021