Search code examples
asp.nettimershared-hostingjobsjob-scheduling

How do I run an accurate but (relatively) simple task scheduler on ASP.Net?


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?


Solution

  • 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.