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:
- IIS launches the web application and loads the
RecurringTaskModule
, which is a class implementing the IHttpModule
interface.
- Next, the
LoadManifest()
method reads the configured details defined in the <revalee>
section of the web.config
.
- After validating the configuration, a “heartbeat” callback is scheduled with Revalee to run at
DateTimeOffset.Now
.
- The Revalee (Windows) Service receives and performs the “heartbeat” callback immediately.
- The
RecurringTaskModule
's BeginRequest
event is triggered and the incoming “heartbeat” callback (HttpRequest
) is analyzed.
- The “heartbeat”
HttpRequest
triggers all recurring tasks (loaded from the web.config) to be scheduled with the Revalee Service.
- 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)
{
}
}
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)
{
int taskParameterDelimiterIndex = absolutePath.IndexOf('/', parameterStartingIndex);
if (taskParameterDelimiterIndex < 0)
{
if ((absolutePath.Length - parameterStartingIndex) == 32)
{
Guid heartbeatId;
if (Guid.TryParseExact(absolutePath.Substring(parameterStartingIndex), "N", out heartbeatId))
{
if (heartbeatId.Equals(_Id))
{
this.OnActivate();
}
}
}
}
else
{
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);
}
}
}
}
return analysis;
}
return NonRecurringRequest; }
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.