ASP.NET 程式中的背景工作 (2)

摘要:續上集,使用現成的 ASP.Net Long-Running Interval Task,並改進第一版的 SendMailTask 類別。

實驗過上一篇筆記的範例之後,已經知道那個範例在實務應用上仍有不足之處,其中一個比較重要的問題,就是背景工作無法得知 ASP.NET 應用程式是否正要結束,因而可能產生資料遺失的問題。

這篇筆記中的範例會使用 ASP.Net Long-Running Interval Task 裡面現成的 IntervalTask 類別來建立背景工作。

先說結論:這個 IntervalTask 類別只能建立一個 task,故也只適用於比較簡單的場合。如果需要在一個 ASP.NET 應用程式中建立多個定期執行的背景工作,就得自己動手修改此類別,或改用其他解決方案,如 FluentSchedulerQuartz.NET(寫在下一篇)。

實作練習

Step 1:下載 ASP.Net Long-Running Interval Task

下載完整原始碼之後,我們的 ASP.NET 應用程式只需要參考其中的 AspNetIntervalTask.csproj。

Step 2:修改 SendMailTask 類別

上一篇筆記中的 SendMailTask 類別含糊地包辦了「工作」和「排程」兩項任務(雖然稱不上排程,頂多就是定期執行而已,這裡只是為了方便說明)。現在有了現成的 IntervalTask 類別幫我們處理掉「排程」的動作,原先的 SendMailTask 類別就可以簡化許多。變成這樣:

public class SendMailTask
{
    public SendMailTask()
    {
        // 從組態檔載入相關參數,例如 SmtpHost、SmtpPort、SenderEmail 等等.
    }

    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 以配合外部的終止事件。
        while (Brass9.Threading.IntervalTask.Current.ShuttingDown == false)
        {
            string msg = String.Format("DoSendMail() at {0:yyyy/MM/dd HH:mm:ss}", DateTime.Now);
            Log(msg);
            Thread.Sleep(2000);
        }
        Log("Shutting down task at " + DateTime.Now.ToString());
    }

}

IntervalTask 類別提供了偵測 ASP.NET 應用程式是否正要結束的功能,所以我們可以在背景工作中隨時檢查,若發現應用程式正要結束,就立刻結束背景工作。

Step 3:Global.asax

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        var sendMailTask = new SendMailTask();
        var task = IntervalTask.CreateTask(sendMailTask.DoSendMail);
        task.SetInterval(5000);

        // The following code throws exception because IntervalTask only support one task.
        /*
            var task2 = IntervalTask.CreateTask(() =>
            {
                // dummy. 
            });
            task2.SetInterval(2000);
        */
    }

    protected void Application_End(object sender, EventArgs e)
    {
        IntervalTask.Current.Dispose();
    }
}


Step 4:部署至 IIS 並觀察執行結果

IIS 設定的部分請參考上一篇筆記。這次觀察的重點只有一個:網站啟動之後,查看 log.txt 檔案確認背景工作有執行,然後到 IIS 管理員中手動回收網站的 app pool。此時 log.txt 裡面應該會看到一則訊息:「Shutting down task at 某個時間」。

改善上一篇文章的 SendMailTask 類別

IntervalTask 之所以能收到 ASP.NET 應用程式即將關閉的通知,是因為實作了 System.Web.Hosting.IRegisteredObject 介面,並透過 HostingEnvironment.RegisterObject 方法向應用程式管理員登記:我(IntervalTask 物件本身)要收到應用程式結束通知。

System.Web.Hosting.IRegisteredObject 介面只定義了一個 Stop 方法:

void Stop(bool immediate)

其參數 immediate 代表外在執行環境是否要求立刻結束。ASP.NET 應用程式管理員第一次呼叫此方法時會傳入 false,讓我們的物件有一些緩衝時間來進行解除登記和收尾的工作。如果在一段時間(約 30 秒)之後,物件尚未完成解除登記的動作,應用程式管理員會再呼叫一次 Stop 方法,此時會傳入 true,對物件下最後通牒:就算你不解除登記,返回之後也會被自動移除。

了解這個註冊物件的機制之後,我們可以回頭把上一篇文章裡的 SendEmailTask 再加強一下,讓它也能夠在網站即將停止時收到通知。底下就是修改後的、依然陽春但包山包海自給自足的 SendEmailTask 背景工作類別,主要變動的部分是第 1 行、第 7 行、第 55~59 行:

public class SendMailTask : System.Web.Hosting.IRegisteredObject
{
    private bool _stopping = false;

    public SendMailTask()
    {
        HostingEnvironment.RegisterObject(this);   // 向 ASP.NET 應用程式管理員註冊此物件
    }

    public void Run()
    {
        var aThread = new Thread(TaskLoop);
        aThread.IsBackground = true;
        aThread.Priority = ThreadPriority.BelowNormal;  // 避免此背景工作拖慢 ASP.NET 處理 HTTP 請求.
        aThread.Start();
    }

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

    private void TaskLoop()
    {
        // 設定每一輪工作執行完畢之後要間隔幾分鐘再執行下一輪工作.
        const int LoopIntervalInMinutes = 1000 * 60 * 1;

        Log("TaskLoop on thread ID: " + Thread.CurrentThread.ManagedThreadId.ToString());
        while (!_stopping)
        {
            try
            {
                DoSendMail();
            }
            catch (Exception ex)
            {
                // 發生意外時只記在 log 裡,不拋出 exception,以確保迴圈持續執行.
                Log(ex.ToString());                    
            }
            finally
            {
                // 每一輪工作完成後的延遲.
                System.Threading.Thread.Sleep(LoopIntervalInMinutes);
            }
        }
    }

    private void DoSendMail()
    {
        // 發送 email。這裡只固定輸出一筆文字訊息至 log 檔案,方便觀察測試。
        string msg = String.Format("DoSendMail() at {0:yyyy/MM/dd HH:mm:ss}", DateTime.Now);
        Log(msg);
    }

    public void Stop(bool immediate)
    {
        _stopping = true; 
        System.Web.Hosting.HostingEnvironment.UnregisterObject(this);
    }
}

小結

IntervalTask 類別只能建立一個 task,實用性打了折扣,但作為參考學習的範例還是不錯的。這次也一併修改了上一篇筆記中的 SendMailTask 類別,儘管陽春依舊,對於一些簡單的需求以及無法使用第三方元件的場合,還算堪用。

相關文章

Post Comments

技術提供:Blogger.