[ASP.NET]使用 Hangfire 來處理非同步的工作

有功能讓使用者等到火大嗎?
可以使用 Hangfire 來處理非同步的工作哦!

問題

在前一篇「好用的 HostingEnvironment.QueueBackgroundWorkItem」,

有使用 QBWI 來處理非同步,但是如果發生錯誤要自已去處理。

也想要 使用 Polly 來讓程式有 Retry 的機制 ,但似乎也有許多的地方要自行處理的。

所以就改用 Hangfire 來試看看,主要是因為他會先將要執行的 Task 存到 DB中,然後再去執行它,而且還提供Dashboard。

下圖是筆者要解的問題,主要是表單系統呼叫Workflow起單時,需要花費太多的時間,所以就將起Workflow Task放到 Hangfire 裡面去。

image

 

實作

1.建立 Hangfire 所需的Table。

從「Hangfire.SqlServer/Install.sql」拿 Script Run 到 DB中(我是在WebAP中安裝 SQL Express 來放 Hangfire的資料),或是給有建立Table的帳號,執行 Hangfire會幫你建立這些Table。

2.在現有的 Workflow Services Application 使用 Hangfire 。

2.1.將版本調整成 .NET 4.5 (Hangfire最新版是使用.NET 4.5,也有 .NET 4.0版本的)

2.2.安裝 Hangfire.Core 及 Hangfire.SqlServer

因為只是要將 Task 寫進 Hangfire 所以只要安裝這2個 package 就可以了。

image

image

 

2.3.在 Global.asax.cs 的 Application_Start 設定 Hangfire 使用的DB連線字串 (因為我們要將Task寫到Hangfire去),如下,


protected void Application_Start(object sender, EventArgs e)
{
	GlobalConfiguration.Configuration
		.UseSqlServerStorage("連到hangfire的DB連線名稱 or 字串");
}

 

2.4.在需要執行 Task 的地方,呼叫 BackgroundJob.Enqueue (如果呼叫端需要回傳值的話,可以做一個假的回給它),如下,


BackgroundJob.Enqueue(() => StartMyTask(apid, startInfo, extendInfo));
//使用 Hangfire 去執行的話, nested obj 會 parse 不出來,所以用字串傳遞
public void StartMyTask(string arg1, string arg2, string arg3)
{
	// 要執行的事項
}

 

這裡有幾個部份要注意,

2.4.1.要執行的 Task,用一個 Method 封裝起來比較方便。

2.4.2.執行的 Method 參數最好是字串,如果是物件的話,不要是 nested 物件,這樣會Parse不出來,然後報錯哦!

 

 

3.把資料放到 Hangfire 後,就要建立Web API 專案來讓 Hangfire 去執行 Task 及提供 Dashboard。

3.1.新增一個名叫 HangfireDashboard 的 Web API 專案(.NET 4.5 以上)。

3.2.安裝 Hangfire 套件

image

 

3.3.再來依「Making ASP.NET application always running」方式來讓 HangfireDashboard 可執行 Task 及提供 Dashboard。

3.3.1.新增 HangfireBootstrapper Class


public class HangfireBootstrapper : IRegisteredObject
{
	public static readonly HangfireBootstrapper Instance = new HangfireBootstrapper();

	private readonly object _lockObject = new object();
	private bool _started;

	private BackgroundJobServer _backgroundJobServer;

	private HangfireBootstrapper()
	{
	}

	public void Start()
	{
		lock (_lockObject)
		{
			if (_started) return;
			_started = true;
			HostingEnvironment.RegisterObject(this);
			GlobalConfiguration.Configuration
				.UseSqlServerStorage("連到hangfire的DB連線名稱 or 字串")
				.UseNLogLogProvider(); // 也可以使用 NLog, 請記得安裝 NLog 及 NLog config 哦
				
			// 建立Background JobSserver 來處理 Job
			_backgroundJobServer = new BackgroundJobServer();
		}
	}

	public void Stop()
	{
		lock (_lockObject)
		{
			if (_backgroundJobServer != null)
			{
				_backgroundJobServer.Dispose();
			}
			HostingEnvironment.UnregisterObject(this);
		}
	}

