ASP.NET 程式中的背景工作 (3) - 使用 Quartz.NET

摘要:續上集,這次試試更強大的排程系統:Quartz.NET。

Quartz.NET 簡介

Quartz.NET 是:
  • 一套彈性的工作排程系統,同時兼顧了簡單和複雜的排程需求;
  • Java 的 Quartz 類別庫移植到 .NET 平台,開放原始碼,採用 Apache 2.0 授權條款;
它的特色:
  • 支援叢集(cluster)架構(目前僅限搭配 AdoJobstore 使用);
  • 支援 SQL Azure;
  • 支援 cron 表示式(這是一種用來表達時間排程的字串樣板)。

有一份投影片對入門學習挺有幫助:Quartz.NET - Enterprise Job Scheduler for .NET Platform

我也做了個簡化版的概念圖,可對照底下的範例程式碼來協助理解。如下圖:


使用 Quartz.NET 的基本步驟
  1. 加入組件參考
  2. 編輯 web.config 以設定排程
  3. 撰寫我們的工作類別
  4. 修改 Global.asax.cs 以啟動工作排程
以下是個非常簡單的實作練習,範例程式仍是基於上一篇筆記的 SendMailTask 修改而來。使用的工具是 Visual Studio 2013,Target Framework 是 .NET 4.0(以確認可用於既有專案)。

實作練習

Step 1:加入組件參考

可透過 NuGet 取得 Quartz.NET 並加入組件參考。此範例使用的 Quartz.NET 版本是 2.2.2。

Step 2:編輯 web.config

通常我們會想要把工作排程相關設定寫在組態檔裡面,以便將來隨時調整排程。這裡的範例完全使用程式碼,沒有用到組態檔。

Step 3:撰寫我們的工作類別

直接使用上一篇筆記中的 SendMailTask,只需要一點點修改:
  • Quartz.NET 的工作類別必須實作 Quartz.IJob 介面。
  • Quartz.IJob 介面只定義了一個方法:
    void Execute(IJobExecutionContext context);

基於上述資訊,將原本的 SendMailTask 類別改成這樣:

public class SendMailTask : IJob
{
    private void Log(string msg)
    {
        System.IO.File.AppendAllText(@"C:\Temp\log.txt", msg + Environment.NewLine);
    }

    public void DoSendMail()
    {
        Log("Entering DoSendMail() at " + DateTime.Now.ToString());
        // 發送 email。這裡只固定輸出一筆文字訊息至 log 檔案,方便觀察測試。
        // 每發送一封 email 就檢查一次 IntervalTask.Current.SuttingDown 以配合外部的終止事件。
        string msg = String.Format("DoSendMail() at {0:yyyy/MM/dd HH:mm:ss}", DateTime.Now);
        Log(msg);
        Thread.Sleep(2000);
    }

    public void Execute(IJobExecutionContext context)
    {
        DoSendMail();
    }
}

簡單起見,我還是沒有用其他 logging 套件。Log 訊息仍然是輸出至 C:\test.log.txt。

Step 4: 修改 Global.asax.cs

public class Global : System.Web.HttpApplication
{
    private IScheduler _schedular = null;

    protected void Application_Start(object sender, EventArgs e)
    {
        // 建立簡單的、以 RAM 為儲存體的排程器
        var schedulerFactory = new Quartz.Impl.StdSchedulerFactory();
        _schedular = schedulerFactory.GetScheduler();

        // 建立工作
        IJobDetail job = JobBuilder.Create<sendmailtask>()
                            .WithIdentity("SendMailJob")
                            .Build();

        // 建立觸發器
        ITrigger trigger = TriggerBuilder.Create()
                                .WithCronSchedule("0 0/1 * * * ?")  // 每一分鐘觸發一次。
                                .WithIdentity("SendMailTrigger")
                                .Build();  

        // 把工作加入排程
        _schedular.ScheduleJob(job, trigger);

        // 啟動排程器
        _schedular.Start();
    }

    protected void Application_End(object sender, EventArgs e)
    {
        _schedular.Shutdown(false);
    }
}

如果只是簡單的定時觸發器,不想要使用 cron 表示法(如上方範例中的 "0 0/1 * * * ?"),以下是另一種寫法(取自官網教學文件):

ITrigger trigger = TriggerBuilder.Create()
    .WithIdentity("trigger1", "group1")
    .StartNow()
    .WithSimpleSchedule(x => x
        .WithIntervalInSeconds(10)
        .RepeatForever())
    .Build();


Step 5:部署與測試

讓應用程式自動啟動且持續運行的 IIS 相關設定請參考 ASP.NET 程式中的背景工作 (1)。設定完成後,實際觀察 log.txt 檔案的內容,確認背景工作有按照程式中的排程設定,每一分鐘觸發一次。

注意事項

以本文的範例為基礎,我陸續做了點小實驗,發現一些需要注意的地方:
  • 預設可重複執行同一項工作:Quartz 會在每次觸發排程工作時建立你的 Job 物件,然後呼叫 Execute 方法。這表示你的 Job 類別可能會被同時建立多個執行個體。如果想要限定同一時間只能有一個 Job 物件,可以對你的 Job 類別套用 [DisallowConcurrentExecutionAttribute]。如此一來,只要目前的工作尚未執行完畢,後續觸發的工作都會延後執行,以確保一次只執行同一項工作。
  • 別向 ASP.NET 應用程式管理員註冊你的工作物件:承上,由於 Quartz 會在每次觸發排程工作時建立你的 Job 物件,你的 Job 類別不該實作 System.Web.Host.IRegisteredObject,也不要在建構函式中呼叫 HostingEnvironment.RegisterObject(this) 來向 ASP.NET 應用程式管理員註冊此物件,否則 Quartz 排程器若觸發你的工作 N 次,記憶體中就會有 N 個你的 Job 物件(即使這些工作已經執行完畢),而當網站停止(App Domain 釋放)時,ASP.NET 應用程式管理員也會逐一呼叫這些 N 個物件的 Stop 方法。如欲在 Job 類別裡面判斷是否該中止工作,可以利用 IJob.Execute 方法傳入的 context 參數來取得 scheduler 物件的關閉旗號,例如:if (context.Scheduler.IsShutDown) ....。在前面的範例中, Application_End 事件呼叫 Scheduler 物件的 Shutdown 方法之後,其 IsShutDown 屬性會被設定為 true,代表排程器已經停止。
  • 錯誤處理:預設情況下,若你的工作在執行過程中拋出 exception(沒有捕捉並處理 exception),就只有這份工作執行個體受影響(工作中止),這意味著 Quartz 排程器不會掛掉,你的 ASP.NET 應用程式不會停止,而且排程器依舊能夠在下一次預定時間到達時觸發並啟動下一個工作。按照官方文件的說明,實作 IJob.Execution 的方法應該要用一個 try...catch 區塊捕捉所有的 exception;若想要拋出 exception 讓排程器知道發生錯誤了,以及後續該如何處理(例如立刻再啟動工作),只能夠拋出 JobExecutionException。

小結

初次使用 Quartz,感覺不錯,下次再試點別的寫法。
Happy coding :)

相關文章
延伸閱讀

Post Comments

技術提供:Blogger.