65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Revalee 和 MVC 安排定期任务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (11投票s)

2014年5月19日

MIT

7分钟阅读

viewsIcon

29368

downloadIcon

310

一个 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的类运行如下:

  1. IIS 启动 Web 应用程序并加载RecurringTaskModule,这是一个实现IHttpModule接口的类。
  2. 接下来,LoadManifest()方法读取*web.config*文件中<revalee>节中定义的配置详细信息。
  3. 在验证配置后,会安排一个“心跳”回调到 Revalee,使其在DateTimeOffset.Now时运行。
  4. Revalee (Windows) 服务会接收并立即执行“心跳”回调。
  5. RecurringTaskModuleBeginRequest事件被触发,并分析传入的“心跳”回调(HttpRequest)。
  6. “心跳”HttpRequest会触发所有(从*web.config*加载的)重复任务被安排到 Revalee 服务。
  7. 模块等待处理未来传入的重复任务请求。

如果分析表明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 使用的一个项目小部件”。
© . All rights reserved.