ABP后台服务 - 后台作业和后台工人
7.1 ABP后台服务 - 后台作业和后台工人
7.1.1 简介
ABP提供了后台作业和后台工人,来执行应用程序中的后台线程的某些任务。
7.1.2 后台作业
由于各种各样的原因,你需要后台作业以队列和持久化的方式来排队执行某些任务。
例如:
用户等待执行一个长时任务。例如:某个用户按下了报表按钮生成一个需要长时间等待的报表。你添加这个工作到队列中,当报表生成完毕后,发送报表结果到该用户的邮箱。
重试创建并持久化任务操作,以保证任务的成功执行。例如:在后台作业中发送一封邮件,有些问题可能会导致发送失败(网络连接异常,或者主机宕机);由于有后台作业以及持久化机制,在问题排除后,可以重试执行失败的任务,以保证任务的成功执行。
关于作业持久化
了解更多信息可以查看源码:Background Job Store
1. 创建后台作业
通过继承 BackgroundJob\ 类或者直接实现 IBackgroundJob\ 接口,我们可以创建一个后台作业。
下面是一个极其简单的后台作业示例:
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;
}
[UnitOfWork]
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);
}
}
我们注入了用户仓储(取得用户邮箱信息)和邮件发送服务,简单实现邮件的发送。在这里 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] 特性,因为我们可以自由的切换到其他作业管理中,以后可能会使用Binary序列化等等。
参数应该做到简洁,不应该包含实体或者其他非序列化的对象。正如 SimpleSendEmailJob 所展示的,我们应该仅存储实体的Id并从仓储内得到该实体的作业。
2. 添加作业到队列中
在定义后台作业后,我们可以注入并使用 IBackgroundJobManager 接口来添加作业到队列中。
上面我们已经定义了TestJob类,让我们来看下如何使它排队:
public class MyService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public MyService(IBackgroundJobManager backgroundJobManager)
{
_backgroundJobManager = backgroundJobManager;
}
public void Test()
{
_backgroundJobManager.Enqueue<TestJob, int>(42);
}
}
我们以42作为参数来排队。IBackgroundJobManager将会被实例化,并使用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 (or EnqueueAsync) 方法还有其它参数如:priority 和 delay。
3. 后台作业管理
BackgroundJobManager 默认实现了IBackgroundJobManager接口。它能够被其它的后台作业提供者给替换掉(如:集成Hangfire)。在 BackgroundJobManager 中默认定义了一些信息:
在单线程中,它是一种简单的 FIFO(先进先出) 的作业队列。使用 IBackgroundJobStore 来持久化作业。
对作业重试执行,直到作业执行成功(不抛出任何异常但是记录进日志)或者操作超时。默认作业超时设置是2天。
当作业执行成功后,作业将会从数据库中删除。如果执行超时,那么该作业会被设置为 abandoned 并保留在数据库中。
作业重试执行的时间间隔会慢慢增长。第一次重试是等待1分钟,第二次是等待2分钟,第三次是等待4分钟等等。
以固定的时间间隔轮询存储中的作业。查询作业是通过优先级和重试次数来排序的。
后台作业存储
BackgroundJobManager 默认是需要数据存储来保存和检索作业的。如果你没有实现 IBackgroundJobStore,那么它会使用 InMemoryBackgroundJobStore 来存储作业,当然作业不会被持久化到数据库中。你可以简单的实现它来存储作业到数据库中或者你可以在module-zero直接使用,因为它已被实现。
如果你使用了第三方作业管理(如:Hangfire),那就不需要实现 IBackgroundJobStore。
4. 配置
你可以在你的模块方法:PreInitialize 中,使用 Configuration.BackgroundJobs 配置后台作业系统。
禁用后台作业
你可能想在你的应用中禁用后台作业:
public class MyProjectWebModule : AbpModule
{
public override void PreInitialize()
{
Configuration.BackgroundJobs.IsJobExecutionEnabled = false;
}
//...
}
这种需求及其少见。但是想象一下,同时开启同一个应用程序的多个实例且使用的是同一个数据库。在这种情况下,每个应用程序的作业将查询相同的数据库并执行它们。这会导致相同的任务的多次执行,并还会导致其它的一些问题。为了阻止这个,你有两个选择:
你可以仅为你应用程序的一个实例开启后台作业
你可以禁用Web应用的所有实例的后台作业,并且创建一个独立的应用程序(如:Windows服务)来执行你的后台作业。
5. 异常处理
由于后台作业管理默认应该重试失败的作业,它会处理(并记录日志)所有的异常。如果你想当异常发生的时候被通知,你可以创建事件处理来处理 AbpHandledExceptionData。后台会触发这个事件并且附上一个 BackgroundJobException 异常对象,该对象包裹了真实的异常(对该异常获取 InnerException)。
6. 集成Hangfire
后台作业管理者可以被其他后台作业管理者替换。详情请参考集成Hangfire
7.1.3 后台工人
后台工人是不同于后台作业的。在应用程序中,它们是运行在后台单个线程中。一般来说,它们周期性的执行一些任务,例如:
后台工人能够周期性地执行旧日志的删除
后台工人可以周期性地确定非活跃性用户并且发送邮件给这些用户,使这些用户返回到你的网站中。
1. 创建后台工人
创建后台工人,我们应该实现 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();
}
}
}
这是一个真实性的代码,你可以在module-zero找到。
如果派生自 PeriodicBackgroundWorkerBase,你需要实现 DoWork 方法,在该方法中实现你周期性执行的逻辑。
如果派生自 BackgroundWorkerBase 或者直接实现 IBackgroundWorker 接口,你将要重写/实现 Start,Stop, WaitStop 方法。 Start 和 Stop 是非阻塞方法, WaitStop应该等待后台工人完成当前的重要任务。
2. 注册后台工人
在创建后台工人后,我们应该添加它到 IBackgroundWorkerManager。通常是在我们的模块的 PostInitialize 方法中配置:
public class MyProjectWebModule : AbpModule
{
//...
public override void PostInitialize()
{
var workManager = IocManager.Resolve<IBackgroundWorkerManager>();
workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>());
}
}
一般情况我们都是在PostInitialize做的,这没有严格的要求。你能够在任何地方注入IBackgroundWorkerManager并添加后台工人到运行时中。当你的应用程式停止后,IBackgroundWorkerManager将停止和释放所有被注册过的后台工人。
3. 后台工人生命周期
后台工人通常是作为单例来执行的。这里也没有严格的限制,如果你需要同一个后台工人在多个实例中,你可以使它做为瞬时对象并添加多个实例到IBackgroundWorkerManager,在这种情况下,后台工人将被参数化(你有一个LogCleaner类,但是有2个后台LogCleaner工人实例,它们会看到和清除不同的日志文件夹)。
4. 高级计划
ABP的后台worker系统是相当简单的。如上所述,除了周期性的运行workes,它没有调度系统。如果你想要更高级的调度系统功能,建议你使用 Quartz或者其他类库。
7.1.4 使你的应用程序一直运行
后台作业和后台工人仅在你的应用程序运行的时候才工作。Asp.Net应用默认是关闭的, 如果web应用长时间没有被请求执行。所以,如果你的后台作业是宿主在Web应用中执行的,你应该确保你的web应用是被配置为一直执行的。否则,后台作业仅在你的web应用在使用的时候才会执行。
有一些技术可以实现这个,最简单的方法是用一个外部程序向你的web应用周期性的发送请求。因此,你也可以检测你的web应用是否启动并运行。Hangfire文档中描述了另外的实现方法。