Click here to Skip to main content
Click here to Skip to main content
Go to top

Scheduling Recurring Tasks with Revalee and MVC

, 23 May 2014
Rate this:
Please Sign up or sign in to vote.
An example of requesting a daily report after a long delay from an MVC controller.

Introduction

How many times have you read about someone wanting to run a regularly occurring task within their web application? If you've been reading the online discourse long enough, then the answer is: a lot. And by “a lot”, I mean it's a daily question.

So what's the answer, you ask? Revalee.

Come again? That's Revalee and it's pronounced like the word reveille, as in “a signal to arise.”

Background

Revalee is Windows Service that you use in conjunction with your web application. In most cases (although this is certainly not a requirement), you would install the Revalee Service on the same server as your web hosting environment (IIS). Next, you use the Revalee client libraries within your ASP.NET application (whether MVC or not) to communicate to Revalee. The best part, Revalee is a free, open-source project.

How does it work? To keep it brief: Revalee (as a Windows Service) listens for callback requests from your web application. (There are authentication and verification steps to make sure no one attempts to spoof your requests too, but this is the 30,000-foot overview so we'll move right along.) Revalee then logs your application's callback request, which in a nutshell can be interpreted as “Hey, Revalee, call my URL back at this date & time.” Later, at the indicated date & time, Revalee calls back your web application just like any web application user might do from their browser. That's it.

What all this means is that you can keep your application's business logic all contained in one place. No more having to split your application's functionality into various chunks, including some hard-to-maintain ones. Some code lives in your web code, some in archaic command line routines used by the task scheduler, and even, some in your database's job scheduler. How is all that maintained? Who configures it? Is all of that code checked into your source code repository? Is the configuration all documented? We have all experienced this. Hopefully, Revalee can help you solve some of this.

For more background information on Revalee and how it works under the hood, take a look at these previous articles: Scheduling tasks with Revalee and MVC and Scheduling tasks with Revalee and MVC (Part 2). Those previous articles focused on scheduling one-time callbacks with Revalee (think: send a future email to a particular user). This article will now focus on scheduling recurring callback requests.

Disclaimer: Revalee is a free, open source project written by the development team that I am a part of. It is available on GitHub and is covered by the MIT License. If you are interested, download it and check it out.

Recurring Tasks

So you have your new ASP.NET MVC application written, it's been tested & deployed, and users are finally making good use of it. Great! However, as expected, the first post-launch enhancement request (aka. Phase 2) arrives mere moments after the application went into production: “We need a high-level, summary report emailed to the business team nightly.” It's not an unreasonable request. It is time, however, to use Revalee.

First of all, let's prep the web server (IIS) to work with Revalee and recurring tasks. To do this, you will need to add some elements to your application's web.config. You'll start by defining a new section (<revalee>) in the configuration file:

<configuration>
    <configSections>
        <section name="revalee" 
        type="Revalee.Client.Configuration.RevaleeSection" requirePermission="false" />
        ...
    </configSections>
    ...
</configuration>

Next, you will add the contents of the new <revalee> configuration section. The details included in this configuration section define where Revalee is installed, how it is configured, and what the details of your recurring task (or tasks) will be:

<revalee>
    <clientSettings serviceBaseUri="http://localhost:46200" 
    authorizationKey="YOUR_SECRET_KEY" />
    <recurringTasks callbackBaseUri="http://yourwebapp.com">
        <task periodicity="daily" hour="05" 
        minute="00" url="/Report/DailySummary" />
    </recurringTasks>
</revalee>

Finally, you include a custom module that will make use of the configuration information listed above, known as the RecurringTaskModule.

<system.webServer>
    <modules runAllManagedModulesForAllRequests="false">
        <add name="RevaleeRecurringTasks"
             type="Revalee.Client.RecurringTasks.RecurringTaskModule, Revalee.Client"
             preCondition="managedHandler" />
    </modules>
</system.webServer>

Based on the <task> element listed in the example code above, your web application will now self-register a recurring Revalee callback to http://yourwebapp.com/Report/DailySummary every day at 5:00 AM. Simply put, this means that your MVC application's ReportController will be running its DailySummary() method every day at 5:00 AM. All of the business logic has now been funneled into the DailySummary() method. That's it: no fuss, no muss.

Under the Hood

So how does the RecurringTaskModule work? Rendered to its simplest form, this IHttpModule implementing class operates as follows:

  1. IIS launches the web application and loads the RecurringTaskModule, which is a class implementing the IHttpModule interface.
  2. Next, the LoadManifest() method reads the configured details defined in the <revalee> section of the web.config.
  3. After validating the configuration, a “heartbeat” callback is scheduled with Revalee to run at DateTimeOffset.Now.
  4. The Revalee (Windows) Service receives and performs the “heartbeat” callback immediately.
  5. The RecurringTaskModule's BeginRequest event is triggered and the incoming “heartbeat” callback (HttpRequest) is analyzed.
  6. The “heartbeat” HttpRequest triggers all recurring tasks (loaded from the web.config) to be scheduled with the Revalee Service.
  7. The module waits to process future, incoming recurring task requests.

If the analysis indicates that the HttpRequest is, in fact, a recurring task, then the request is intercepted (that is, HttpApplication.CompleteRequest() is called); otherwise, the request continues on through the normal HttpRequest processing pipeline.

You may be wondering: why are recurring tasks scheduled every time the web application loads (and if you weren't wondering about that, you are now)? Won't this result in a recurring task being scheduled multiple times? The simple answer is: yes. (And that's OK.) When the RecurringTaskModule receives an HttpRequest for a recurring task, it uses the first such request received to process the recurring task and then ignores all other instances of incoming requests for the same recurring task. To identify duplicate tasks, each task is identified by a hash computed from its constituent properties. Thus, two tasks cannot share the exact same details, namely, periodicity, hour offset, minute offset, and callback URL.

IHttpModule.BeginRequest

Generating your own IHttpModule may be something that you need to do for a future project, so let's delve into the handling of the BeginRequest event (just one aspect of the IHttpModule to be sure, but not an insignificant one):

private void context_BeginRequest(object sender, EventArgs e)
{
    HttpApplication application = sender as HttpApplication;

    if (application != null && application.Context != null && application.Request != null && _Manifest != null)
    {
        HttpRequest request = application.Request;

        RequestAnalysis analysis = _Manifest.AnalyzeRequest(request);

        if (analysis.IsRecurringTask)
        {
            ConfiguredTask taskConfig;

            if (_Manifest.TryGetTask(analysis.TaskIdentifier, out taskConfig))
            {
                if (RevaleeRegistrar.ValidateCallback(new HttpRequestWrapper(request)))
                {
                    if (taskConfig.SetLastOccurrence(analysis.Occurrence))
                    {
                        application.Context.Items.Add(_InProcessContextKey, BuildCallbackDetails(request));
                        application.Context.RewritePath(taskConfig.Url.AbsolutePath, true);
                        _Manifest.Reschedule(taskConfig);
                        return;
                    }
                }
            }

            application.Context.Response.StatusCode = (int)HttpStatusCode.OK;
            application.Context.Response.SuppressContent = true;
            application.CompleteRequest();
            return;
        }
    }
}

The AnalyzeRequest() method (more on this later) determines whether (or not) an incoming HttpRequest is a recurring task. If it is recurring, the SetLastOccurrence() method is the final “gatekeeper” determining whether not an incoming recurring task request should be ultimately processed or ignored (because a duplicate request of this particular recurring task was already processed).

Recurring Task Periodicity

What levels of recurrence does Revalee support, you ask? Hourly and daily.

An hourly recurring task is defined as follows:

attribute value
periodicity "hourly"
minute Value between 0 and 59 (inclusive)
url Url of the callback target
<task periodicity="hourly" minute="45" url="/Report/HourlyUpdate" />

A daily recurring task is defined as follows:

attribute value
periodicity "daily"
hour Value between 0 and 23 (inclusive) [24-hour format]
minute Value between 0 and 59 (inclusive)
url Url of the callback target
<task periodicity="daily" hour="18" minute="15" url="/Report/DailySummary" />

Other levels of recurrence can be achieved by including additional <task> elements (for sub-hour recurrence) and/or custom processing at the Controller.Action() level (for super-daily recurrence). For example, to only process the requests on every Friday at 6:15 PM you might include the following code in your ReportController:

public ActionResult DailySummary()
{
    if (RecurringTaskModule.IsProcessingRecurringCallback)
    {
        if (DateTime.Now.DayOfWeek == DayOfWeek.Friday)
        {
            // Your once every Friday code goes here
        }
    }

    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

AnalyzeRequest()

So how do you prevent the processing of every, single incoming HttpRequest from bogging down your web application? Keep it fast and simple for the vast majority of requests. In this case, String.StartsWith() using the Ordinal string comparison is the gatekeeper. This will return false as soon as the first character does not match the value of _RecurringTaskHandlerAbsolutePath. Only those requests that are determined to be recurring tasks processed in a more thorough manner, à la:

internal RequestAnalysis AnalyzeRequest(HttpRequest request)
{
    string absolutePath = request.Url.AbsolutePath;

    if (absolutePath.StartsWith(_RecurringTaskHandlerAbsolutePath, StringComparison.Ordinal))
    {
        var analysis = new RequestAnalysis();
        analysis.IsRecurringTask = true;
        int parameterStartingIndex = _RecurringTaskHandlerAbsolutePath.Length;

        if (absolutePath.Length > parameterStartingIndex)
        {
            // AbsolutePath format:
            // task       -> ~/__RevaleeRecurring.axd/{identifier}/{occurrence}
            // heartbeat  -> ~/__RevaleeRecurring.axd/{heartbeatId}

            int taskParameterDelimiterIndex = absolutePath.IndexOf('/', parameterStartingIndex);

            if (taskParameterDelimiterIndex < 0)
            {
                // no task parameter delimiter

                if ((absolutePath.Length - parameterStartingIndex) == 32)
                {
                    Guid heartbeatId;

                    if (Guid.TryParseExact(absolutePath.Substring(parameterStartingIndex), "N", out heartbeatId))
                    {
                        if (heartbeatId.Equals(_Id))
                        {
                            this.OnActivate();
                        }
                    }
                }
            }
            else
            {
                // task parameter delimiter present

                if ((absolutePath.Length - taskParameterDelimiterIndex) > 1)
                {
                    if (long.TryParse(absolutePath.Substring(taskParameterDelimiterIndex + 1),
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out analysis.Occurrence))
                    {
                        analysis.TaskIdentifier = absolutePath.Substring(parameterStartingIndex,
                            taskParameterDelimiterIndex - parameterStartingIndex);
                    }
                }
            }
        }

        // If TaskIdentifier is not set the default will be "", which will be discarded by the HttpModule

        return analysis;
    }

    return NonRecurringRequest;   // A static return result.
}

Conclusion

Between the details in your web.config and the code in your ReportController, you've managed to keep all of your business logic encapsulated within your web application. That makes it easier to maintain long-term. As far as that nightly, summary report, all you had to do was write the DailySummary() method. Now that is simple and elegant. Nice!

Further Reading

History

  • [2014.May.19] Initial post.
  • [2014.May.19] Added 'Further Reading' section.
  • [2014.May.23] Amended 'Further Reading' section with UrlValidator, a Project Widget used by Revalee.

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

László Á. Koller
Software Developer (Senior) Sage Analytic
United States United States
A member of the Sage Analytic software development studio since 2000. Located in Northern NJ. Likes Formula 1, Star Wars, and other things. Enjoys a good laugh.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalVolynsky Alex23-May-14 12:07 
SuggestionI build the same a little simpler... PinmemberVadK22-May-14 14:51 
QuestionSQL Server PinmemberDewey20-May-14 2:52 
AnswerRe: SQL Server PinmemberLászló Á. Koller20-May-14 3:23 
QuestionRegarding Task scheduling in Web apps PinmemberTridip Bhattacharjee19-May-14 21:15 
AnswerRe: Regarding Task scheduling in Web apps PinmemberVaso00720-May-14 2:15 
GeneralRe: Regarding Task scheduling in Web apps PinmemberLászló Á. Koller20-May-14 3:54 
AnswerRe: Regarding Task scheduling in Web apps Pinmembergiammin20-May-14 3:40 
GeneralRe: Regarding Task scheduling in Web apps PinmemberLászló Á. Koller20-May-14 3:56 
GeneralRe: Regarding Task scheduling in Web apps Pinmembergiammin20-May-14 4:03 
GeneralRe: Regarding Task scheduling in Web apps PinmemberLuka23-May-14 5:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140921.1 | Last Updated 23 May 2014
Article Copyright 2014 by László Á. Koller
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid