1

在 ASP.NET Core 網站執行定時排程

 3 years ago
source link: https://blog.darkthread.net/blog/aspnet-core-background-task/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在 ASP.NET Core 網站執行定時排程

2020-07-22 09:49 PM 6 3,939

為網站加入定期排程算很常見的設計,可用來處理過期 Cache 清除、資料定期刷新,系統狀態監控及自動問題修復。過去在 ASP.NET 時代,我會用一種笨但有效的方法 - 跑 Windows 排程每隔幾分鐘呼叫特定網頁執行任務,遇到較複雜需管理介面甚至要能重試的需求,也曾用過 Hangfire。來到 ASP.NET Core 時代,ASP.NET Core 本身內建背景排程機制,面對需要持續定期執行的作業,現在又多了輕巧高整合性的新選擇。

這篇文章將以範例展示如何在現有 ASP.NET Core 實現一個簡單定期排程 - 每隔五秒記錄當下網站應用程式記憶體用量。

首先,我們要寫一個類別實作 IHostedService 介面,其中要包含兩個方法,StartAsync(CancellationToken) 及 StopAsync(CancellationToken)。

StartAsync() 通常是在 Startup.Configure() 及 IApplicationLifetime.ApplicationStarted 事件之前被呼叫,以便在其中設定定時器、啟動執行緒好運行背景作業。常見寫法是在 Startup.ConfigureServices() 中 services.AddHostedService<T>(),這裡借用 ASP.NET Core 新增修改刪除(CRUD)介面傻瓜範例的程式修改:

public void ConfigureServices(IServiceCollection services)
{
    //註冊 DB Context,指定使用 Sqlite 資料庫
    services.AddDbContextPool<JournalDbContext>(options =>
    {
        //TODO: 實際應用時,連線字串不該寫死在程式碼裡,應應移入設定檔並加密儲存
        options.UseSqlite(Configuration.GetConnectionString("SQLite"));
    });

    services.AddRazorPages();
    
    //加入背景服務
    services.AddHostedService<MemoryUsageMonitor>();
}

若要在 Startup.Configure() 後執行,則可加在 Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureServices(services =>
        {
            //加入背景服務
            services.AddHostedService<MemoryUsageMonitor>();
        });

StopAsync() 則在網站停機過程觸發,通常用於執行 Disposable / finalizer 等 Unmanaged 資源的回收。當然,如果是意外關機或程式崩潰就不會被呼叫,故需考量此類意外狀況的善後(如果有需要的話)。StopAsync() 會接受一個 Cancellation Token 參數,預設有 5 秒的 Timeout,意味著 StopAsync() 的收拾動作必須在五秒內完成,超過時限 IsCancellationRequested 會變成 true,雖然 ASP.NET Core 仍會繼續等事情做完,但會被判定關機程序異常。

以下是 MemoryUsageMonitor.cs 範例:

using Microsoft.Extensions.Hosting;
using NLog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CRUDExample.Models
{
    public class MemoryUsageMonitor : IHostedService, IDisposable
    {
        static Timer _timer;
        ILogger _logger = NLog.LogManager.GetCurrentClassLogger();

        public MemoryUsageMonitor()
        {
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new Timer(DoWork, null,
                TimeSpan.Zero,
                TimeSpan.FromSeconds(10));
            return Task.CompletedTask;
        }

        private int execCount = 0;

        public void DoWork(object state)
        {
            //利用 Interlocked 計數防止重複執行
            Interlocked.Increment(ref execCount);
            if (execCount == 1)
            {
                try
                {
                    _logger.Info(
                        $"Memory Usage = {Process.GetCurrentProcess().WorkingSet64}"
                        );
                }
                catch (Exception ex)
                {
                    _logger.Error("發生錯誤 = "  + ex);
                }
            }
            Interlocked.Decrement(ref execCount);
        }


        public Task StopAsync(CancellationToken cancellationToken)
        {
            //調整Timer為永不觸發,停用定期排程
            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}

補充一點,Timer 的原則是時間到就觸發,故要考慮前一次呼叫 DoWork 還沒跑完,下個週期又將開始的狀況,若要做到前次還沒跑完先不執行,需自行實作防止重複執行機制,我在範例是用支援多執行緒的 Interlocked.Increment() 及 Decrement() 增刪計數器,確保同時間只有單一作業會執行。

實測成功:

最後我還想驗證一事,前面有提到 StopAsync() 會收到 CancellationToken,應在五秒內完成工作,否則將視為關機異常。我故意將 StopAsync() 改成乾等 10 秒,看看會發生什麼事:

public Task StopAsync(CancellationToken cancellationToken)
{
    _logger.Debug("Start StopAsync...");
    //調整Timer為永不觸發,停用定期排程
    _timer?.Change(Timeout.Infinite, 0);

    //故意等10秒才結束
    var waitTime = DateTime.Now.AddSeconds(10);
    while (DateTime.Now.CompareTo(waitTime) < 0)
    {
        Thread.Sleep(1000);
        _logger.Debug(cancellationToken.IsCancellationRequested);
    }
    return Task.CompletedTask;
}

在 Debug Console 按下 Ctrl-C 停止網站,由 Log 可觀察到 cancellationToken.IsCancellationRequested 確實在五秒後變成 true,但 10 秒迴圈仍會跑完:

但因超過時限,會得到 OperationCanceledException。若將 waitTime 改為 2 秒,錯誤即告消失,得證:

參考文件:Background tasks with hosted services in ASP.NET Core


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK