❤️ 关注 Furion 微信公众号有惊喜哦!
🫠 遇到问题
问题反馈
到 Furion 开源仓库反馈
成为赞助商
支持 Furion 的开源事业
Skip to main content

26.1 调度作业

📝 模块更新日志
  • 新特性

    •   定时任务看板列表支持作业分组名排序 4.8.8.43 ⏱️2023.09.14 #I7YQ9V
    •   定时任务作业计划 OnChanged 事件处理 4.8.8.29 ⏱️2023.06.25 e4c4cf1
    •   定时任务支持二级虚拟目录 VisualPath 配置部署 4.8.8.20 ⏱️2023.05.18 #I740IA
    •   定时任务作业处理程序工厂 IJobFactory 支持 4.8.8.13 ⏱️2023.05.08 ad58dd3
    •   定时任务 Schedular.CompileCSharpClassCode(code) 支持动态编译作业处理程序代码 4.8.8.7 ⏱️2023.04.30 fe1e8a1
    •   定时任务支持配置 IJob 执行异常 FallbackAsync 回退策略 4.8.8.6 ⏱️2023.04.25 7671489
    •   定时任务支持在非 IOC/DI 项目类型中使用 4.8.8.5 ⏱️2023.04.24 #I6YJNB
    •   定时任务看板支持自定义刷新频率 SyncRate 功能 4.8.7.43 ⏱️2023.04.12 703b465
    •   定时任务看板支持完全自定义 RequestPath 入口地址功能 4.8.7.34 ⏱️2023.04.04 24736f6
    •   定时任务一系列 .AlterTo 修改作业触发器触发时间便捷方法 4.8.7.31 ⏱️2023.03.31 0349017
    •   定时任务看板 UI 作业列表 最近执行时间 列和优化显示效果 4.8.7.12 ⏱️2023.03.15 26462a8 cb5dd17
    •   定时任务作业计划/工厂立即执行 RunJob 方法 4.8.7.11 ⏱️2023.03.15 #I6LD9X
    •   定时任务看板 UI 提供立即执行功能 4.8.7.11 ⏱️2023.03.15 #I6LD9X
    •   定时任务作业执行上下文 JobExecutionContext 服务提供器 ServiceProvider 属性 4.8.7.10 ⏱️2023.03.14 02586f8
    •   定时任务 HTTP 作业,支持定时请求互联网 URL 地址 4.8.7.7 ⏱️2023.03.11 01d4466
查看变化
services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
    •   定时任务作业触发器 Trigger 执行结果 Result 和执行耗时 ElapsedTime 属性 4.8.7.7 ⏱️2023.03.11 01d4466
    •   定时任务作业看板支持查看作业触发器执行结果 Result 和执行耗时 ElapsedTime 属性 4.8.7.7 ⏱️2023.03.11 01d4466
    •   定时任务休眠时长和唤醒时机日志输出 4.8.7.6 ⏱️2023.03.08 #I6LANE
    •   定时任务 IScheduler.[Try]UpdateDetail(builder => {})IScheduler.[Try]UpdateTrigger(triggerId, builder => {}) 重载方法 4.8.6 ⏱️2023.02.08 6e43a54
查看变化
  • 更新作业信息
// 返回 ScheduleResult 类型
var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
}, out var jobDetail);

// 无返回值
scheduler.UpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
});
  • 更新作业触发器
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
}, out var trigger);

// 无返回值
scheduler.UpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
});
    •   定时任务 Dashboard 可自定义入口地址 /schedule 4.8.5.6 ⏱️2023.02.02 c5639f5
    •   定时任务执行上下文 RunId 属性,用于标识单次作业触发器执行 4.8.5.1 ⏱️2023.01.30 1aac470
    •   定时任务 Dashboard 查看作业触发器最近运行记录功能 4.8.4.3 ⏱️2023.01.03 e7d24d8
    •   定时任务作业触发器 trigger.GetTimelines() 获取最近 10 条运行记录列表 4.8.4.3 ⏱️2023.01.03 e7d24d8
    •   定时任务 Dashboard 看板 4.8.4 ⏱️2022.12.30 d3f9669
    •   定时任务 IScheduler.GetEnumerable() 方法,可将作业计划转换成可枚举字典 4.8.4 ⏱️2022.12.30 4d5235c
    •   定时任务配置选项 options.JobDetail.LogEnabled 配置,可自动输出执行日志 4.8.3.7 ⏱️2022.12.14 58d2c20
    •   定时任务 IScheduler 对象每次操作后自动刷新和提供手动刷新 Reload() 方法 4.8.3.3 ⏱️2022.12.09 #I65EQ1
    •   定时任务间隔分钟作业触发器 Triggers.PeriodMinutes(5)[PeriodMinutes(5)] 特性 4.8.2.8 ⏱️2022.12.01 8e1f06f
    •   定时任务工作日作业触发器 Triggers.Workday()[Workday] 特性 4.8.2.6 ⏱️2022.11.30 28b2d20
    •   定时任务作业校对功能,可对误差进行校正 4.8.2.6 ⏱️2022.11.30 f725a25
    •   定时任务 Triggers 所有带 AtCron 表达式触发器构建器及特性 4.8.2.5 ⏱️2022.11.29 #I63PLR
    •   定时任务批量添加 SchedulerBuilder 作业功能 4.8.2.4 ⏱️2022.11.29 5faa67b
    •   定时任务 BuildSqlType 配置,可设置生成不同数据库类型的 SQL 语句 4.8.2.3 ⏱️2022.11.29 293f9bc !675
    •   JobDetailTrigger 自定义 ConvertToSQL 输出 SQL 配置 4.8.2 ⏱️2022.11.27 0bb9d8f
    •   作业触发器 ResetOnlyOnce 属性,支持只运行一次的作业重新启动服务重复执行 4.8.1.5 ⏱️2022.11.25 a8be728
    •   动态作业处理程序委托支持 4.8.1.8 ⏱️2022.11.27 e02266c
  • 突破性变化

    •   定时任务看板 SyncRate 配置,前后端采用最新的 SSE 推送技术替代 4.8.8.29 ⏱️2023.06.25 e4c4cf1
    •   定时任务动态作业 DynamicJob 委托/方法签名 4.8.7.10 ⏱️2023.03.14 6d56b53
查看变化

减少记忆负担,统一动态作业和普通作业的 ExecuteAsync 方法签名,故做出调整。

由:

options.AddJob((serviceProvider, context, stoppingToken) =>
{
serviceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));

调整为:

options.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
    •   定时任务底层所有代码,日志,注释,文档 4.8.1.10 ⏱️2022.12.05
  • 问题修复

    •   定时任务因上一版本修改 4e2615b 导致自定义作业触发器异常问题 4.8.8.36 ⏱️2023.07.06 #I7J59D
    •   定时任务因上一版本修改 4e2615b 导致 Cron 解析异常问题 4.8.8.32 ⏱️2023.06.28 #I7GQ5I
    •   定时任务设置额外数据不支持 long/int64 类型参数问题 4.8.8.31 ⏱️2023.06.28 4e2615b
    •   定时任务休眠毫秒数大于 int.MaxValue 时出现 ArgumentOutOfRangeException 4.8.8.27 ⏱️2023.06.21 #I7F6ZT
    •   定时任务通过作业 Id 删除作业不能删除作业触发器问题 4.8.7.35 ⏱️2023.04.05 312ca35
    •   定时任务作业状态为 积压:0归档:6 时调用立即执行后不能恢复上一次状态 4.8.7.18 ⏱️2023.03.21 6f5aae8
    •   定时任务更新作业 null 值默认被跳过问题 4.8.7.17 ⏱️2023.03.20 #I6OHO4
    •   定时任务生成 SQL 语句没有处理 ' 转义问题 4.8.7.15 ⏱️2023.03.19 #I6NXKA
    •   定时任务服务在停止进程时会卡住 30秒 问题 4.8.7.8 ⏱️2023.03.13 #I6MI9I #I6MHOU
    •   定时任务看板删除不存在的作业触发器出现空异常 4.8.7.7 ⏱️2023.03.11 01d4466
    •   定时任务 StartAll 出现个别作业显示 无触发时间 的状态 4.8.4.14 ⏱️2023.01.12 #I6A08X
    •   定时任务停止作业触发器后运行记录不能写入最新记录问题 4.8.4.8 ⏱️2023.01.05 d4c553f
    •   定时任务使用 Furion.Pure 包访问 Dashboard 出现 404 问题 4.8.4.2 ⏱️2023.01.02 21977b7
    •   定时任务通过 scheduler.RemoveTrigger(triggerId) 报异常问题 4.8.3.3 ⏱️2022.12.09 #I65EQ1
    •   定时任务作业触发器配置了 EndTimeStartTime 之后 Status 没有对应上 4.8.3.1 ⏱️2022.12.09 52a5506
    •   定时任务通过 scheduler.AddTrigger(triggerBuilder) 无效的问题 4.8.3.1 ⏱️2022.12.09 #I65EQ1
    •   作业拥有多个触发器时暂停作业后依然存在个别未暂停的清空(并发问题) 4.8.2.12 ⏱️2022.12.07 #I655W9
    •   作业触发器不符合下一次执行规律但 NextRunTime 不为 null 情况 4.8.1.5 ⏱️2022.11.25 a8be728
    •   运行时启动/暂停作业无效问题 4.8.1.6 ⏱️2022.11.25 #I6368M
    •   定时任务生成的 SQL 语句不支持 MySQL 问题 4.8.1.7 ⏱️2022.11.26 #I638ZC
  • 其他更改

    •   定时任务 GC 回收逻辑,避免高频添加作业导致 尾延迟 问题 4.8.8.3 ⏱️2023.04.21 #I6XIV8
    •   定时任务日志设计,减少不必要的日志输出 4.8.8.3 ⏱️2023.04.21 #I6XI2L
    •   定时任务动态委托作业持久化逻辑,采用不触发持久化操作 4.8.7.36 ⏱️2023.04.06 7bb58b6
    •   定时任务 Http 作业 HttpMethod 属性拼写错成 HttpMedhod 4.8.7.24 ⏱️2023.03.28 !756
    •   定时任务配置选项 BuilSqlType 属性命为 BuildSqlType 4.8.7.11 ⏱️2023.03.15 92117b8
    •   定时任务查看作业触发器运行记录由保存 10条 改为 5条 4.8.7.7 ⏱️2023.03.07 01d4466
    •   定时任务调度器时间精度,控制持续执行一年误差在 100ms 以内 4.8.2.9 ⏱️2022.12.01 334d089
    •   定时任务作业计划工厂 GetNextRunJobs() 方法逻辑 4.8.2.7 ⏱️2022.11.30 #I63VS2
  • 文档

    •   作业执行器实现超时文档 4.8.3.8 ⏱️2022.12.20
    •   作业触发器 ResetOnlyOnce 文档 4.8.1.5 ⏱️2022.11.25 a8be728
    •   通过 Roslyn 动态编译代码创建 IJob 类型文档 4.8.1.5 ⏱️2022.11.25 2c5e5be
    •   自定义 JobDetailTrigger 输出 SQL 文档 4.8.2 ⏱️2022.11.27 0bb9d8f
4.8.0 以下版本说明

Furion 4.8.0+ 版本采用 Sundial 定时任务替换原有的 TaskScheduler查看旧文档

版本说明

以下内容仅限 Furion 4.8.0 + 版本使用。

26.1.1 关于调度作业

调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。

26.1.1.1 使用场景

定时任务的应用场景非常广,几乎是每一个软件系统必备功能:

  • 叫你起床的闹钟
  • 日历日程提醒
  • 生日纪念日提醒
  • 定时备份数据库
  • 定时清理垃圾数据
  • 定时发送营销信息,邮件
  • 定时上线产品,比如预售产品,双十一活动
  • 定时发送优惠券
  • 定时发布,实现 Devops 功能,如 Jenkins
  • 定时爬虫抓数据
  • 定时导出报表,历史统计,考勤统计
  • ...

26.1.2 快速入门

  1. 定义作业处理程序 MyJob
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
  1. Startup.cs 注册 Schedule 服务:
services.AddSchedule(options =>
{
// 注册作业,并配置作业触发器
options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行
});
  1. 查看作业执行结果
info: 2022-12-02 16:51:33.5032989 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 16:51:33.5180669 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 16:51:34.1452041 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 16:51:34.1541701 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 16:51:34.1748401 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 16:51:35.0712571 +08:00 星期五 L MyJob[0] #4
<job1> [C] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 16:51:35.000 -> 2022-12-02 16:51:36.000
info: 2022-12-02 16:51:36.0317375 +08:00 星期五 L MyJob[0] #14
<job1> [C] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 16:51:36.000 -> 2022-12-02 16:51:37.000
info: 2022-12-02 16:51:37.0125007 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> * * * * * * 3ts 2022-12-02 16:51:37.000 -> 2022-12-02 16:51:38.000
info: 2022-12-02 16:51:38.0179920 +08:00 星期五 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> * * * * * * 4ts 2022-12-02 16:51:38.000 -> 2022-12-02 16:51:39.000

JobExecutionContext 重写了 ToString() 方法并提供以下几种格式:

# 持续运行格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 -> 下一次触发时间

# 触发停止格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 [触发器终止状态]

26.1.2.1 指定作业 Id

默认情况下,不指定作业 Id 会自动生成 job[编号]

services.AddSchedule(options =>
{
options.AddJob<MyJob>("myjob", Triggers.Secondly());
});

查看作业执行结果:

info: 2022-12-02 17:15:43.3024818 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:15:43.3107918 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:15:43.9498664 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <myjob_trigger1> trigger for scheduler of <myjob> successfully appended to the schedule.
info: 2022-12-02 17:15:43.9532894 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <myjob> successfully appended to the schedule.
warn: 2022-12-02 17:15:43.9941565 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:15:44.1230353 +08:00 星期五 L MyJob[0] #6
<myjob> [C] <myjob myjob_trigger1> * * * * * * 1ts 2022-12-02 17:15:44.000 -> 2022-12-02 17:15:45.000
info: 2022-12-02 17:15:45.0854893 +08:00 星期五 L MyJob[0] #9
<myjob> [C] <myjob myjob_trigger1> * * * * * * 2ts 2022-12-02 17:15:45.000 -> 2022-12-02 17:15:46.000
info: 2022-12-02 17:15:46.0100813 +08:00 星期五 L MyJob[0] #13
<myjob> [C] <myjob myjob_trigger1> * * * * * * 3ts 2022-12-02 17:15:46.000 -> 2022-12-02 17:15:47.000

26.1.2.2 多个作业触发器

有时候,一个作业支持多种触发时间,比如 每分钟 执行一次,每 5秒 执行一次,每分钟第 3/7/8秒 执行一次。

services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Minutely() // 每分钟开始
, Triggers.Period(5000) // 每 5 秒,还支持 Triggers.PeriodSeconds(5),Triggers.PeriodMinutes(5),Triggers.PeriodHours(5)
, Triggers.Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)); // 每分钟第 3/7/8 秒
});

查看作业执行结果:

info: 2022-12-02 17:18:53.3593518 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:18:53.3663583 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:18:54.0381456 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0708796 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0770193 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger3> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0800017 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 17:18:54.1206816 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:18:59.0040452 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger2> 5000ms 1ts 2022-12-02 17:18:58.927 -> 2022-12-02 17:19:03.944
info: 2022-12-02 17:19:00.0440142 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger1> * * * * * 1ts 2022-12-02 17:19:00.000 -> 2022-12-02 17:20:00.000
info: 2022-12-02 17:19:03.0149075 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 1ts 2022-12-02 17:19:03.000 -> 2022-12-02 17:19:07.000
info: 2022-12-02 17:19:03.9519350 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger2> 5000ms 2ts 2022-12-02 17:19:03.944 -> 2022-12-02 17:19:08.919
info: 2022-12-02 17:19:07.0116797 +08:00 星期五 L MyJob[0] #4
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 2ts 2022-12-02 17:19:07.000 -> 2022-12-02 17:19:08.000
info: 2022-12-02 17:19:08.0078132 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 3ts 2022-12-02 17:19:08.000 -> 2022-12-02 17:20:03.000
info: 2022-12-02 17:19:08.9298393 +08:00 星期五 L MyJob[0] #14
<job1> [C] <job1 job1_trigger2> 5000ms 3ts 2022-12-02 17:19:08.919 -> 2022-12-02 17:19:13.897
info: 2022-12-02 17:19:13.9056247 +08:00 星期五 L MyJob[0] #8
<job1> [C] <job1 job1_trigger2> 5000ms 4ts 2022-12-02 17:19:13.897 -> 2022-12-02 17:19:18.872
info: 2022-12-02 17:19:18.8791123 +08:00 星期五 L MyJob[0] #12
<job1> [C] <job1 job1_trigger2> 5000ms 5ts 2022-12-02 17:19:18.872 -> 2022-12-02 17:19:23.846

26.1.2.3 串行 执行

默认情况下,作业采用 并行 执行方式,也就是不会等待上一次作业执行完成,只要触发时间到了就自动执行,但一些情况下,我们可能希望等待上一次作业完成再执行,如:

services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 串行,每秒执行
});
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");
await Task.Delay(2000, stoppingToken); // 这里模拟耗时操作,比如耗时2秒
}
}

查看作业执行结果:

info: 2022-12-02 17:23:27.3726863 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:23:27.3830366 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:23:27.9083148 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:23:27.9184699 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 17:23:27.9740028 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:23:28.0638789 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 17:23:28.000 -> 2022-12-02 17:23:29.000
warn: 2022-12-02 17:23:29.1119269 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:29: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
warn: 2022-12-02 17:23:30.0090551 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:30: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
info: 2022-12-02 17:23:31.0121694 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 17:23:31.000 -> 2022-12-02 17:23:32.000
warn: 2022-12-02 17:23:32.0243646 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:32: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
串行 执行规则说明

串行 执行如果遇到上一次作业还未完成那么它会等到下一次触发时间到了再执行,以此重复。

默认情况下,使用 串行 执行但因为耗时导致触发时间到了但实际未能执行会默认输出 warn 警告日志,如需关闭只需要:

services.AddSchedule(options =>
{
options.LogEnabled = false;
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 每秒执行
});

查看作业执行结果:

info: 2022-12-02 17:27:13.1136450 +08:00 星期五 L MyJob[0] #12
<job1> [S] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 17:27:13.000 -> 2022-12-02 17:27:14.000
info: 2022-12-02 17:27:16.0092433 +08:00 星期五 L MyJob[0] #8
<job1> [S] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 17:27:16.000 -> 2022-12-02 17:27:17.000
info: 2022-12-02 17:27:19.0092363 +08:00 星期五 L MyJob[0] #6
<job1> [S] <job1 job1_trigger1> * * * * * * 3ts 2022-12-02 17:27:19.000 -> 2022-12-02 17:27:20.000
info: 2022-12-02 17:27:22.0183594 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 4ts 2022-12-02 17:27:22.000 -> 2022-12-02 17:27:23.000
info: 2022-12-02 17:27:25.0152323 +08:00 星期五 L MyJob[0] #4
<job1> [S] <job1 job1_trigger1> * * * * * * 5ts 2022-12-02 17:27:25.000 -> 2022-12-02 17:27:26.000

26.1.2.4 打印作业完整信息

框架提供了四种方式打印作业完整信息。

  • 第一种:输出完整的作业 JSON 信息:context.ConvertToJSON()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.ConvertToJSON());
await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-12-02 18:00:59.4140802 +08:00 星期五 L MyJob[0] #13
{
"jobDetail": {
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:00:59.390"
},
"trigger": {
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Furion.Schedule.PeriodSecondsTrigger",
"assemblyName": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-12-02 18:00:59.326",
"nextRunTime": "2022-12-02 18:01:04.358",
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"result": null,
"elapsedTime": 100,
"updatedTime": "2022-12-02 18:00:59.390"
}
}
  • 第二种:输出单独的作业 JSON 信息:jobDetail.ConvertToJSON()trigger.ConvertToJSON()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.JobDetail.ConvertToJSON());
_logger.LogInformation(context.Trigger.ConvertToJSON(NamingConventions.UnderScoreCase)); // 支持三种属性名输出规则

await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-12-02 18:02:10.7923360 +08:00 星期五 L MyJob[0] #8
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:02:10.774"
}
info: 2022-12-02 18:02:10.8008708 +08:00 星期五 L MyJob[0] #8
{
"trigger_id": "job1_trigger1",
"job_id": "job1",
"trigger_type": "Furion.Schedule.PeriodSecondsTrigger",
"assembly_name": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"start_time": null,
"end_time": null,
"last_run_time": "2022-12-02 18:02:10.727",
"next_run_time": "2022-12-02 18:02:15.733",
"number_of_runs": 1,
"max_number_of_runs": 0,
"number_of_errors": 0,
"max_number_of_errors": 0,
"num_retries": 0,
"retry_timeout": 1000,
"start_now": true,
"run_on_start": false,
"reset_only_once": true,
"result": null,
"elapsed_time": 100,
"updated_time": "2022-12-02 18:02:10.774"
}
  • 第三种:输出单独的作业 SQL 信息:jobDetail.ConvertToSQL()trigger.ConvertToSQL()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;
