Hangfire 筆記2 - 執行定期排程

想用 ASP.NET Hangfire 跑定期排程,有一個前題是「需確保網站永遠處於執行狀態」,先推薦幾篇相關文章:

摸索過程我發現更簡單的新做法,實測可行,整理設定步驟如下:

  1. IIS AppPool 進階設定
    啟動模式設 AlwaysRunning (註: 記得確認已安裝「應用程式初始化」)

  2. 在 IIS 管理員站台或應用程式的進階設定啟用「預先載入已啟用」(Preload Enabled)

    註: 如想在預先載入時呼叫特定網址可使用 web.config 設定。參考: Use IIS Application Initialization for keeping ASP.NET Apps alive - Rick Strahl's Web Log
      <system.webServer>
        <applicationInitialization remapManagedRequestsTo="Startup.htm"  
                                   skipManagedModules="true">
          <add initializationPage="ping.ashx" />
        </applicationInitialization>
      </system.webServer>
    再註:initializaionPage 指向的網頁應開放匿名存取或允許 AppPool 執行身分有權存取,否則會自動啟動失效。參考 感謝余小章補充。
  3. Hangfire 官方出了一個 Hangfire.AspNet 套件,可簡化 IIS 設定及自己實作 IRegisteredObject 跟 IProcessHostPreloadClient 介面的程序,依據 Github 上的說明,這個新做法未來將取代現有官網所建議的安裝步驟(This package aims to replace the documentation article Making ASP.NET application always running.) 相關文件會晚一點才釋出... (眼看兩年過去了文件還沒好,但身為開發者,我懂,呵)
    沒有文件無妨,直接參考 Github 上的範例專案,我琢磨調整完的 Startup 類別如下:
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading;
    using System.Web;
    using System.Web.Hosting;
    using Hangfire;
    using Hangfire.Logging;
    using Microsoft.Owin;
    using Owin;
    using Hangfire.SQLite;
     
    [assembly: OwinStartup(typeof(MyApp.Startup))]
    namespace MyApp
    {
     
        //REF: https://github.com/HangfireIO/Hangfire.AspNet
        public class Startup : IRegisteredObject
        {
            public Startup()
            {
                HostingEnvironment.RegisterObject(this);
            }
     
            private static readonly string SqliteDbPath = 
                HostingEnvironment.MapPath("~/App_Data/Hangfire.sqlite");
     
            private static BackgroundJobServer backJobServer = null;
     
            public static IEnumerable<IDisposable> GetHangfireConfiguration()
            {
                GlobalConfiguration.Configuration
                    .UseSQLiteStorage($"Data Source={SqliteDbPath};");
     
                backJobServer =  new BackgroundJobServer(
                    new BackgroundJobServerOptions
                    {
                        ServerName = 
                        $"JobServer-{Process.GetCurrentProcess().Id}"
                    });
                yield return backJobServer;
            }
     
            public void Configuration(IAppBuilder app)
            {
                //改用UseHangfireAspNet設定Hangfire服務
                app.UseHangfireAspNet(GetHangfireConfiguration);
                app.UseHangfireDashboard();
     
                ScheduledTasks.Setup();
            }
     
            //ApplicationPool結束時會呼叫
            public void Stop(bool immediate)
            {
                //Thread.Sleep(TimeSpan.FromSeconds(30));
                //Github範例等待30秒,會影響AppPool停止及回收速度
                //這裡改為直接呼叫backJobServer.Dispose()
                if (backJobServer != null)
                {
                    backJobServer.Dispose();
                }
     
                HostingEnvironment.UnregisterObject(this);
            }
        }
    }

設定排程部分我寫成另一顆物件,範例如下。這段程式每次啟動網站都會執行,故 AddOrUpdate() 時要指定排程名稱,排程已存在就只更新不新增,才不會新增一堆重複排程。實務上如求彈性,也可採用資料庫或設定檔管理排程。

using Hangfire;
 
namespace MyApp
{
    public class ScheduledTasks
    {
        private static NLog.ILogger logger = 
            NLog.LogManager.GetLogger("SchTasks");
 
        public static void Setup()
        {
            //REF: https://en.wikipedia.org/wiki/Cron#CRON_expression
            RecurringJob.AddOrUpdate("PerMinute", () => DumpLog(), 
                Cron.Minutely);
        }
 
        private static int Counter = 0;
 
        public static void DumpLog()
        {
            logger.Debug(Counter++.ToString());
        }
    }
}

實測 Hangfire.SQLite 發現一個問題,原本一分鐘跑一次的排程莫名每一分鐘執行 20 次,經調查應為 Bug,Hangfire 預設會開 20 條 Worker Thread,時間一到每個 Worker 都跑了一次。將 Worker 數調為 5 就變成跑 5 次。這問題在 Github 上也被網友被提報為 Issue,作者建議先將 Worker 數設成 1 避開。

所幸,又到了見識 Open Source 奇蹟的時刻,既然是 Open Source,遇到 Bug 自己查自己修也是很合理滴。花了點時間查出原因試著修正,也送了 PR,希望這個問題在未來的版本會被修復。

另外,實測 Hangfire.SQLite 跑定時排程還有另一個問題,當設成每分鐘整點執行,啟動時間並非 100% 精準。例如以下每分鐘一次的排程,每分鐘執行時點卻在 01-15 秒區間移動,為什麼是 15 秒?推測與預設 SchedulePollingInterval = 15 秒有關。 查了 Source Code,跟QueuePollInterval 預設 15 秒有關。

試著改用 SQL Server 或 Memory Storage 則沒發現類似問題,我懷疑這與 SQLite 執行速度不夠快有關,在一篇國外文章也提到類似的觀察。總之,如果系統對執行時間精準度要求很高,使用 SQLiteStorage 前應審慎評估。要解決這個問題,可在 UseSQLiteStorage() 時將 QueuePollInterval 改成 1 秒,誤差將縮小在兩秒以內,代價這會提高 Hangfire 查詢資料庫的頻率較耗損效能,大家可依專案需求自行拿捏。

                .UseSQLiteStorage($"Data Source={SqliteDbPath};", 
                new SQLiteStorageOptions()
                {
                    QueuePollInterval = TimeSpan.FromSeconds(1)
                });
歡迎推文分享:
Published 25 January 2018 06:48 AM 由 Jeffrey
Filed under: , ,
Views: 3,925



意見

# 余小章 said on 24 January, 2018 10:08 PM

補充一下:

Application Pool Identity 要有權限瀏覽 Default Page 或是排除Default Page的授權

# Jeffrey said on 25 January, 2018 12:41 AM

to 余小章,好一個眉角,謝謝提醒,已補充於本文。

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<January 2018>
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication