Web应用程式本身的机制并不适合用来作为执行需要长时程运行的需求,而这类需求却很常见,而常见的解决方式是
加长Timeout时间。使用Queue的机制于背景执行。第1种方式并不是一种正确的解决方式,严格来说不应该这样做。
第2种方式很容易达成,但有个缺点,使用者无法得知执行时的状态,因此无法确认到底是失败了还是仍然在执行中,这个问题可以透过一些设计解决,但都非常複杂。
目前在Asp.Net Core已经提供了「託管服务」IHostedService,可以解决Web本身不适合做为长时程执行的问题,然而单靠「託管服务」要实现整体的完整机制并不容易,而Quaerz.Net这个老牌的排程程式库能弭补这个问题,目前新版的Quaerz.Net 3已与Asp.Net Core已有很好的整合。
底下教学目标达成下列需求
允许Task于背景运行。能将执行过程中回报至UI上。完整的程式可由此下载
初始化Quartz.Net
首先透过nuget将Quartz.Net引用至专案
Install-Package Quartz.AspNetCore
修改Startup.cs加入Quartz.Net的初始化相关程式
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionScopedJobFactory(); q.UseSimpleTypeLoader(); q.UseInMemoryStore(); q.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 10; }); var jobKey = DemoJob.JobKey; q.AddJob<DemoJob>(jobKey, j => j .StoreDurably() .WithDescription("demo job") ); }); // ASP.NET Core hosting services.AddQuartzServer(options => { options.WaitForJobsToComplete = true; }); }
这里为了展示只简单的使用一些基本配置,想要更深入的了解Quartz.Net可以参考官方的quick start。
上面程式中有个地方须注意因为这边的DemoJob并没有关联到任何的Trigger,Quartz.Net预设情况下会自动删除因而会出现错误,因此要加上StoreDurably()。
撰写Job
public class DemoJob : IJob { public readonly static JobKey JobKey = new JobKey(nameof(DemoJob), JobKey.DefaultGroup); public async Task Execute(IJobExecutionContext context) { await Task.Run(() => { for (var i = 0; i < 5; i++) { var message = $"第{i}次执行"; Console.WriteLine(message); context.Result = message; context.CancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)); } }, context.CancellationToken); } }
DemoJob作为展示,单纯的透过Console.WriteLine输出执行状态到Asp.Net core的Host上,因此如果使用IIS作为Host画面上是不会看到任何输出,所以记得要切换至专案作为Host
上面程式中的context.Result为Quartz.Net中提供的回报执行状态机制,后面我们会透过这个参数来取得目前Job中的执行状态,其物件形态为Object也就是说可以自定义想需要的内容,这边单纯的字串来回报。
新增TaskController与NewTask方法,用来建立Task的Trigger,让UI触发执行。
public async Task<IActionResult> NewTask([FromServices] ISchedulerFactory schedulerFactory, CancellationToken cancellationToken) { var scheduler = await schedulerFactory.GetScheduler(cancellationToken); var trigger = TriggerBuilder.Create() .ForJob(DemoJob.JobKey) .StartNow() .WithSimpleSchedule(x => x.WithRepeatCount(0)) .Build(); await scheduler.ScheduleJob(trigger); return RedirectToAction("ExecutingJobs"); }
因为Quartz.Net本身目的是排程功能,而我们希望的Task只是单纯的执行一次,因此Trigger使用Simple并且设定WithRepeatCount为0,那么Trigger执行后即会被Quartz.Net删除。
最后提供一个UI用来显示目前执行中的状态
public async Task<IActionResult> ExecutingJobs([FromServices] ISchedulerFactory schedulerFactory, CancellationToken cancellationToken) { var scheduler = await schedulerFactory.GetScheduler(); var executingJobs = await scheduler.GetCurrentlyExecutingJobs(cancellationToken); var data = executingJobs.Select(x => new JobExecutionContextModel(x)); return View(data); }
这样就简单快速的完成了一个可以在WEB长时程执行的机制并且能够有UI反馈
当然Quartz.Net能做的不止于此,譬如说执行中的Task允许取消机制,实现如Windows排程功能,并且可以让使用透过UI来操作,集群机制等等。