后台Jobs和Workers
ABP提供了后台jobs和workers,他们用于在应用的后台线程中执行一些任务。
后台Jobs用来排队一些需要在后台队列或持久化的方式执行一些任务。以下几种情况可以使用后台jobs:
- 执行一些长时运行的任务而不需要用户等待。例如,用户按了一个'report'按钮开始一个长时运行的报表job。可以将这个job添加到队列,当执行完成的时候通过邮件发送给用户报表结果。
- 创建一个重试且持久化的任务来保证一段代码的成功运行。例如:可以在后台job中发送邮件来克服临时错误并保证它最终被发送。这样,当发送邮件时,用户不必等待。
参见后台Job存储了解更多关于job持久化的信息。
我们可以通过从BackgroundJob<TArgs>类或直接实现IBackgroundJob<TArgs>接口来创建一个后台job类。
这是最简单的后台job:
public class TestJob : BackgroundJob<int>, ITransientDependency { public override void Execute(int number) { Logger.Debug(number.ToString()); } }
后台job定义了Excute方法,它有一个输入参数。参数类型为泛型类参数。
后台job必须注册到依赖注入系统。实现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仓储(获得用户电子邮件)和电子邮件sender(发送邮件的服务)简单的发送邮件。SimpleSendEmailJobArgs为job的参数,定义如下:
[Serializable] public class SimpleSendEmailJobArgs { public long SenderUserId { get; set; } public long TargetUserId { get; set; } public string Subject { get; set; } public string Body { get; set; } }
job参数应该是可以序列化的,因为它被序列化并存储在数据库中。ABP默认后台job管理器使用JSON序列化(它不需要[Serilizable]特性),但是最好定义[Serializable]特性,因为我们可能在将来切换到其他job管理器,它可能使用.NET的内置二进制序列化。
保持参数简单(如DTOs),不要包含实体或其他非序列化的对象。如在SimpleSendEmailJob示例中所示,我们可以存储实体的Id并从实体的仓储中获得实体。
定义后台job之后,我们可以注入并使用IBackgroundJobManager来添加job到队列。参见TestJob定义示例:
public class MyService { private readonly IBackgroundJobManager _backgroundJobManager; public MyService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public void Test() { _backgroundJobManager.Enqueue<TestJob, int>(42); } }
当入队时,我们使用42作为参数。IBackgroundJobManager将实例化并执行TestJob,参数为42。
让我们添加一个上面定义的新的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 }); } }
Enque(或EnqueueAsync)方法还有其他参数如priority和delay。
IBackgroundJobManager默认由BackgroundJobManager实现。它可以被其他后台job提供者(参见hangfire集成)取代。BackgroundJobManager的一些参考信息:
- 它是一个单线程的FIFO的简单job队列。它使用IBackgroundJobStore来持久化jobs(参见下部分)。
- 在job成功运行(不抛出任何异常但是会记录)或超时前会一直重试。默认的重试时间为2天。
- 当成功执行后,它从仓储(数据库)中删除job。如果它超时了,就设置为abandoned并保留在数据库中。
- 每次重试前它会递增时间。第一次尝试等待1分钟,第二次等待2分钟,第三次等待4分钟...
- 它以固定间隔轮训jobs仓储。按优先级(升序)、重试次数(升序)排列查询jobs。
默认的BackgroundJobManager需要一个数据仓储来保存和获取jobs。如果你没实现IBackgroundJobStore那么它使用InMemoryBackgroundJobStore,它不在持久化的数据库中存储jobs。你可以简单的实现它来在数据库中存储jobs或者可以使用已经实现了它的module-zero。
如果你使用了第三方的job管理器(如Hangfire),就不需要实现IBackgroundJobStore了。
你可以在模块的PreInitialize方法中使用Configuration.BackgroundJobs来配置后台job系统。
你可以禁用应用程序的后台job执行:
public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.IsJobExecutionEnabled = false; } //... }
几乎不需要这样做。但是,假如你的应用运行了多个实例并使用同样的数据库(在web farm中)。在这种情况下,每个应用将使用同样的数据库查询job并执行。这会导致同样job的多次执行及其他的问题。为了阻止这种情况,有两种选择:
- 你可以只为应用程序的一个实例启用job执行。
- 你可以为应用程序的所有实例禁用job执行,然后创建一个单独的执行后台jobs的应用程序(例如:windows service)。
因为默认的后台管理器会自动重试失败的jobs,它处理(并记录)所有的异常。如果当异常发生时你想要得到通知,可以创建一个事件处理者来处理AbpHandledExceptionData。后台管理器触发这个事件,它有一个BackgroundJobException异常对象参数,这个参数包装了真正的异常(获取InnerException获取真正的异常)。
后台管理器设计为可以被其他后台job管理器代替。参见hangfire集成文档,使用Hangfire替换它。
后台Workers与后台Jobs不同。他们为在后台运行的简单独立的线程。通常,他们定期执行一些任务。例如:
- 后台worker可以定期的删除老的日志。
- 后台worker可以定期的判定不活动的用户并发送邮件来通知用户。
创建一个后台worker,我们需要实现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 moudle-zero中使用的真实代码。
- 如果你从PeriodicBackgroundWokerBase(如在本例中)继承,需要实现DoWork方法来执行你的定期执行代码。
- 如果你从BackgroundWorkerBase继承或直接实现IBackgroundWorker接口,你需要重写/实现Start,Stop和WaitToStop方法。Start和Stop方法需要为非阻塞的,WaitToStop方法需要等待worker完成它当前关键的job。
创建后台worker后,我们需要把它添加到IBackgroundWorkerManger。最常见的地方为模块的PostInitialize方法:
public class MyProjectWebModule : AbpModule { //... public override void PostInitialize() { var workManager = IocManager.Resolve<IBackgroundWorkerManager>(); workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>()); } }
我们通常在PostInitialize方法中添加worker,但并不限制。你可以在任何地方注入IBackgroundWorkerManager并添加worker。当应用程序关闭时,IBackgroundWorkerManager将停止并释放所有注册的worker。
后台workder一般实现为单例模式。但是并没有限制。如果你需要同个worker类的多个实例,你可以使它为瞬时的并添加多个实例到IBackgroundWorkerManager。在这种情况下,你的worker将为参数化的(也就是说你有一个单独的LogCleaner类但是两个LogCleaner worker实例他们监视并清理不同的日志文件夹)。
ABP后台worker系统比较简单。除了上面展示的定期执行worker,它没有计划安排系统。如果你需要更多计划安排的特征,我们建议你使用Quartz或其他类库。
后台jobs和workers只有当你应用程序运行时才会工作。如果长时间web应用没有请求执行,ABP应用默认会关闭。所以,如果你在web应用中(这是默认行为)寄宿后台job的执行,你需要保证你的web应用配置为一直执行。否则,后台job只有应用在使用时才能工作。
有一些技术可以实现。最简单的方式是使用外部应用定期请求你的web应用。这样,你可以检查文本应用是否已启动并运行。Hangfire文档解释了一些其他的方式。