ASP.NET Core Library – Hangfire


前言

以前寫過 Hangfire 的學習筆記, 但寫的很亂. 這篇做個整理.

 

介紹

Hangfire 是用來做 server task 的, 比如: background job, delay job, schedule job 等等. 它可以做到分鍾級別的 schedule, 任務會通過 SQL Server 來管理 (也可以支持其它 database 但不推薦)

 

參考:

C#-初識Hangfire

官網 docs

 

安裝 & Startup

參考: 官網教程

安裝 nuget

dotnet add package Hangfire.Core
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer

startup

builder.Services.AddHangfire(configuration => configuration
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseSqlServerStorage("Server=192.168.1.152;Database=TestHangfire;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True", new SqlServerStorageOptions
    {
        CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
        SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
        QueuePollInterval = TimeSpan.Zero,
        UseRecommendedIsolationLevel = true,
        DisableGlobalLocks = true
    }));
builder.Services.AddHangfireServer();

配置不重要, 這里是按照官網 example 默認配置, 鏈接上 database 就可以了 (注: Hangfire 會負責創建 tables, 但我們需要先創建好 database, 不然會報錯)

啟動

app.MapHangfireDashboard();

 

Background Job

一般網站都有一個 send enquiry 的功能, 當用戶提交 enquiry 表單后, 系統需要發 email 給管理人.

發 SMTP 是很慢的, 如果讓用戶等待會影響用戶體驗. 所以這種情況就很適合跑一個 background job. 

request 設定好 background job 后就可以直接 response user. 然后系統才背地里去發 SMTP.

這種場景就可以用 Hangfire 來實現了.

public void OnPost()
{
    var enquiryId = 1; // create enquiry to database
    BackgroundJob.Enqueue<EmailService>(e => e.SendEmail(enquiryId));
}

調用 BackgroundJob.Enqueue 就可以了. 它會在 response 之后立刻被執行.

有幾個點需要注意

1. BackgroundJob.Enqueue 的參數是 Expression 而不是 Func, 所以它只能簡單地表達式, 如果要寫負責邏輯就需要開一個方法, 讓表達式去調用方法.

2. 方法必須是 public 的

3. Hangfire 執行 job 時, 會動態的創建這個方法的 class / interface, 它是通過 ActivatorUtilities.CreateInstance 創建的, 支持依賴注入哦, 如果創建失敗 task 就 fail 了, 會去 rety.

4. 方法執行時是完全獨立的一個 scope (線程), 和之前的 request 完全沒有關系. 如果注入 HttpContext 會發現它是 null.

public class IndexModel : PageModel
{
    public string Value { get; set; } = "default";

    public void OnPost()
    {
        Value = "Not Default";
        BackgroundJob.Enqueue(() => DoSomething());
    }

    public void DoSomething() 
    {
        var value = Value; // default
    }
}

上面, DoSomething 執行是是全新的一個 scope, IndexModel 會被創建, 所以 value 是 default.

5. 盡量不要讓方法依賴原本環境的東西, 做一個中間人負責.

比如, 與其把把所有信息以 parameters 形式傳入方法, 更好的做法是讓方法自己去獲取所有信息, 通過一個 Id 作為中間人.

 

Delay Background Job

上面提到的例子是是屬於馬上執行的 background job, 還有一種是 delay job. 比如, 希望 user submit 之后 10 second 才發 email.

public void OnPost()
{
    var enquiryId = 1; // create enquiry in database
    BackgroundJob.Schedule<IEmailService>(s => s.SendEmail(enquiryId), TimeSpan.FromSeconds(10));
}

調用的方法是 .Schedule, 傳入 delay 的 timespan 或者一個絕對時間 DateTimeOffset.

Hangfire 是通過一個 interval 在背后檢查 schedule 的, 它默認的時間是 15 second 檢查一次.

可以通過 options 修改它, 估計是性能考量所以才放 15 秒吧, 不然一直要去 query database check job 也挺傷的.

builder.Services.AddHangfireServer(options => {
    options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
});

 

Recurring Job

上面說的都是一次性執行, recurring job 是用來處理那種每星期/月要執行的 job.

說到這個就得說說 cron expression 了. 它就是用來表達, 每星期, 每月, 還是每逢...什么時辰的.

參考:

CRON 表達式詳解

cron表達式詳解

crontab guru 小工具

Hangfire create/remove recurring job

RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), "cron expression");
RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), Cron.Daily);
RecurringJob.RemoveIfExists("job name");

Cron.Daily 是 Hangfire 的 helper 類, 幫我們創建 cron expression.

也可以封裝成 Service

RecurringJob.AddOrUpdate<FacebookReviewService>("Sync Facebook Review", service => service.SyncToDatabaseAsync(), Cron.Daily(hour: 1));

Service.cs

public class FacebookReviewService
{
    private readonly IWebHostEnvironment _env;

