Overview
A question recently came up in one of the forums concerning the DotNetNuke scheduler. Specifically, the user wanted to know if it was possible to get the scheduler to run a process at a fixed time (this time might be static or it might be set through some administrative UI). To answer this question, we first need to look at how the scheduler works.
Professional DotNetNuke 4 Chapter 8 provides some background information about programming the scheduler which will give us some context for our discussion.
"Because the Scheduler is run under the context of the web application, it is prone to the same types of application recycles as a web application. In a web-hosting environment, it is a common practice to conserve resources by recycling the worker process for a site periodically. When this happens, the Scheduler stops running. Therefore, the tasks run by the Scheduler do not run 24 hours a day, 7 days a week. They are executed according to a defined schedule, but they can only be triggered when the worker process is alive. For this reason, you cannot specify that a task should run every night at midnight. It is not possible in the web environment to meet this type of use case. Instead, you can specify how often a task is run by defining the execution frequency for each task. The execution frequency is defined as every x minutes/hours/days."
A key item to note from this paragraph: The scheduler runs completely at the whim of IIS and ASP.Net. If IIS needs to shut down the worker process, for whatever reason, then the scheduler will strop running until the worker process restarts. What this means is that the work process may not be running at a specific time when your scheduled event is supposed to execute. The worker process will only restart if a request is made to the DotNetNuke application/website.
Solution
So given the limitations of the scheduler, how do we run a task at a fixed time? The trick to making sure a task runs on time is to keep the worker process alive. By default, ASP.Net will shutdown a worker process that has been inactive for 20 minutes. To keep this from happening we need to have requests coming into the website more frequently than every 20 minutes. On a busy site, this is not generally a problem. Less active sites will often resort to using some sort of keep-alive service that makes requests to the website every 10-15 minutes. This ensures that ASP.Net will not unload the worker process. ASP.Net is also configured by default to recycle the application pool about once every 23 hours. As long as you have a keep-alive service running, this should not pose any additional challenge.
So now that we have the scheduler running, we need to figure out how to have the scheduler run our task at a specific time. Because of the previous limitations, the DotNetNuke scheduler does not come with a UI for specifying a fixed execution time. By digging into the guts of the scheduler we can easily overcome this limitation.
When we create our scheduler task, we inherit from DotNetNuke.Services.Scheduling.SchedulerClient. The standard constructor, as shown in Chapter 8, takes a ScheduleHistoryItem as a parameter, which is then saved to the SchedulerClient.
Public Sub New(ByVal objScheduleHistoryItem As DotNetNuke.Services.Scheduling.ScheduleHistoryItem)
MyBase.new()
Me.ScheduleHistoryItem = objScheduleHistoryItem
End Sub
Looking at the class diagram, we see that ScheduleHistoryItem inherits from ScheduleItem. ScheduleItem includes the TimeLapse property that tells the scheduler the number of time units to wait until the next scheduled execution. We can use this property to control when our task executes next.
When we are through performing our work in the DoWork method of our SchedulerClient, we will normally set the objScheduleHistoryItem.Succeeded property to true. This signals the scheduler framework that our work is complete and that it succeeded. The scheduler will use this information to set the next time to execute our task. This is accomplished in the WorkCompleted method of the CoreScheduler class. Buried in this method is the key to our workaround (the relevant portions of the code is shown below).
Shared Sub WorkCompleted(ByRef objSchedulerClient As SchedulerClient)
...
Dim objScheduleHistoryItem As ScheduleHistoryItem
objScheduleHistoryItem = objSchedulerClient.ScheduleHistoryItem
...
Select Case objScheduleHistoryItem.TimeLapseMeasurement
Case "s"
objScheduleHistoryItem.NextStart = objScheduleHistoryItem.StartDate.AddSeconds(objScheduleHistoryItem.TimeLapse)
Case "m"
objScheduleHistoryItem.NextStart = objScheduleHistoryItem.StartDate.AddMinutes(objScheduleHistoryItem.TimeLapse)
Case "h"
objScheduleHistoryItem.NextStart = objScheduleHistoryItem.StartDate.AddHours(objScheduleHistoryItem.TimeLapse)
Case "d"
objScheduleHistoryItem.NextStart = objScheduleHistoryItem.StartDate.AddDays(objScheduleHistoryItem.TimeLapse)
End Select
...
CoreScheduler.UpdateScheduleHistory(objScheduleHistoryItem)
Dim objEventLogInfo As New LogInfo
...
End Sub
Notice that the ScheduleHistoryItem.TimeLapse of our task is saved back to the database. This basically means that by calculating the timelapse in our SchedulerClient, and setting this value before we return from completing our work that we can control when the scheduler will next execute our task. So after setting Succeeded, I just add another line of code to set the timelapse value.
The only step left is to include a method for determining the proper timelapse value. This is a method I used on a previous project to run a task every morning at 6am. It should not be too much of a stretch for you to come up with a method to read a ScheduleItemSettings property to calculate the correct timelapse. (NOTE: My example assumes that the TimeLapseMeasurement is set to minutes, hence the use of TicksPerMinute)
Private Function GetElapsedTimeTillNextStart() As Integer
Dim Tomorrow As DateTime = DateTime.Today.AddDays(1)
Dim nextStart As New DateTime(Tomorrow.Year, Tomorrow.Month, Tomorrow.Day, 6, 0, 0)
Dim elapseMinutes As Integer = (nextStart.Ticks - DateTime.Now.Ticks) \ TimeSpan.TicksPerMinute
Return elapseMinutes
End Function
One final word of caution: This method is not guaranteed to run your process at an exact time. It may fluctuate by a couple minutes depending on your keep-alive duration, other currently executing tasks, and several other factors. For my purposes, it was enough to know that my task would run every day at approximately 6am. If you need your task to run at an exact time, then you should probably look at a solution that uses an external scheduler and avoids the ASP.Net pipeline.