var trigger = context.Trigger;

_logger.LogInformation(jobDetail.ConvertToSQL("作业信息表名", PersistenceBehavior.Appended)); // 输出新增语句
_logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Removed, NamingConventions.Pascal)); // 输出删除语句
_logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Updated, NamingConventions.UnderScoreCase)); // 输出更新语句

await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-12-02 18:03:11.8543760 +08:00 星期五 L MyJob[0] #13
INSERT INTO 作业信息表名(
jobId,
groupName,
jobType,
assemblyName,
description,
concurrent,
includeAnnotations,
properties,
updatedTime
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp32',
NULL,
1,
0,
'{}',
'2022-12-02 18:03:11.836'
);
info: 2022-12-02 18:03:11.8636268 +08:00 星期五 L MyJob[0] #13
DELETE FROM 作业触发器表名
WHERE TriggerId = 'job1_trigger1' AND JobId = 'job1';
info: 2022-12-02 18:03:11.8669134 +08:00 星期五 L MyJob[0] #13
UPDATE 作业触发器表名
SET
trigger_id = 'job1_trigger1',
job_id = 'job1',
trigger_type = 'Furion.Schedule.PeriodSecondsTrigger',
assembly_name = 'Furion',
args = '[5]',
description = NULL,
status = 2,
start_time = NULL,
end_time = NULL,
last_run_time = '2022-12-02 18:03:11.778',
next_run_time = '2022-12-02 18:03:16.794',
number_of_runs = 1,
max_number_of_runs = 0,
number_of_errors = 0,
max_number_of_errors = 0,
num_retries = 0,
retry_timeout = 1000,
start_now = 1,
run_on_start = 0,
reset_only_once = 1,
result = NULL,
elapsed_time = 100,
updated_time = '2022-12-02 18:03:11.836'
WHERE trigger_id = 'job1_trigger1' AND job_id = 'job1';
  • 第四种:输出单独的作业 Monitor 信息:jobDetail.ConvertToMonitor()trigger.ConvertToMonitor()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.JobDetail.ConvertToMonitor());
_logger.LogInformation(context.Trigger.ConvertToMonitor());

await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-12-02 18:04:06.2833095 +08:00 星期五 L MyJob[0] #8
┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
┣ MyJob

┣ jobId: job1
┣ groupName:
┣ jobType: MyJob
┣ assemblyName: ConsoleApp32
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022-12-02 18:04:06.254
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
info: 2022-12-02 18:04:06.2868205 +08:00 星期五 L MyJob[0] #8
┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━
┣ Furion.Schedule.PeriodSecondsTrigger

┣ triggerId: job1_trigger1
┣ jobId: job1
┣ triggerType: Furion.Schedule.PeriodSecondsTrigger
┣ assemblyName: Furion
┣ args: [5]
┣ description:
┣ status: Running
┣ startTime:
┣ endTime:
┣ lastRunTime: 2022-12-02 18:04:06.189
┣ nextRunTime: 2022-12-02 18:04:11.212
┣ numberOfRuns: 1
┣ maxNumberOfRuns: 0
┣ numberOfErrors: 0
┣ maxNumberOfErrors: 0
┣ numRetries: 0
┣ retryTimeout: 1000
┣ startNow: True
┣ runOnStart: False
┣ resetOnlyOnce: True
┣ result:
┣ elapsedTime: 100
┣ updatedTime: 2022-12-02 18:04:06.254
┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━

26.1.2.5 运行时(动态)操作作业

有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业:

  1. 注册 services.AddSchedule() 服务
// 可以完全动态操作,只需要注册服务即可
services.AddSchedule();

// 也可以部分静态,部分动态注册
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));
});
  1. 注入 ISchedulerFactory 服务
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}

public void AddJob()
{
_schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());
}
}
  1. 查看作业执行结果
info: 2022-12-02 18:07:33.7799062 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 18:07:33.7971487 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 18:07:33.8751390 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:07:33.8805159 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 18:07:33.9013656 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 18:07:38.9241031 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-02 18:07:38.813 -> 2022-12-02 18:07:43.863
info: 2022-12-02 18:07:43.0865787 +08:00 星期五 L System.Logging.ScheduleService[0] #16
The <动态作业 Id_trigger1> trigger for scheduler of <动态作业 Id> successfully appended to the schedule.
warn: 2022-12-02 18:07:43.0894163 +08:00 星期五 L System.Logging.ScheduleService[0] #16
Schedule hosted service cancels hibernation and GC.Collect().
info: 2022-12-02 18:07:43.1129824 +08:00 星期五 L System.Logging.ScheduleService[0] #16
The scheduler of <动态作业 Id> successfully appended to the schedule.
info: 2022-12-02 18:07:43.8810686 +08:00 星期五 L MyJob[0] #17
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-02 18:07:43.863 -> 2022-12-02 18:07:48.848
info: 2022-12-02 18:07:44.0104025 +08:00 星期五 L MyJob[0] #16
<动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 1ts 2022-12-02 18:07:44.000 -> 2022-12-02 18:07:45.000
info: 2022-12-02 18:07:45.0092441 +08:00 星期五 L MyJob[0] #8
<动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 2ts 2022-12-02 18:07:45.000 -> 2022-12-02 18:07:46.000

26.1.2.6 作业触发器特性

默认情况下,框架不会扫描 IJob 实现类的作业触发器特性,但可以设置作业的 IncludeAnnotations 进行启用。

  1. 启用 IncludeAnnotations 扫描
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true)
, Triggers.PeriodSeconds(5)); // 这里可传可不传,传了则会自动载入特性和这里配置的作业触发器

// 还可以更简单~~
options.AddJob(typeof(MyJob).ScanToBuilder());

// 还可以批量新增 Furion 4.8.2.4+
options.AddJob(App.EffectiveTypes.ScanToBuilders());
});
  1. MyJob 中添加多个作业触发器特性
[Minutely]
[Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)]
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
await Task.CompletedTask;
}
}
  1. 查看作业执行结果
info: 2022-12-02 18:12:56.4199663 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 18:12:56.4287962 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 18:12:56.6149505 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6205117 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6266132 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger3> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6291006 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 18:12:56.6454334 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 18:13:00.0842828 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger2> * * * * * 1ts 2022-12-02 18:13:00.000 -> 2022-12-02 18:14:00.000
info: 2022-12-02 18:13:01.5260220 +08:00 星期五 L MyJob[0] #16
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-02 18:13:01.494 -> 2022-12-02 18:13:06.492
info: 2022-12-02 18:13:03.0076111 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 1ts 2022-12-02 18:13:03.000 -> 2022-12-02 18:13:07.000
info: 2022-12-02 18:13:06.4954400 +08:00 星期五 L MyJob[0] #13
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-02 18:13:06.492 -> 2022-12-02 18:13:11.463
info: 2022-12-02 18:13:07.0180453 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 2ts 2022-12-02 18:13:07.000 -> 2022-12-02 18:13:08.000
info: 2022-12-02 18:13:08.0114292 +08:00 星期五 L MyJob[0] #13
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 3ts 2022-12-02 18:13:08.000 -> 2022-12-02 18:14:03.000
info: 2022-12-02 18:13:11.4774564 +08:00 星期五 L MyJob[0] #16
<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-02 18:13:11.463 -> 2022-12-02 18:13:16.445