    public FacebookReviewService(
        IWebHostEnvironment env
    )
    {
        _env = env;
    }

    [AutomaticRetry(Attempts = 0)]
    public async Task SyncToDatabaseAsync()
    {
       // do anything
    }
}

AutomaticRetry 是聲明是否失敗了要自動重試. 0 就是不要. 

 

狀況

在設計 job 的時候要記得, server schedule 並不穩定. 有可能遇到 server down, job runtime error 等等的情況.

1. Runtime error and retry

當 job 出現 runtime error 時, Hangfire 默認會 retry 10 次, 每次 retry 都會有一個間隔時間.

它的 delay 算法是

如果想修 retry count 和 delay, 可以放 AutomaticRetryAttribute 到 job 方法上, 0 表示不要 retry, AttemptsExceededAction.Delete | Fail 意思是 error 以后是否要把這個 job 洗掉后者留一個 status fail 做計入 (這個不影響它 retry).

public class EmailService : IEmailService
{
    [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
    public void SendEmail(int enquiryId)
    {

    }
}

由於它有 retry 的機制, 所以在設計 job 時, 需要做 transaction 確保原子性, 或者把方法做成冪等,

2. Miss execute time

Server down, retry 都有可能導致 job 運行的時間和預想的不一致. 比如預設每星期一凌晨 12 點跑.

結果那個時段 server down 了, Hangfire 會在 server up 的時候立刻補上錯過的 job. 

Retry 的 delay 間隔, 也會造成運行時間和預期不符. 

所以在設計 job 時也需要考慮到時間.

3. 超時任務

job 運行太耗時, 與至於下一次的運行也啟動了. 這時就容易出現混亂. 這個視乎是風水的問題. 應該避免大任務執行, 把它切分成小任務.

部分部分去 complete.

 

數據結構

Job 是查看所有運行過的 job, 不管是成功失敗.

State 是所有 job 每一次 state change 的記入, 包括了 Enqueued, Processing, Succeeded 等

Set 是 recurring job 的 definition, crod expression 這些

其它就比較少會去看.

 

Dashboard

26-01-2022 Issue: Dashboard page blank after upgrade to .net 6.0

hot reload 和 Hangfire dashboard 撞. 目前沒有看到有 github issue. wordaround 是關掉 hot reload.

訪問 /Hangfire 就可以看見 build-in 的 dashboard 了

這里還可以 manual trigger job 或者移除 job 哦. 這也是 Hangfire 的一大賣點.

想自定義 url 可以這樣做

app.MapHangfireDashboard("/jobs");

Read-only

app.MapHangfireDashboard("/Hangfire", new DashboardOptions
{
    IsReadOnlyFunc = (DashboardContext context) => true
});

Authorize

默認只有 dev 情況下才能無授權訪問 dashboard, 通過自定義 IDashboardAuthorizationFilter 就可以設定權限訪問.

public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        // Allow all authenticated users to see the Dashboard (potentially dangerous).
        return httpContext.User.Identity.IsAuthenticated;
    }
}

注: UseHangfireDashboard 要在 authentication, authorize middleware 之后. 

app.MapHangfireDashboard("/Hangfire", new DashboardOptions
{
    Authorization = new [] { new MyAuthorizationFilter() }
});

上面這個方法比較過時了, 如果是有搭配 login 界面的話推薦使用 MapHangfireDashboardWithAuthorizationPolicy 來做

builder.Services.AddAuthorization(options =>
    options.AddPolicy("HangfireDashboard", policy => policy.RequireAuthenticatedUser())
);
app.MapHangfireDashboardWithAuthorizationPolicy("HangfireDashboard");

還有一招是用 basic authentication, 參考: Stack Overflow – ASP.NET Core MVC Hangfire custom authentication

 

MapHangfireDashboard vs UseHangfireDashboard

Map 是 endpoint routing 的版本, 盡量用 Map 唄

 

Error : JobStorage.Current property value has not been initialized

如果沒有調用 app.MapHangfireDashboard 而直接使用 RecurringJob.AddOrUpdate 是會報錯的.

相關提問: Stack Overflow – JobStorage.Current property value has not been initialized. You must set it before using Hangfire Client or Server API

解決方式有 2 種

1. 調用 MapHangfireDashboard

2. RequiredService<JobStorage> 激活它一下

app.Services.GetRequiredService<JobStorage>();
RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), Cron.Daily(hour: 6));

 

Multiple Server

今天突然發現 job duplicated 了. 然后發現既然有 2 個 server instance.

后來發現, 原來是我忘了把 staging server 關掉. 低級錯誤. 哈哈

下面這個代碼可以查看當前的 server instance 有哪些

var monitoringApi = JobStorage.Current.GetMonitoringApi();
var removingServers = monitoringApi.Servers().Where(e => e.Name.Contains("myserver")).ToList();
removingServers.ForEach(removingServer => JobStorage.Current.GetConnection().RemoveServer(removingServer.Name));

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM