本節內容:
ABP提供了后台作業和工作者,用來在后台線程里執行應用里的某些任務。
后台作業用一種隊列且持久穩固的方式安排一些待執行后台任務,你可能有幾個理由,需要用到后台作業,例如:
- 為執行長時間運行的任務而用戶無需等待,例如:用戶按了一下“報告”按鈕開始一個長時間運行的報告任務,你把這個任務添加到隊列里,它任務完成后,通過電子郵件發送報告結果給你。
- 為創建可重試且持久穩固的任務來保證一個代碼將會被完成運行,例如:你可以在后台作業里發送一個電子郵件,為克服臨時的失敗且保證它最終將會被發送,當然當發送電子郵件時用戶也無需等待。
查看后台作業存儲小節獲取有關作業持久化的更多信息。
我們可以通過繼承BackgroundJob<TArgs>類或直接實現IBackgroundJob<TArgs>接口來創建一個后台作業。
下列為一個非常簡單的后台作業:
public class TestJob : BackgroundJob<int>, ITransientDependency { public override void Execute(int number) { Logger.Debug(number.ToString()); } }
一個后台作業定義了一個Execute方法,接受一個輸入參數,參數類型就是定義泛型類的參數,如上例所示。
一個后台作業必須注冊到依賴注入,實現ITransientDependency是最簡單的方式。
讓我們定義一個更真實的作業:在后台隊列里發送電子郵件:
public class SimpleSendEmailJob : BackgroundJob<SimpleSendEmailJobArgs>, ITransientDependency { private readonly IRepository<User, long> _userRepository; private readonly IEmailSender _emailSender; public SimpleSendEmailJob(IRepository<User, long> userRepository, IEmailSender emailSender) { _userRepository = userRepository; _emailSender = emailSender; } public override void Execute(SimpleSendEmailJobArgs args) { var senderUser = _userRepository.Get(args.SenderUserId); var targetUser = _userRepository.Get(args.TargetUserId); _emailSender.Send(senderUser.EmailAddress, targetUser.EmailAddress, args.Subject, args.Body); } }
我們注入user倉儲(可獲取用戶電子郵件)和郵件發送器(一個發送郵件的服務),並簡單的發送郵件,SimpleSendEmailJobArgs是作業的參數,它的定義如下所示:
[Serializable] public class SimpleSendEmailJobArgs { public long SenderUserId { get; set; } public long TargetUserId { get; set; } public string Subject { get; set; } public string Body { get; set; } }
一個作業的參數應當可序列化,因為它要被序列化后存儲到數據庫,雖然ABP默認后台作業管理器使用JSOn序列化器(它不需要使用[Serializable]特性),更好還是定義為[Serializable],因為將來我們可能會替換成另一個作業管理器,可能會使用.net內置的二進制序列化器。
保存你的參數簡單(如DTO),不要包含實體或其它非序列化對象,如所示的SimpleSendEmailJob,我們可以只存儲一個實體的Id,並通過它從作業內部的倉儲里獲取實體。
在定義完一個后台作業之后,我們可以注入並使用IBackgroundJobManager給隊列添加一個作業,看一下使用上面已定義的TestJob的例子:
public class MyService { private readonly IBackgroundJobManager _backgroundJobManager; public MyService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public void Test() { _backgroundJobManager.Enqueue<TestJob, int>(42); } }
當加入隊列時我們發送參數42, IBackgroundManager將實體化並以42為TestJob的參數執行它。
讓我們看一下使用上面定義的SimpleSendEmailJob的例子:
[AbpAuthorize] public class MyEmailAppService : ApplicationService, IMyEmailAppService { private readonly IBackgroundJobManager _backgroundJobManager; public MyEmailAppService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public async Task SendEmail(SendEmailInput input) { await _backgroundJobManager.EnqueueAsync<SimpleSendEmailJob, SimpleSendEmailJobArgs>( new SimpleSendEmailJobArgs { Subject = input.Subject, Body = input.Body, SenderUserId = AbpSession.GetUserId(), TargetUserId = input.TargetUserId }); } }
Enqueue(或EnqueueAsync) 有其它參數,如priority和delay。
BackgroundJobManager默認實現了IBackgroundJobManager,它可被其它后台作業提供器替代(查看Hangfire文檔)。如下為一些關於默認BackgroudJobManager的信息:
- 它是一個簡單的作業隊列,以FIFO(先進先出)方式單線程作業,它使用IBackgroundJobStore來持久化作業(見下一小節)。
- 它一直重試作業執行直到作業成功運行(只記錄日志不拋出異常)或超時,默認超時為一個作業2天。
- 在作業成功運行后,它從存儲(數據庫)里刪除這個作業,如果超時了,就把這個作業設置為“被拋棄的”,然后離開數據庫。
- 它在重試一個作業之間遞增等待時間,第一次重試,等待1分鍾,第二次重試,等待2分鍾,第三次重試,等待4分鍾,如此類推。
- 它在固定的間隔里給作業的存儲投票,查詢作業按優先級(升序)排序,然后按嘗試次數(升序)排。
默認的BackgroundJobManager需要一個數據存儲來保存和獲取作業,如果你沒有實現IBackgroundJobStore,它會使用InMemoryBackgroundJobStore,它不在持久化的數據庫中保存作業,你可以簡單的實現這個接口,讓作業存儲到一個數據庫或使用已經實現該接口的module-zero。
如果你使用第三方的作業管理器(如Hangfire),不需要實現IBackgroundJobStore。
你可以在模塊的PreInitialize方法里,使用Configuration.BackgroundJobs配置你的后台作業系統。
你可能會想為你的應用禁用后台作業執行:
public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.IsJobExecutionEnabled = false; } //... }
很少需要這樣,但考慮一下你正在運行一個應用的多個實例並訪問同一個數據庫,這種情況下,每個應用將向同個數據庫查詢作業並執行它們,這可能導致同個作業的多次執行和一些其它問題,為阻止這種情況,我們有兩個選擇:
- 你可以只允許應用的一個實例來完成作業的執行。
- 你可以禁用應用的所有實例執行作業,再單獨創建一個應用(如:一個windows服務)來執行后台作業。
后台作業管理器設計成可被其它后台管理器所替換,查看Hangfire集成文檔如何用Hangfire代替。
后台工作者與后台作業不同,它簡單的依賴應用在后台運行的線程,通常地,它定期執行一些任務,例如:
- 一個后台工作者可以定期刪除舊日志。
- 一個后台工作者可以定期檢測不活躍的用戶,然后發郵件給他們,讓他們重新使用你的應用。
為創建一個后台工作者,我們應當實現IBackgroundWorker接口,我們還可以選擇直接從BackgroundWorkerBase或PeriodicBackgroundWorkerBase基類上繼承。
假設我們想把超過30天未登錄的用戶設置為“消極”的,代碼如下:
public class MakeInactiveUsersPassiveWorker : PeriodicBackgroundWorkerBase, ISingletonDependency { private readonly IRepository<User, long> _userRepository; public MakeInactiveUsersPassiveWorker(AbpTimer timer, IRepository<User, long> userRepository) : base(timer) { _userRepository = userRepository; Timer.Period = 5000; //5 seconds (good for tests, but normally will be more) } [UnitOfWork] protected override void DoWork() { using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) { var oneMonthAgo = Clock.Now.Subtract(TimeSpan.FromDays(30)); var inactiveUsers = _userRepository.GetAllList(u => u.IsActive && ((u.LastLoginTime < oneMonthAgo && u.LastLoginTime != null) || (u.CreationTime < oneMonthAgo && u.LastLoginTime == null)) ); foreach (var inactiveUser in inactiveUsers) { inactiveUser.IsActive = false; Logger.Info(inactiveUser + " made passive since he/she did not login in last 30 days."); } CurrentUnitOfWork.SaveChanges(); } } }
這是一段真實的代碼,它工作在ABP的module-zero里。
- 如果你從PeriodicBackgroundWorkerBase繼承(如這個例子),需要實現DoWork方法來執行你的定期工作。
- 如果你從BackgroundWorkerBase繼承或直接實現IBackgroundWorker,需要重寫/實現Start、Stop和WaitToStop方法,Start和Stop方法應當是非阻塞的,WaitToStop方法需要等待工作者完成它當前的工作。
在完成創建后台工作者后,需要把它添加到IBackgroundWorkerManager,非常通用的地方是:你模塊的PostInitialize方法里:
public class MyProjectWebModule : AbpModule { //... public override void PostInitialize() { var workManager = IocManager.Resolve<IBackgroundWorkerManager>(); workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>()); } }
雖然我們通常在PostInitialize里添加工作者,但不是一定要這樣,你可以在任何地方注入IBackgroundWorkerManager,然后在運行時添加工作者。
當你應用關閉時,IBackgroundWorkerManager將停止並釋放所有已注冊的工作者。
后台工作者以單例模式被創建,但也不是一定要這樣,如果你需要同個工作者類的多個實例,你可以使它是“暫時的”並添加多個實例到IBackgroundWorkermanager,在這種情況下,你的工作者可能需要參數(假設你有一個單獨的LogCleaner類,但有兩個LogCleaner工作者實例用來監視和刪除不同的日志目錄)。
只有當你的應用運行時,后台作業和工作者才能工作,如果一個Web應用長時間沒有收到訪問請求,它默認地會被關閉,所以,如果你的宿主后台作業運行在你的web應用里(這是默認行為),你應當確保你的web應用被配置成一直運行,否則,只有當你的應用在使用的時候,后台作業才能工作。
這里有些技術可以做到這點,一個非常簡單的辦法是:從一個外部應用里定期訪問你的Web應用,從而你可以一直檢查你的web應用是否一直運行着。Hangfire文檔解釋了一些其它方法。
kid1412附:英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Background-Jobs-And-Workers