26.1.2.7 HTTP 请求作业

版本说明

以下内容仅限 Furion 4.8.7.7 + 版本使用。

HTTP 请求作业通常用于定时请求/访问互联网地址。

services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
System.Net.Http.IHttpClientFactory 错误

如遇 Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate 'Furion.Schedule.HttpJob'. 错误,请先注册 servces.AddHttpClient() 服务。

作业执行日志如下:

info: 2023-03-11 11:05:36.3616747 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-03-11 11:05:36.3652411 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-03-11 11:05:36.5172940 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-03-11 11:05:36.5189296 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-03-11 11:05:36.5347816 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
warn: 2023-03-11 11:05:41.5228138 +08:00 星期六 L System.Logging.ScheduleService[0] #15
Schedule hosted service will sleep <4970> milliseconds and be waked up at <2023-03-11 11:05:46.486>.
info: 2023-03-11 11:05:41.5542865 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[100] #9
Start processing HTTP request GET https://www.chinadot.net/
info: 2023-03-11 11:05:41.5589056 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[100] #9
Sending HTTP request GET https://www.chinadot.net/
info: 2023-03-11 11:05:44.1305461 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[101] #8
Received HTTP response headers after 2566.7836ms - 200
info: 2023-03-11 11:05:44.1343977 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[101] #8
End processing HTTP request after 2584.2327ms - 200
info: 2023-03-11 11:05:48.6475959 +08:00 星期六 L System.Logging.ScheduleService[0] #4
Received HTTP response body with a length of <63639> output as follows - 200
<!DOCTYPE html><html><head>
<title>dotNET China | 让 .NET 开发更简单,更通用,更流行</title>
......
</body></html>

26.1.2.8 委托方式作业

有时我们需要快速开启新的定时作业但不考虑后续持久化存储(如数据库存储),这时可以使用委托作业方式,如:

services.AddSchedule(options =>
{
// 和 IJob 的 ExecuteAsync 方法签名一致
options.AddJob((context, stoppingToken) =>
{
// 可通过 context.ServiceProvider 解析服务;框架提供了 .GetLogger() 拓展方法输出日志
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
});

作业执行日志如下:

info: 2023-03-21 14:22:34.1910781 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-03-21 14:22:34.1967420 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-03-21 14:22:34.6163320 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-03-21 14:22:34.6195112 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-03-21 14:22:34.6398162 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2023-03-21 14:22:39.7171392 +08:00 星期二 L System.Logging.DynamicJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 1ts 2023-03-21 14:22:39.575 -> 2023-03-21 14:22:44.623
info: 2023-03-21 14:22:44.6986483 +08:00 星期二 L System.Logging.DynamicJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 2ts 2023-03-21 14:22:44.623 -> 2023-03-21 14:22:49.657

26.1.2.9 非 IOC/DI 项目中使用

版本说明

以下内容仅限 Furion 4.8.8.5 + 版本使用。

在一些不支持依赖注入的项目类型如 Console、WinForm、WPF 中,可以通过以下方式使用:

  • 方式一:无需获取其他服务对象
_ = new ServiceCollection()
.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Period(5000));
})
.GetScheduleHostedService()
.StartAsync(new CancellationTokenSource().Token);
  • 方式二:需要后续解析服务
// 注册服务并构建
IServiceProvider services = new ServiceCollection()
.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Period(5000));
})
.BuildServiceProvider();

// 启动作业调度主机服务
services.GetScheduleHostedService()
.StartAsync(new CancellationTokenSource().Token);

// 解析作业计划工厂
var schedulerFactory = services.GetService<ISchedulerFactory>();
小知识

只需要将 services 对象用类的静态属性存储起来即可,如:

public class DI
{
public static IServiceProvider Services {get; set;}
}

之后通过 DI.Services = services; 即可,后续便可以通过 DI.Services.GetService<T>() 解析服务。

26.1.3 作业信息 JobDetail 及构建器

26.1.3.1 关于作业信息

框架提供了 JobDetail 类型来描述作业信息,JobDetail 类型提供以下只读属性

属性名属性类型默认值说明
JobIdstring作业 Id
GroupNamestring作业组名称
JobTypestring作业处理程序类型,存储的是类型的 FullName
AssemblyNamestring作业处理程序类型所在程序集,存储的是程序集 Name
Descriptionstring描述信息
Concurrentbooltrue作业执行方式,如果设置为 false,那么使用 串行 执行,否则 并行 执行
IncludeAnnotationsboolfalse是否扫描 IJob 实现类 [Trigger] 特性触发器
Propertiesstring"{}"作业信息额外数据,由 Dictionary<string, object> 序列化成字符串存储
UpdatedTimeDateTime?作业更新时间

26.1.3.2 关于作业信息构建器

作业信息 JobDetail 是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 JobDetail 对象呢?

JobBuilder 是作业调度模块提供可用来生成运行时 JobDetail 的类型,这样做的好处可避免外部直接修改运行时 JobDetail 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。

作业调度模块提供了多种方式用来创建 JobBuilder 对象。

  1. 通过 Create 静态方法创建
// 根据作业 Id 创建
var jobBuilder = JobBuilder.Create("job1");

// 根据 IJob 实现类类型创建
var jobBuilder = JobBuilder.Create<MyJob>();

// 根据程序集名称和类型完全限定名(FullName)创建
var jobBuilder = JobBuilder.Create("YourProject", "YourProject.MyJob");

// 根据 Type 类型创建
var jobBuilder = JobBuilder.Create(typeof(MyJob));

// 通过委托创建动态作业
var jobBuilder = JobBuilder.Create((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});
  1. 通过 JobDetail 类型创建

这种方式常用于在运行时更新作业信息。

var jobBuilder = JobBuilder.From(jobDetail);

//也可以通过以下方式
var jobBuilder = jobDetail.GetBuilder();
  1. 通过 JSON 字符串创建

该方式非常灵活,可从配置文件,JSON 字符串,或其他能够返回 JSON 字符串的地方创建。

var jobBuilder = JobBuilder.From(@"{
""jobId"": ""job1"",
""groupName"": null,
""jobType"": ""MyJob"",
""assemblyName"": ""ConsoleApp13"",
""description"": null,
""concurrent"": true,
""includeAnnotations"": false,
""properties"": ""{}"",
""updatedTime"": null
}");