	void IRegisteredObject.Stop(bool immediate)
	{
		Stop();
	}
}

 

3.3.2.新增 ApplicationPreload Class (詳細可以參考:Speeding up your application with the IIS Auto-Start feature )


public class ApplicationPreload : System.Web.Hosting.IProcessHostPreloadClient
{
	public void Preload(string[] parameters)
	{
		HangfireBootstrapper.Instance.Start();
	}
}

 

3.3.3.在 global.asax.cs 中,去啟動或停止 BackgroundJob


public class WebApiApplication : System.Web.HttpApplication
{
	protected void Application_Start()
	{
		AreaRegistration.RegisterAllAreas();
		System.Web.Http.GlobalConfiguration.Configure(WebApiConfig.Register);
		FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
		RouteConfig.RegisterRoutes(RouteTable.Routes);
		BundleConfig.RegisterBundles(BundleTable.Bundles);
		//啟動 BackgroundJob
		HangfireBootstrapper.Instance.Start();
	}

	protected void Application_End(object sender, EventArgs e)
	{
		//停止 BackgroundJob
		HangfireBootstrapper.Instance.Stop();
	}
}

 

3.3.4.新增 Startup 來設定啟用 Dashboard。


public class Startup
{
	public void Configuration(IAppBuilder app)
	{
		//使用 Dashboard,可以設定顯示 dashboard 的 path
		app.UseHangfireDashboard("/ds");
	}
}

 

3.3.5.在 namespace 上面加入 OwinStartup 的設定

[assembly: OwinStartup(typeof(HangfireDashboard.Startup))]

 

註:如果您要設定 Access Dashboard 的權限,可以實作 Hangfire.Dashboard.IAuthorizationFilter ,如下,


public class AllowAllAuthorizationFilter : Hangfire.Dashboard.IAuthorizationFilter
{
	public bool Authorize(IDictionary<string, object> owinEnvironment)
	{
		//比較使用都的資訊,驗證OK就回傳true
		return true;
	}
}
//然後,在 Startup Class 裡使用它,如下,
public class Startup
{
	public void Configuration(IAppBuilder app)
	{
		//使用 Dashboard,並任何人 Access 它
		app.UseHangfireDashboard("/ds", new DashboardOptions
		{
			AuthorizationFilters = new[] { new AllowAllAuthorizationFilter() }
		});
	}
}

 

HangfireDashboard 建立好了之後,就可以開起來看一下狀況。

image

 

使用狀況

Hangfire如果Job有錯誤的話,會自動 Retry 10 次,而每次 Retry 的時間間隔會逾來逾久。超過10次後,就會留在 Failed 等待手動將它移進 Queue 之中,或是將它刪除,如下,

image

 

Job執行成功之後,會自動清 Detail 的資料。如下圖,雖然有94筆成功,但詳細資料的Job目前只留 8 筆而已,

image

 

而預設詢問Job是 15 秒,也可以依狀況來調整,如下我使用1分鐘,


var options = new BackgroundJobServerOptions
{
    SchedulePollingInterval = TimeSpan.FromMinutes(1)
};

var server = new BackgroundJobServer(options);

 

其他例如平行處理的數量(預設是 處理器數目 * 5)、Retry試次數也都是可以調整的哦!

在實作的過程中,主要遇到要如何切出 Task 放到 Hangfire 之中,以及放進 Hangfire 之前需要先做一些卡關的作業。

例如,請假需要先將假扣掉後,才把 Task 放進去。而批示時,也要先將資訊記到別的Table去,讓批示著認為已經批好了。

再來就是要考慮到如果Job真的無法成功時,對應的補償的機制。

 

參考資料

Hangfire Documentation

Processing jobs in a web application

Calling methods with delay

Hangfire.SqlServer/Install.sql

[ASP.NET]好用的 HostingEnvironment.QueueBackgroundWorkItem

Making ASP.NET application always running

介紹好用函式庫:NLog - Advanced .NET Logging