There are many posts on running scheduled tasks on ASP.Net. Most involve using the HTTP cache expiry (which is not really an elegant solution) and not very reliable or very accurate.
Others recommend that the best way is to have a service running on the server designed explicitly for this. But what about users who are running on shared hosting and don't have the privilege to run service?
Last year, I found some posts on Stack Overflow to implement a Registered object as a class and implement that to run scheduled tasks. I expanded on that solution and came up with a configurable timer running on a separate thread to run scheduled tasks very well.
This has worked very well in production for the last two years. I'd like to post this solution so it can help someone... Of course, maybe someone can help improve it too...
Here goes - First, here is the main Job class... (And yes, I do prefer VB.NET)
Imports System.Web.Hosting
Public Class JobHost
Implements IRegisteredObject
Dim _ShutDown As Boolean
Property Running As Boolean
Property JobTimer As Threading.Timer
ReadOnly Property ShuttingDown As Boolean
Get
Return _ShutDown
End Get
End Property
Public Sub New()
HostingEnvironment.RegisterObject(Me)
End Sub
Public Sub InitTimer(callback As Threading.TimerCallback, Start As Integer, period As Integer)
_JobTimer = New Threading.Timer(callback, Nothing, Start * 1000, period * 1000)
End Sub
Public Sub [Stop](immediate As Boolean) Implements Web.Hosting.IRegisteredObject.Stop
_ShutDown = True
If _JobTimer IsNot Nothing Then _JobTimer.Dispose()
_JobTimer = Nothing
If immediate Then
HostingEnvironment.UnregisterObject(Me)
Else
If Not _Running Then HostingEnvironment.UnregisterObject(Me)
End If
End Sub
End Class
The above class implements a function called InitTimer
that takes a callback parameter as well as Start and Period parameters. This is in seconds - but you can change it to millisec by removing the 1000 multiplier.
Next, in our Global.asax
Application Start event, we initialize the class and create a Callback method that in turn fires methods at various intervals.
I've left a lot of original code and method calls for the purpose of illustration - you can remove them all. As you can see, I am also logging how long each group of tasks takes.
<%@ Application Language="VB" %>
<script runat="server">
Private CurMin As Integer = 0
Private iJob As JobHost, il_Start As Long
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
LogAction("System", "------ App Start ------", String.Empty, True)
RegisterRoutes(Routing.RouteTable.Routes)
WorkOrderClass.UpdateClosedWO()
ConfigClass.LoadAppConfig()
ClearActionLog(2) ' Keep only 2 months of action logs
InitPDF()
If ConfigurationManager.AppSettings("EnableJobs") IsNot Nothing Then
LogAction("System", "Jobs Enabled", "On Server: " & Environment.MachineName, True)
DeleteTempFiles()
MailSystemLog()
DocumentClass.EnsureDocFolder()
WorkOrderClass.UpdateAbandoned()
DeleteEventsData()
iJob = New JobHost
iJob.InitTimer(AddressOf TimerCallback, 60, 10)
End If
End Sub
Sub TimerCallback(state As Object)
If iJob.Running OrElse iJob.ShuttingDown Then Exit Sub ' Exit if previous job is already running or if application is shutting down
If Now.Minute <> CurMin AndAlso DAL.ConnectionAvailable Then ' Only perform tasks when minute changes and Database is available
' Set flag and culture
iJob.Running = True
Dim li_Hr As Integer = Now.ToUniversalTime.Hour
Threading.Thread.CurrentThread.CurrentCulture = New Globalization.CultureInfo("en-US") ' Is needed to change thread culture from server culture
CurMin = Now.ToUniversalTime.Minute
Timer1min() ' Fire tasks that must run every minute
If CurMin Mod 5 = 0 Then Timer5min() ' Fire tasks that must run every 5 minutes
If CurMin Mod 30 = 0 Then Timer30min() ' 30 min tasks
If li_Hr Mod 6 = 0 And CurMin = 2 Then Timer6hrs() ' 6 hourly tasks at x:02 to keep prevent same time as 5 min and 30 min tasks
If li_Hr = 5 And CurMin = 4 Then Timer5Z() ' Tasks at 5:04am
iJob.Running = False
End If
End Sub
' 1 Min tasks
Private Sub Timer1min()
WorkOrderClass.CalcBatch()
End Sub
' 5 Min tasks
Private Sub Timer5min()
il_Start = Now.Ticks
UpdateUsersOnline()
POP3.CheckMail()
LogAction("System", "Timer5min End", "Duration: " & Format((Now.Ticks - il_Start) / 10000, "#,##0") & "ms", True)
End Sub
' 30 min tasks
Private Sub Timer30min()
il_Start = Now.Ticks
WorkOrderClass.CheckStatusUpdates()
WorkOrderClass.EmailAlerts()
CusRepClass.SendReports()
AdvRepClass.SendReports()
SeqClass.TrackCont()
SeqClass.SendUpdates()
CusRepEntClass.CleanLog()
WorkOrderClass.CreateNotify(1)
LogAction("System", "Timer30min End", "Duration: " & Format((Now.Ticks - il_Start) / 10000, "#,##0") & "ms", True)
End Sub
Private Sub Timer6hrs()
il_Start = Now.Ticks
WorkOrderClass.UpdateAbandoned()
LogAction("System", "Timer6hr End", "Duration: " & Format((Now.Ticks - il_Start) / 10000, "#,##0") & "ms", True)
End Sub
Private Sub Timer5Z() ' at 5am UTC
DemClass.SendNotifications()
DeleteTempFiles()
WorkOrderClass.CreateNotify(2) ' 2=Daily notification
WorkOrderClass.SendAbandoned(False)
If Now.ToUniversalTime.Date.DayOfWeek = DayOfWeek.Sunday Then WorkOrderClass.SendAbandoned(True)
End Sub
' Other methods in Global.asax removed...
I've tried to put as many comments as possible. As you can see, the timer fires the first time after 60 seconds (gives enough time for the app to start-up and finish initialization methods) and then fires every 10 seconds thereafter.
If a task is already running when the timer is fired, then no new task is started - although you can change this if you like.
Of course, you need to keep your ASP.Net application alive when running tasks. Any external service that periodically pings a page on your site will keep it alive.