如果使用的是 .NET7,可使用 """ 避免转义,如:

var jobBuilder = JobBuilder.From("""
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:00:59.390"
}
""");
关于属性名匹配规则

支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法) 命名方式。

不支持 UnderScoreCase(下划线命名法) ,如 "include_annotations": true

  1. 还可以通过 Clone 静态方法从一个 JobBuilder 创建
var jobBuilder = JobBuilder.Clone(fromJobBuilder);
克隆说明

克隆操作只会克隆 AssemblyNameJobTypeGroupNameDescriptionConcurrentIncludeAnnotationsPropertiesDynamicExecuteAsync(动态作业)。

  • 不会克隆 JobIdUpdatedTime
  1. 还可以通过 LoadFrom 实例方法填充当前的 JobBuilder

比如可以传递匿名类型,类类型,字典 Dictionary<string, object> 类型:

// 会覆盖所有相同的值
jobBuilder.LoadFrom(new
{
Description = "我是描述",
Concurrent = false
});

// 支持多个填充,还可以配置跳过 null 值覆盖
jobBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
Concurrent = false,
IncludeAnnotations = default(object) // 会跳过赋值
}, ignoreNullValue: true);

// 支持忽略特定属性名映射
jobBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
Concurrent = false,
IncludeAnnotations = default(object) // 会跳过赋值
}, ignorePropertyNames: new[]{ "description" });

// 支持字典类型
jobBuilder.LoadFrom(new Dictionary<string, object>
{
{"Description", "这是新的描述" },
{"include_annotations", false },
{"updatedTime", DateTime.Now }
});
关于属性名匹配规则

支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法)UnderScoreCase(下划线命名法) 命名方式。

26.1.3.3 设置作业信息构建器

JobBuilder 提供了和 JobDetail 完全匹配的 Set[属性名] 方法来配置作业信息各个属性,如:

services.AddSchedule(options =>
{
var jobBuilder = JobBuilder.Create<MyJob>()
.SetJobId("job1") // 作业 Id
.SetGroupName("group1") // 作业组名称
.SetJobType("Furion.Application", "Furion.Application.MyJob") // 作业类型,支持多个重载
.SetJobType<MyJob>() // 作业类型,支持多个重载
.SetJobType(typeof(MyJob)) // 作业类型,支持多个重载
.SetDescription("这是一段描述") // 作业描述
.SetConcurrent(false) // 并行还是串行方式,false 为 串行
.SetIncludeAnnotations(true) // 是否扫描 IJob 类型的触发器特性,true 为 扫描
.SetProperties("{}") // 作业额外数据 Dictionary<string, object> 类型序列化,支持多个重载
.SetProperties(new Dictionary<string, object> { { "name", "Furion" } }) // 作业类型额外数据,支持多个重载,推荐!!!
.SetDynamicExecuteAsync((context, stoppingToken) => {
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}) // 动态委托处理程序,一旦设置了此委托,那么优先级将大于 MyJob 的 ExecuteAsync
;

options.AddJob(jobBuilder, Triggers.PeriodSeconds(5));
});

26.1.3.4 作业信息/构建器额外数据

有时候我们需要在作业运行的时候添加一些额外数据,或者实现多个触发器共享数据,经常用于 串行 执行中(并行 也同样工作),后面一个触发器需等待前一个触发器完成。

public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;

var count = jobDetail.GetProperty<int>("count");
jobDetail.AddOrUpdateProperty("count", count + 1); // 递增 count

_logger.LogInformation($"count: {count} {context}");

await Task.CompletedTask;
}
}

查看作业运行日志:

info: 2022-12-03 23:16:46.5150228 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-03 23:16:46.5197497 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-03 23:16:46.6987703 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-03 23:16:46.7003295 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-03 23:16:46.7248216 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-03 23:16:51.7013640 +08:00 星期六 L MyJob[0] #8
count: 0 <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-03 23:16:51.663 -> 2022-12-03 23:16:56.656
info: 2022-12-03 23:16:56.6768044 +08:00 星期六 L MyJob[0] #9
count: 1 <job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-03 23:16:56.656 -> 2022-12-03 23:17:01.635
info: 2022-12-03 23:17:01.6454604 +08:00 星期六 L MyJob[0] #8
count: 2 <job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-03 23:17:01.635 -> 2022-12-03 23:17:06.608
info: 2022-12-03 23:17:06.6247917 +08:00 星期六 L MyJob[0] #6
count: 3 <job1> [C] <job1 job1_trigger1> 5s 4ts 2022-12-03 23:17:06.608 -> 2022-12-03 23:17:11.586

作业调度模块为 JobDetailJobBuilder 提供了多个方法操作额外数据:

// 查看所有额外数据
var properties = jobDetail.GetProperties();

// 查看单个额外数据,返回 object
var value = jobBuilder.GetProperty("key");

// 查看单个额外数据泛型
var value = jobDetail.GetProperty<int>("key");

// 添加新的额外数据,支持链式操作,如果键已存在,则跳过
jobDetail.AddProperty("key", "Furion").AddProperty("key1", 2);

// 添加或更新额外数据,支持链式操作,不存在则新增,存在则替换,推荐
jobDetail.AddOrUpdateProperty("key", "Furion").AddOrUpdateProperty("key1", 2);

// 还可以通过委托的方式:如果键不存在则插入 count = newValue,否则更新为 value(旧值)+1
jobDetail.AddOrUpdateProperty("count", newValue, value => value + 1);

// 删除某个额外数据,支持链式操作,如果 key 不存在则跳过
jobDetail.RemoveProperty("key").RemoveProperty("key1");

// 清空所有额外数据
jobDetail.ClearProperties();
作业额外数据类型支持

作业额外数据每一项的值只支持 int32int64stringboolnull 或它们组成的数组类型。

26.1.3.5 作业信息特性

作业信息特性 [JobDetail] 是为了方便运行时或启动时快速创建作业计划构建器而提供的,可在启动时或运行时通过以下方式创建,如:

[JobDetail("job1", "这是一段描述")]
[PeriodSeconds(5, TriggerId = "trigger1")]
public class MyJob : IJob
{
}
  • 启动 IncludeAnnotations 属性自动填充
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>()
.SetIncludeAnnotations(true)); // 此时 [JobDetail] 配置的非空属性将自动复制给 JobBuilder,[PeriodSeconds] 也会自动创建 TriggerBuilder
});
  • 手动扫描并创建作业计划构建器
var schedulerBuilder = typeof(MyJob).ScanToBuilder();
  • 通过程序集类型扫描批量创建作业计划构建器

也可以用于作业持久化 Preload 初始化时使用:

public IEnumerable<SchedulerBuilder> Preload()
{
// 扫描所有类型并创建
return App.EffectiveTypes.Where(t => t.IsJobType())
.Select(t => t.ScanToBuilder());

// 还可以更简单~~
return App.EffectiveTypes.ScanToBuilders();
}

作业信息特性还提供了多个属性配置,如:

  • JobId:作业信息 Id,string 类型
  • GroupName:作业组名称,string 类型
  • Description:描述信息,string 类型
  • Concurrent:是否采用并行执行,bool 类型,如果设置为 false,那么使用 串行 执行

使用如下:

[JobDetail("jobId")]    // 仅作业 Id
[JobDetail("jobId", "这是一段描述")] // 描述
[JobDetail("jobId", false)] // 串行
[JobDetail("jobId", false, "这是一段描述")] // 串行 + 描述
[JobDetail("jobId", Concurrent = false, Description = "这是一段描述")]
[JobDetail("jobId", Concurrent = false, Description = "这是一段描述", GroupName = "分组名")]
public class MyJob : IJob
{
// ....
}

26.1.3.6 多种格式字符串输出

JobDetailJobBuilder 都提供了多种将自身转换成特定格式的字符串。

  1. 转换成 JSON 字符串
var json = jobDetail.ConvertToJSON();

字符串打印如下:

{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-04 11:51:00.483"
}
  1. 转换成 SQL 字符串
// 输出新增 SQL,使用 CamelCase 属性命名
var insertSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Appended
, NamingConventions.CamelCase);
// 更便捷拓展
var insertSql = jobDetail.ConvertToInsertSQL("tbName", NamingConventions.CamelCase);

// 输出删除 SQL,使用 Pascal 属性命名
var deleteSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Removed
, NamingConventions.Pascal);
// 更便捷拓展
var deleteSql = jobDetail.ConvertToDeleteSQL("tbName", NamingConventions.Pascal);

// 输出更新 SQL,使用 UnderScoreCase 属性命名
var updateSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Updated
, NamingConventions.UnderScoreCase);
// 更便捷拓展
var updateSql = jobDetail.ConvertToUpdateSQL("tbName", NamingConventions.UnderScoreCase);

字符串打印如下:

-- 新增语句
INSERT INTO tbName(
jobId,
groupName,
jobType,
assemblyName,
description,
concurrent,
includeAnnotations,
properties,
updatedTime
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp13',
NULL,
1,
0,
'{}',
'2022-12-04 11:53:05.489'
);
-- 删除语句
DELETE FROM tbName
WHERE JobId = 'job1';
-- 更新语句
UPDATE tbName
SET
job_id = 'job1',
group_name = NULL,
job_type = 'MyJob',
assembly_name = 'ConsoleApp13',
description = NULL,
concurrent = 1,
include_annotations = 0,
properties = '{}',
updated_time = '2022-12-04 11:53:05.489'
WHERE job_id = 'job1';
  1. 转换成 Monitor 字符串
var monitor = jobDetail.ConvertToMonitor();

字符串打印如下:

┏━━━━━━━━━━━  JobDetail ━━━━━━━━━━━
┣ MyJob

┣ jobId: job1
┣ groupName:
┣ jobType: MyJob
┣ assemblyName: ConsoleApp13
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022-12-04 11:55:11.186
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
  1. 简要字符串输出
var str = jobDetail.ToString();

字符串打印如下:

<job1> 这是一段描述 [C]

26.1.3.7 自定义 SQL 输出配置

版本说明

以下内容仅限 Furion 4.8.2 + 版本使用。

services.AddSchedule(options =>
{
options.JobDetail.ConvertToSQL = (tableName, columnNames, jobDetail, behavior, naming) =>
{
// 生成新增 SQL
if (behavior == PersistenceBehavior.Appended)
{
return jobDetail.ConvertToInsertSQL(tableName, naming);
}
// 生成更新 SQL
else if (behavior == PersistenceBehavior.Updated)
{
return jobDetail.ConvertToUpdateSQL(tableName, naming);
}
// 生成删除 SQL
else if (behavior == PersistenceBehavior.Removed)
{
return jobDetail.ConvertToDeleteSQL(tableName, naming);
}

return string.Empty;
};
});
  • ConvertToSQL 委托参数说明
    • tableName:数据库表名称,string 类型
    • columnNames:数据库列名:string[] 类型,只能通过 索引 获取
    • jobDetail:作业信息 JobDetail 对象
    • behavior:持久化 PersistenceBehavior 类型,用于标记 新增更新 还是 删除 操作
    • naming:命名法 NamingConventions 类型,包含 CamelCase(驼峰命名法)Pascal(帕斯卡命名法)UnderScoreCase(下划线命名法)
注意事项

如果在该自定义 SQL 输出方法中调用 jobDetail.ConvertToSQL(..) 会导致死循环。

26.1.3.8 启用作业执行日志输出

版本说明

以下内容仅限 Furion 4.8.3.7 + 版本使用。

通常我们需要在 IJob 实现类中输出作业触发日志,如 _logger.LogInformation($"{context}");

public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}

但这样的 范式代码 几乎每一个 IJob 实现类都可能输出,所以在 Furion 4.8.3.7+ 版本提供了更便捷的配置,无需每一个 IJob 编写 _logger.LogInformation($"{context}");

配置启用如下:

services.AddSchedule(options =>
{
options.JobDetail.LogEnabled = true; // 默认 false
});

之后 MyJob 可以更加精简了,日志类别自动设置为 MyJob 类型完整限定名。

public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// 这里写业务逻辑即可,无需调用 _logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}

作业执行日志如下:

info: 2022-12-14 11:56:12.3963326 +08:00 星期三 L Furion.Application.MyJob[0] #4
<job1> [C] <job1 job1_trigger2> 5s 1ts 2022-12-14 11:56:08.361 -> 2022-12-14 11:56:13.366
info: 2022-12-14 11:56:13.4100745 +08:00 星期三 L Furion.Application.MyJob[0] #6
<job1> [C] <job1 job1_trigger2> 5s 2ts 2022-12-14 11:56:13.366 -> 2022-12-14 11:56:18.376
info: 2022-12-14 11:56:18.3931380 +08:00 星期三 L Furion.Application.MyJob[0] #9
<job1> [C] <job1 job1_trigger2> 5s 3ts 2022-12-14 11:56:18.376 -> 2022-12-14 11:56:23.360

26.1.4 作业处理程序 IJob

作业处理程序是作业符合触发时间执行的业务逻辑代码,通常由程序员编写,作业处理程序需实现 IJob 接口。

26.1.4.1 如何定义

public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// your code...
}
}

26.1.4.2 JobExecutingContext 上下文

JobExecutingContext 上下文作为 ExecuteAsync 方法的第一个参数,包含以下运行时信息:

  • JobExecutingContext 属性列表
    • JobId:作业 Id
    • TriggerId:当前触发器 Id
    • JobDetail:作业信息
    • Trigger:作业触发器
    • OccurrenceTime:作业计划触发时间,最准确的记录时间
    • ExecutingTime:实际执行时间(可能存在误差)
    • RunId:本次作业执行唯一 IdFurion 4.8.5.1+ 提供
    • Result:设置/读取本次作业执行结果,Furion 4.8.7.7+ 提供
    • ServiceProvider:服务提供器,Furion 4.8.7.10+ 提供
  • JobExecutingContext 方法列表
    • .ConvertToJSON(naming):将上下文转换成 JSON 字符串
    • .ToString():输出为字符串

26.1.4.3 作业处理程序实例

版本说明

以下内容仅限 Furion 4.8.8.13 + 版本使用。

默认情况下,作业处理程序会在作业触发器符合触发条件下通过 ActivatorUtilities.CreateInstance 动态创建,也就是每次触发都会创建新的 IJob 实例,如:

var jobHandler = ActivatorUtilities.CreateInstance(_serviceProvider, jobType);

其中 _serviceProvider单例服务提供器,所以 IJob 实现类只能通过构造函数注入 单例服务。如果没有范围作用域服务的需求,那么可以将 IJob 注册为单例服务,这样就可以避免每次重复创建 IJob 实例,对性能和减少内存占用有不小优化。 如:

services.AddSingleton<YourJob>();

如果希望能够在构造函数注入范围作用域或瞬时作用域,可实现 IJobFactory 接口,如:

using Furion.Schedule;
using Microsoft.Extensions.DependencyInjection;

namespace Furion.Application;

public class JobFactory : IJobFactory
{
public IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context)
{
return ActivatorUtilities.CreateInstance(serviceProvider, context.JobType) as IJob;

// 如果通过 services.AddSingleton<YourJob>(); 或 serivces.AddScoped<YourJob>(); 或 services.AddTransient<YourJob> 可通过下列方式
// return serviceProvider.GetRequiredService(context.JobType) as IJob;
}
}

之后注册 JobFactory 即可,如:

services.AddSchedule(options =>
{
// 添加作业处理程序工厂
options.AddJobFactory<JobFactory>();
});

这样作业就可以注入范围和瞬时服务了。

26.1.4.4 依赖注入

实现 IJob 的作业处理程序类型默认注册为 单例那么只要是单例的服务,皆可以通过构造函数注入,如:ILogger<>IConfiguration

public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;

public MyJob(ILogger<MyJob> logger
, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
  • 如果是非 单例 的接口,如 瞬时范围 服务,可通过 IServiceScopeFactory 创建
推荐 IJobFactory 方式

Furion 4.8.8.13+ 版本可以通过上一小节 IJobFactory 统一实现。推荐

public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;

public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_schedulerFactory = scopeFactory;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
using var serviceScope = _scopeFactory.CreateScope();
var repository = serviceScope.ServiceProvider.GetService<IRepository<User>>();

_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
  • 针对高频定时任务,比如每秒执行一次,或者更频繁的任务
推荐 IJobFactory 方式

Furion 4.8.8.13+ 版本可以通过上一小节 IJobFactory 统一实现。推荐

为了避免频繁创建作用域和销毁作用域,可创建长范围的作用域。

public class MyJob : IJob, IDisposable
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScope _serviceScope;

public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_serviceScope = scopeFactory.CreateScope();
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var repository = _serviceScope.ServiceProvider.GetService<IRepository<User>>();
var user = await repository.GetAsync(1);

_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}

public void Dispose()
{
_serviceScope?.Dispose();
}
}

26.1.4.5 动态作业 DynamicJob

框架提供了便捷的动态作业 DynamicJob 类型,可通过 Func<JobExecutingContext, CancellationToken, Task> 委托传入,无需创建 IJob 实现类型。

框架还为 JobExecutionContext 属性 ServiceProvder 提供了 .GetLogger() 拓展方法,方便快速获取 ILogger<System.Logging.DynamicJob> 日志对象实例。

// 通过 JobBuilder 创建
var jobBuilder = JobBuilder.Create((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});

// 通过 jobBuilder 方法 SetDynamicExecuteAsync 创建
jobBuilder.SetDynamicExecuteAsync((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});

// 通过 AddJob 创建
service.AddSchedule(options =>
{
options.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
});

// 通过 ISchedulerFactory 创建
_schedulerFactory.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));

动态作业执行结果:

info: 2022-12-04 12:26:18.6562296 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:26:18.6618404 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:26:18.8727764 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:26:18.8745765 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:26:18.9013540 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:26:23.8753926 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:26:23.837 -> 2022-12-04 12:26:28.835
info: 2022-12-04 12:26:28.8686474 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:26:28.835 -> 2022-12-04 12:26:33.823
info: 2022-12-04 12:26:33.8531796 +08:00 星期日 L System.Logging.DynamicJob[0] #13
<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:26:33.823 -> 2022-12-04 12:26:38.820
动态作业和普通作业的区别
  • 动态作业处理程序类型是:DynamicJob 类型
  • 动态作业提供的 .GetLogger() 拓展输出日志类别是:System.Logging.DynamicJob
  • 如果普通作业同时设置了 SetJobTypeSetDynamicExecuteAsync,那么优先作为动态作业执行。
  • 动态作业无法将 Func<..> 进行序列化持久化存储

26.1.4.6 使用 Roslyn 动态创建

版本说明

以下内容仅限 Furion 4.8.8.7 + 版本使用。

按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 Roslyn 动态编译 C# 代码。

  1. 根据字符串创建 IJob 类型
// 调用 Schedular 静态类提供的 CompileCSharpClassCode 方法
var jobAssembly = Schedular.CompileCSharpClassCode(@"
using Furion.Schedule;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace YourProject;

public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;

public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($""我是 Roslyn 方式创建的:{context}"");
await Task.CompletedTask;
}
}
");

// 生成运行时 MyJob 类型
var jobType = jobAssembly.GetType("YourProject.MyJob");
  1. 注册作业
// 可以在启动的时候添加
services.AddSchedule(options =>
{
options.AddJob(jobType
, Triggers.PeriodSeconds(5));
});

// 也可以完全在运行时添加(常用)
_schedulerFactory.AddJob(jobType
, Triggers.PeriodSeconds(5));

查看作业执行日志:

info: 2022-12-04 12:38:00.6249410 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:38:00.6294089 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:38:00.7496005 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:38:00.7514579 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:38:00.7836777 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:38:05.7389682 +08:00 星期日 L YourProject.MyJob[0] #6
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:38:05.713 -> 2022-12-04 12:38:10.692
info: 2022-12-04 12:38:10.7108416 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:38:10.692 -> 2022-12-04 12:38:15.673
info: 2022-12-04 12:38:15.6925578 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:38:15.673 -> 2022-12-04 12:38:20.656

惊不惊喜,意外意外~

小知识

通过 Roslyn 的方式支持创建 IJobJobDetailTriggerScheduler 哦,自行测试。😊

26.1.4.7 作业执行异常处理

正常情况下,程序员应该保证作业执行程序总是稳定运行,但有时候会出现一些不可避免的意外导致出现异常,如网络异常等。

下面给出模拟出现异常和常见的处理方式例子:

services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(3));
});
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}

public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");

// 模拟异常
var num = 10;
var n = 0;
var c = num / n;

return Task.CompletedTask;
}
}

输出日志如下:

info: 2023-04-22 22:18:04.2149071 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-04-22 22:18:04.2189082 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-04-22 22:18:04.3216571 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-22 22:18:04.3230110 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-04-22 22:18:04.3521056 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2023-04-22 22:18:07.3782666 +08:00 星期六 L MyJob[0] #17
<job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308
fail: 2023-04-22 22:18:07.6652239 +08:00 星期六 L System.Logging.ScheduleService[0] #17
Error occurred executing <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.DivideByZeroException: Attempted to divide by zero.
at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 29
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 79
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
info: 2023-04-22 22:18:10.3507729 +08:00 星期六 L MyJob[0