使用 Revalee 和 MVC 安排定期任务






4.75/5 (11投票s)
一个 MVC 控制器在长时间延迟后请求每日报告的示例。
引言
您读过多少次关于有人想在他们的 Web 应用程序中运行定期任务的故事?如果您阅读在线讨论足够长的时间,那么答案是:很多。当我说“很多”时,我指的是每天都会出现这个问题。
那么,您会问,答案是什么?*Revalee*。
再说一遍?那是 Revalee,它的发音与*reveille*(起床号)一词相同,意思是“唤醒的信号”。
背景
Revalee是一个 Windows 服务,您需要将其与您的 Web 应用程序结合使用。在大多数情况下(尽管这绝对不是必需的),您会将 Revalee 服务安装在与您的 Web 托管环境 (IIS) 相同的服务器上。接下来,您将在 ASP.NET 应用程序(无论是 MVC 还是其他)中使用 Revalee 客户端库与 Revalee 进行通信。最棒的是,Revalee 是一个免费的开源项目。
它是如何工作的?简单来说:Revalee(作为一个 Windows 服务)会监听来自您 Web 应用程序的回调请求。(还会有身份验证和验证步骤,以确保没有人试图伪造您的请求,但这只是 30,000 英尺的概览,所以我们将继续前进。)Revalee 然后会记录您应用程序的回调请求,这可以概括为“嘿,Revalee,请在指定日期和时间回调我的 URL。”之后,在指定的日期和时间,Revalee 会像任何 Web 应用程序用户从浏览器中可能做的那样,回调您的 Web 应用程序。就是这样。
这意味着您可以将应用程序的业务逻辑全部集中在一个地方。不再需要将应用程序的功能分成各种零散的部分,包括一些难以维护的部分。一些代码驻留在您的 Web 代码中,一些驻留在任务计划程序使用的旧式命令行例程中,甚至还有一些驻留在您数据库的任务计划程序中。所有这些是如何维护的?谁来配置?所有这些代码是否都已签入您的源代码存储库?所有配置都已记录在案吗?我们都经历过这些。希望 Revalee 能帮助您解决其中一些问题。
有关 Revalee 的更多背景信息以及它在后台的工作原理,请参阅以下之前的文章:使用 Revalee 和 MVC 调度任务 和 使用 Revalee 和 MVC 调度任务 (第 2 部分)。那些之前的文章侧重于使用 Revalee 安排一次性回调(例如:向特定用户发送未来的电子邮件)。本文将重点介绍安排重复回调请求。
免责声明:Revalee是我所在开发团队编写的一个免费的开源项目。它在GitHub上可用,并受MIT 许可证的保护。如果您有兴趣,请下载并查看。
重复任务
您的新的 ASP.NET MVC 应用程序已经编写完成,经过测试并已部署,用户们终于充分利用它了。太棒了!然而,正如预期的那样,第一个发布后的增强请求(又名第二阶段)在应用程序上线后不久就来了:“我们需要每天晚上向业务团队发送一份高级摘要报告。”这并非不合理的要求。然而,现在是时候使用 Revalee 了。
首先,让我们准备好 Web 服务器 (IIS) 以便与 Revalee 和重复任务配合使用。为此,您需要在应用程序的*web.config*文件中添加一些元素。您将首先在配置文件中定义一个新的节(<revalee>
)。
<configuration>
<configSections>
<section name="revalee"
type="Revalee.Client.Configuration.RevaleeSection" requirePermission="false" />
...
</configSections>
...
</configuration>
接下来,您将添加新的<revalee>
配置节的内容。此配置节中包含的详细信息定义了 Revalee 的安装位置、配置方式以及您的重复任务(或任务)的详细信息。
<revalee>
<clientSettings serviceBaseUri="https://:46200"
authorizationKey="YOUR_SECRET_KEY" />
<recurringTasks callbackBaseUri="http://yourwebapp.com">
<task periodicity="daily" hour="05"
minute="00" url="/Report/DailySummary" />
</recurringTasks>
</revalee>
最后,您需要包含一个自定义模块,该模块将利用上面列出的配置信息,称为RecurringTaskModule
。
<system.webServer>
<modules runAllManagedModulesForAllRequests="false">
<add name="RevaleeRecurringTasks"
type="Revalee.Client.RecurringTasks.RecurringTaskModule, Revalee.Client"
preCondition="managedHandler" />
</modules>
</system.webServer>
基于上面示例代码中列出的<task>
元素,您的 Web 应用程序将自动注册一个 Revalee 重复回调到http://yourwebapp.com/Report/DailySummary
,时间是每天早上 5:00。简单来说,这意味着您的 MVC 应用程序的ReportController
将每天早上 5:00 运行其DailySummary()
方法。所有业务逻辑现在都已通过DailySummary()
方法实现。就是这样:简单省事。
幕后
那么RecurringTaskModule
是如何工作的?最简单的形式是,这个实现IHttpModule
的类运行如下:
- IIS 启动 Web 应用程序并加载
RecurringTaskModule
,这是一个实现IHttpModule
接口的类。 - 接下来,
LoadManifest()
方法读取*web.config*文件中<revalee>
节中定义的配置详细信息。 - 在验证配置后,会安排一个“心跳”回调到 Revalee,使其在
DateTimeOffset.Now
时运行。 - Revalee (Windows) 服务会接收并立即执行“心跳”回调。
RecurringTaskModule
的BeginRequest
事件被触发,并分析传入的“心跳”回调(HttpRequest
)。- “心跳”
HttpRequest
会触发所有(从*web.config*加载的)重复任务被安排到 Revalee 服务。 - 模块等待处理未来传入的重复任务请求。
如果分析表明HttpRequest
实际上是一个重复任务,那么该请求将被拦截(即,调用HttpApplication.CompleteRequest()
);否则,请求将继续通过正常的HttpRequest
处理管道。
您可能会想:为什么重复任务在每次 Web 应用程序加载时都会被安排(如果您之前没想过,现在会想了)?这不会导致重复任务被安排多次吗?简单的答案是:是的。(这没关系。)当RecurringTaskModule
接收到重复任务的HttpRequest
时,它会使用接收到的第一个此类请求来处理该重复任务,然后忽略所有其他对同一重复任务的传入请求。为了识别重复的任务,每个任务都由其构成属性计算出的哈希值来标识。因此,两个任务不能共享完全相同的详细信息,即周期性、小时偏移量、分钟偏移量和回调 URL。
IHttpModule.BeginRequest
您可能需要在未来的项目中生成自己的IHttpModule
,所以让我们深入研究BeginRequest
事件的处理(确实是IHttpModule
的一个方面,但并非微不足道的一个方面)。
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;
}
}
}
AnalyzeRequest()
方法(稍后详细介绍)确定传入的HttpRequest
是否为重复任务。如果是重复的,SetLastOccurrence()
方法是最终的“守门员”,它决定是否应该处理传入的重复任务请求,或者忽略它(因为该特定重复任务的重复请求已被处理)。
重复任务周期性
您会问,Revalee 支持哪些级别的重复?每小时和每天。
一个**每小时**的重复任务定义如下:
attribute | value |
周期性 | "hourly" |
minute | 值介于 0 到 59(含)之间 |
url | 回调目标的 URL |
<task periodicity="hourly" minute="45" url="/Report/HourlyUpdate" />
一个**每日**的重复任务定义如下:
attribute | value |
周期性 | "daily" |
hour | 值介于 0 到 23(含)之间 [24 小时制] |
minute | 值介于 0 到 59(含)之间 |
url | 回调目标的 URL |
<task periodicity="daily" hour="18" minute="15" url="/Report/DailySummary" />
可以通过包含额外的<task>
元素(用于亚小时重复)和/或在Controller.Action()
级别进行自定义处理(用于超日重复)来实现其他级别的重复。例如,要仅在每周五晚上 6:15 处理请求,您可以在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()
那么,您如何防止每个传入的HttpRequest
都拖垮您的 Web 应用程序呢?保持其速度快且简单,以处理绝大多数请求。在这种情况下,使用Ordinal string
比较的String.StartsWith()
是守门员。一旦第一个字符与_RecurringTaskHandlerAbsolutePath
的值不匹配,它就会return false
。只有那些被确定为重复任务的请求才会被更彻底地处理,例如:
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.
}
结论
在*web.config*中的详细信息和ReportController
中的代码之间,您已成功将所有业务逻辑封装在您的 Web 应用程序中。这使得长期维护更加容易。至于那个每日摘要报告,您所要做的就是编写DailySummary()
方法。现在这既简单又优雅。真棒!
延伸阅读
历史
- [2014.05.19] 初始发布。
- [2014.May.19] 添加了“延伸阅读”部分。
- [2014.May.23] 修正了“延伸阅读”部分,添加了“UrlValidator,Revalee 使用的一个项目小部件”。