以下範例使用 dotnet core mvc 專案,參考 hangfire 官網文件:ASP.NET Core Applications及其他參考資料練習,相關連結隨附於後不再贅述
Sample Code:Github
參考連結
- ASP.NET Core Applications
- 使用 HANGFIRE 處理 ASP.NET MVC/WEB API 長時間與排程工作
- ASP.NET Core 使用 Hangfire 做排程
- Hangfire: Writing unit tests
- 如何對 Hangfire Job 撰寫測試
OverView
從上圖可以看到 hangfire 的三種腳色
- Client:負責建立各種任務,可藉由 hangfire 提供的方法建立即時、延遲、重複任務。由 Client 負責將其序列化之後儲存於
Storage
- Storage:儲存任務資料用,有很多 Storage 套件可供選擇,支援 SQL、Redis 等常見主流的 storage 方案
- Server:從
storage
中取得任務並執行
How to Start
建立 dotnet core mvc 專案
安裝 nuget 套件
Hangfire.AspNetCore
Hangfire.MemoryStorage
此處因為練習而採用 memory storage,production 記得要改用持久化的 storage 解決方案
啟用 hangfire dashboard
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseHangfireServer();
app.UseHangfireDashboard( pathMatch: "/hangfire", options: new DashboardOptions() { Authorization = new[] { new MyAuthFilter() } } );
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddHangfire(config => { config.UseMemoryStorage(); config.UseConsole(); }); }
|
1 2 3 4 5 6 7
| public class MyAuthFilter : IDashboardAuthorizationFilter { public bool Authorize([NotNull] DashboardContext context) { return true; } }
|
設定 dashboard 並建立第一個 job
1 2 3 4 5 6 7
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobs) { backgroundJobs.Enqueue(() => Console.WriteLine("Hello world from Hangfire!")); }
|
這樣一來就會可以在網站路由/hangfire
底下看到剛剛建立的Console.WriteLine
已經完成
Job 類型
Fire-and-forget jobs
依照Calling methods in background一文說明,我們在上一個步驟執行的就是背景調用方法
1
| var jobId = BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget!"));
|
實際上他並不是當下就執行那個任務,而是轉為下列行為:
- 序列化方法及參數
- 根據序列化的內容建立一個新的
背景執行任務
- 將這個
背景執行任務
儲存於我們所設定的持久化儲存機制內,也就是storage
- 將
背景執行任務
放在 Queue 的執行隊列中排隊
做完了這些事情後,程式返回原先的地方依序往下執行
上面這一段有點饒舌,簡單來說就是,當我們使用Enqueue
這個方法執行 Console.WriteLine 的時候,其實是把這個東西轉變成一個背景任務,放到 hangfire 的 Queue 裡面排隊。
那他什麼時候才會執行呢?這就要靠另外一個腳色hangfire server
來檢查 Queue 裡面還有沒有任務要執行,有的話它會先把這個任務隱藏,這樣其他的人就沒辦法看到這個任務,這個是為了避免同一個任務被多次執行,接著就是執行任務了,等到執行完成,他才會把這個任務從 Queue 裡面刪除
Delayed jobs
中文有點難理解,英文原文反而很清楚,所以其實學 IT 的人應該還是要習慣看英文啦~
官網文件介紹這個情境是,假設有新會員註冊,也許你會希望在會員註冊的第二天發送一封 Email 給他們。我覺得這樣的介紹很不錯,看其他人的 Blog 都說有好幾種排程,我就在想那什麼時候會用到這些東西呢?果然官網沒有讓我失望啊。正好呼應了學新東西最好的方式,就是看官網文件
下面就是建立一個延遲調用的語法範例,看起來很好懂啊
1
| var jobId = BackgroundJob.Schedule(() => Console.WriteLine("Delayed!"), TimeSpan.FromDays(7));
|
但是其實這邊暴露了另外一個重要資訊喔,那就是hangfire server
要多久來檢查定期任務呢?所以其實是有提供設定方式的
1 2 3 4 5 6
| var options = new BackgroundJobServerOptions { SchedulePollingInterval = TimeSpan.FromMinutes(1) };
var server = new BackgroundJobServer(options);
|
這裡還有特別提到如果是在 ASP.NET 應用程式的話,還需要做一些額外的事情,詳情就看一下官網說明囉
Recurring jobs
官網範例只有一行,Cron 類別看來是跟 linux crontab 一樣的東西,可以看一下鳥哥的文章第十五章、例行性工作排程(crontab),或者是wiki 說明
1 2 3
| RecurringJob.AddOrUpdate(() => Console.WriteLine("Recurring!"), Cron.Daily);
RecurringJob.AddOrUpdate(() => Console.Write("Powerful!"), "0 12 * */2");
|
加入識別 ID 語法如下
1 2
| RecurringJob.AddOrUpdate("some-id", () => Console.WriteLine("wow!!"), Cron.Hourly);
|
識別 ID 在某些 Storage 可能會區分大小寫;識別 ID 應為 Unique 值
實際上會在 dashboard 顯示
那為甚麼要有識別 ID 咧?因為要方便我們操作啊。我們可以透過識別 ID 去觸發執行,或是移除
1 2 3 4 5
| RecurringJob.RemoveIfExists("some-id");
RecurringJob.Trigger("some-id");
|
想看原文的請 follow這裡
Batches & Batch Continuations
付費功能,有興趣請自行瀏覽官網說明
加入預設儀錶板資訊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public void ConfigureServices(IServiceCollection services) { services.AddHangfire(config => { config.UseMemoryStorage(); config.UseConsole(); config.UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.ServerCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.RecurringJobCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.RetriesCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.EnqueuedAndQueueCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.ScheduledCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.ProcessingCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.SucceededCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.FailedCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.DeletedCount) .UseDashboardMetric(Hangfire.Dashboard.DashboardMetrics.AwaitingCount); }); }
|
單元測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class MyJob { protected IBackgroundJobClient JobClient { get => this._jobClient ?? (this._jobClient = new BackgroundJobClient()); set => this._jobClient = value; } private IBackgroundJobClient _jobClient; public MyJob() : this(new BackgroundJobClient()) { }
public MyJob(IBackgroundJobClient jobClient) { this.JobClient = jobClient; }
[Hangfire.Dashboard.Management.Support.Job] [DisplayName("呼叫內部方法")] public void SomeWork01(PerformContext context = null, IJobCancellationToken cancellationToken = null) { if (cancellationToken.ShutdownToken.IsCancellationRequested) { return; } context.WriteLine($"測試用,Now:{DateTime.Now}"); Thread.Sleep(30000); } public void EnqueueJob() { this.JobClient.Enqueue(() => this.SomeWork01(null, JobCancellationToken.Null)); } }
|
production 的部分,MyJob 的 EnqueueJob 方法,負責的事情就是將某一件任務進行排程;而我們的單元測試,也只是要驗證這一件事情。
因此在單元測試的部分,我們首先建立一個 Mock,並透過建構式注入,稍後才可以透過 mock 物件檢查是否有接收到參數,檢查的部分我們僅驗證任務的名稱是否正確
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [TestClass] public class UnitTest1 { [TestMethod] public void Enqueue驗證有呼叫Create方法() { var mockJobClient = Substitute.For<IBackgroundJobClient>(); var demoJob = new MyJob(mockJobClient); demoJob.EnqueueJob(); mockJobClient.Received().Create( Arg.Is<Job>(p => p.Method.Name == nameof(MyJob.SomeWork01)), Arg.Any<EnqueuedState>() ); } }
|