背景服務執行週期性工作 - 使用 BackgroundService 與 Sgbj.Cron.CronTimer

  • 692
  • 0
  • C#
  • 2024-10-21

上一篇「背景常駐執行計時工作 - 使用 BackgroundService 與 PeriodicTimer」介紹了使用 BackgroundService 與 PeriodicTimer 實作計時的工作處理。

但如果有時候一些工作處理並不是每幾秒鐘或每幾分鐘、每幾個小時這樣的週期行為時,使用 PeriodicTimer 就無法滿足這樣的需求,而在使用 Hangfire 建立 Recurring Job 時都會使用到 Cron Expression 去定義工作的執行週期,而就有看到這麼一個 NuGet 套件就有提供這樣的功能,所以就拿來試試看。

Cron

"cron" 其實是 「chronograph」 的縮寫或變體。這個詞源自於希臘語「χρόνος」(chronos),意思是時間。cron 是一個用於類 Unix 系統的時間驅動任務排程工具,能在指定的時間自動執行程式或腳本。

cron 的名字反映了它的功能:基於時間的排程與自動化。例如,使用 cron,你可以設置在每天的特定時間執行某個指令或腳本。這種時間驅動的特性使它在伺服器管理、定時任務執行等方面非常有用。

因此,雖然 "cron" 並不是一個正式的縮寫,而是來源於「chronos」,其名字的選擇恰好描述了它的作用。

https://zh.wikipedia.org/zh-tw/Cron

在設定 Cron Expression 時,我都習慣使用 crontab guru 這個線上工具做編輯,因為可以即時地在網頁上就可以得知 expression 的週期是否符合我們的預期,使用 Hangfire  建立 Recurring Job 時就會用到 Cron Expression 定義時間週期。

https://crontab.guru/

 

Cronos

Cronos 是由 Hangfire 團隊開發的一個開源 .NET Library,專門用於處理 Cron 表達式。

https://github.com/HangfireIO/Cronos

以下是它的主要特性與功能介紹:

  1. 主要功能
    處理 Cron 表達式:提供簡單且靈活的 API,可以解析和評估 Cron 表達式,用於計時執行任務。
    支持時區:內建對時區的處理能力,讓 Cron 表達式能夠正確應用於不同時區。
  2. 應用場景
    用於需要基於 Cron Expression 執行任務的 .NET 應用服務,例如週期性資料備份、定時發送郵件等。
    在類似 Hangfire 的背景工作系統中,精確地安排作業執行。

在專案裡並不需要安裝使用 Hangfire 時,可以透過 Cronos 這個工具來取得週期時間,來試試看

例如,我想要列出從現在開始的一個小時內每五分鐘的時間(At every 5th minute)

或是要列出從現在開始的一個月,每 6 小時, 在 12:00 AM 和 08:59 PM 之間,  只有星期一和星期五的日期時間(At minute 0 past every 6th hour from 0 through 20 on Monday and Friday)

Cronos 是可以支援到「秒」的週期,例如想要取得從現在開始到 10 分鐘內,偶數分鐘每 30 秒的時間

以往要取得這種週期循環的日期時間會比較麻煩一些,但透過 Cronos 就可以比較方便一些。

而會先介紹 Cron Expression 以及 Cronos,則是因為 Sgbj.Cron.CronTimer 是相依 Cronos 的一個工具套件,所以就先做個介紹說明。

 

Sgbj.Cron.CronTimer

直接看它的說明就知道這是用來做什麼事情

接著就用 BackgroundService 和 CronTimer 來做一個週期的工作處理,週期時間定義就用「從現在開始,偶數分鐘每 12 秒的時間就寫一次 Log」

using Cronos;
using Sgbj.Cron;

namespace Sample.WebApplication.BackgroundServices;

/// <summary>
/// class CronTimeerBackgroundService
/// </summary>
public class CronTimeerBackgroundService : BackgroundService
{
    private readonly ILogger<CronTimeerBackgroundService> _logger;

    private readonly TimeProvider _timeProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="CronTimeerBackgroundService"/> class
    /// </summary>
    /// <param name="logger">The logger</param>
    /// <param name="timeProvider">The timeProvider</param>
    public CronTimeerBackgroundService(ILogger<CronTimeerBackgroundService> logger, TimeProvider timeProvider)
    {
        this._logger = logger;
        this._timeProvider = timeProvider;
    }

    /// <summary>
    /// Executes the stopping token
    /// </summary>
    /// <param name="stoppingToken">The stopping token</param>
    /// <exception cref="NotImplementedException"></exception>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is starting.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);

        // cron expression 每個偶數分數裡的每 12 秒
        var cronExpression = CronExpression.Parse("*/12 */2 * * * *", CronFormat.IncludeSeconds);
        
        // 使用 CronTimer
        using var cronTimer = new CronTimer(cronExpression, TimeZoneInfo.Local);

        while (await cronTimer.WaitForNextTickAsync(stoppingToken) && stoppingToken.IsCancellationRequested is false)
        {
            this._logger.LogInformation(
                "{DateTimeNow} [{MachineName}] {TypeName} Processing.",
                $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
                Environment.MachineName,
                this.GetType().Name);
        }
    }
    
    /// <summary>
    /// Stops the cancellation token
    /// </summary>
    /// <param name="cancellationToken">The cancellation token</param>
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is stopping.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);
        await base.StopAsync(cancellationToken);
    }
}

觀察執行的狀況,的確是只有在偶數分鐘的每 12 秒執行寫 Log

完成!

 

相關內容

以上