26.1 调度作业
📝 模块更新日志
-
新特性
- 定时任务
Http
作业请求头Headers
和作业分组Group
和描述Description
支持 4.8.8.46 ⏱️2023.10.09 #I85Z7S - 定时任务看板列表支持作业分组名排序 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
所有带At
的Cron
表达式触发器构建器及特性 4.8.2.5 ⏱️2022.11.29 #I63PLR
- 定时任务
-
- 定时任务批量添加
SchedulerBuilder
作业功能 4.8.2.4 ⏱️2022.11.29 5faa67b
- 定时任务批量添加
-
-
JobDetail
和Trigger
自定义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
-
突破性变化
查看变化
减少记忆负担,统一动态作业和普通作业的 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
-
问题修复
- 定时任务设置触发器
Result
后作业执行异常不能重置问题 4.9.1.7 ⏱️2023.11.24 147215f - 定时任务高频作业下持久化操作出现阻塞卡问题 4.8.8.51 ⏱️2023.11.06 f1d0b4a
- 定时任务看板中间件
SSE
请求不是长连接导致连接频繁初始化销毁 4.8.8.49 ⏱️2023.10.26 1997f1b - 定时任务因上一版本修改 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 - 定时任务作业触发器配置了
EndTime
和StartTime
之后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
- 定时任务
-
文档
以下内容仅限 Furion 4.8.0 +
版本使用。
26.1.1 关于调度作业
调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。
26.1.1.1 使用场景
定时任务的应用场景非常广,几乎是每一个软件系统必备功能:
- 叫你起床的闹钟
- 日历日程提醒
- 生日纪念 日提醒
- 定时备份数据库
- 定时清理垃圾数据
- 定时发送营销信息,邮件
- 定时上线产品,比如预售产品,双十一活动
- 定时发送优惠券
- 定时发布,实现 Devops 功能,如 Jenkins
- 定时爬虫抓数据
- 定时导出报表,历史统计,考勤统计
- ...
26.1.2 快速入门
- 定义作业处理程序
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;
}
}
- 在
Startup.cs
注册Schedule
服务:
services.AddSchedule(options =>
{
// 注册作业,并配置作业触发器
options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行
});
- 查看作业执行结果
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 运行时(动态)操作作业
有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业:
- 注册
services.AddSchedule()
服务
// 可以完全动态操作,只需要注册服务即可
services.AddSchedule();
// 也可以部分静态,部分动态注册
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));
});
- 注入
ISchedulerFactory
服务
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
public void AddJob()
{
_schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());
}
}
- 查看作业执行结果
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
进行启用。
- 启用
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());
});
- 在
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;
}
}
- 查看作业执行结果
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 = "{}"; // 设置请求报文体
// request.Headers.Add("framework", "Furion"); // Furion 4.8.8.46+ 支持
// request.GroupName = "group"; // Furion 4.8.8.46+ 支持
// request.Description = "作业请求描述"; // Furion 4.8.8.46+ 支持
}, 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
类型提供以下只读属性:
属性名 | 属性类型 | 默认值 | 说明 |
---|---|---|---|
JobId | string | 作业 Id | |
GroupName | string | 作业组名称 | |
JobType | string | 作业处理程序类型,存储的是类型的 FullName | |
AssemblyName | string | 作业处理程序类型所在程序集,存储的是程序集 Name | |
Description | string | 描述信息 | |
Concurrent | bool | true | 作业执行方式,如果设置为 false ,那么使用 串行 执行,否则 并行 执行 |
IncludeAnnotations | bool | false | 是否扫描 IJob 实现类 [Trigger] 特性触发器 |
Properties | string | "{}" | 作业信息额外数据,由 Dictionary<string, object> 序列化成字符串存储 |
UpdatedTime | DateTime? | 作业更新时间 |
26.1.3.2 关于作业信息构建器
作业信息 JobDetail
是作业调度模块提供 运行时的只读类型,那么我们该如何创建或变更 JobDetail
对象呢?
JobBuilder
是作业调度模块提供可用来生成运行时 JobDetail
的类型,这样做的好处可避免外部直接修改运行时 JobDetail
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 JobBuilder
对象。
- 通过
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;
});
- 通过
JobDetail
类型创建
这种方式常用于在运行时更新作业信息。
var jobBuilder = JobBuilder.From(jobDetail);
//也可以通过以下方式
var jobBuilder = jobDetail.GetBuilder();
- 通过
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
- 还可以通过
Clone
静态方法从一个JobBuilder
创建
var jobBuilder = JobBuilder.Clone(fromJobBuilder);
克隆操作只会克隆 AssemblyName
,JobType
,GroupName
,Description
,Concurrent
,IncludeAnnotations
,Properties
,DynamicExecuteAsync
(动态作业)。
- 不会克隆
JobId
,UpdatedTime
。
- 还可以通过
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
作业调度模块为 JobDetail
和 JobBuilder
提供了多个方法操作额外数据:
// 查看所有额外数据
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();
作业额外数据每一项的值只支持 int32
,int64
,string
,bool
,null
或它们组成的数组类型。
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 多种格式字符串输出
JobDetail
和 JobBuilder
都提供了多种将自身转换成特定格式的字符串。
- 转换成
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"
}
- 转换成
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';
- 转换成
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 ━━━━━━━━━━━
- 简要字符串输出
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
:本次作业执行唯一Id
,Furion 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
- 如果普通作业同时设置了
SetJobType
和SetDynamicExecuteAsync
,那么优先作为动态作业执行。 - 动态作业无法将
Func<..>
进行序列化持久化存储
26.1.4.6 使用 Roslyn
动态创建
以下内容仅限 Furion 4.8.8.7 +
版本使用。
按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 Roslyn
动态编译 C#
代码。
- 根据字符串创建
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");
- 注册作业
// 可以在启动的时候添加
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
的方式支持创建 IJob
,JobDetail
,Trigger
,Scheduler
哦,自行测试。😊
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] #8
<job1> [C] <job1 job1_trigger1> 3s 2ts 2023-04-22 22:18:10.308 -> 2023-04-22 22:18:13.318
fail: 2023-04-22 22:18:10.4292529 +08:00 星期六 L System.Logging.ScheduleService[0] #8
Error occurred executing <job1> [C] <job1 job1_trigger1> 3s 2ts 2023-04-22 22:18:10.308 -> 2023-04-22 22:18:13.318.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
从上面执行日志可以看出,作业执行出现异常只会影响当前触发时间的执行,但不会影响下一次执行。出现这种情况通常配置 重试策略 确保每次作业处理程序可能执行成功,如重试 3
次,如:
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(3)
.SetNumRetries(3)); // 重试三次
});
输出日志如下:
info: 2023-04-22 22:25:00.7244392 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-04-22 22:25:00.7293195 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-04-22 22:25:00.8796238 +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:25:00.8852651 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-04-22 22:25:00.9348100 +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:25:03.9357047 +08:00 星期六 L MyJob[0] #12
<job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
warn: 2023-04-22 22:25:04.0147234 +08:00 星期六 L System.Logging.ScheduleService[0] #12
Retrying 1/3 times for <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
info: 2023-04-22 22:25:05.0243650 +08:00 星期六 L MyJob[0] #12
<job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
warn: 2023-04-22 22:25:05.0963359 +08:00 星期六 L System.Logging.ScheduleService[0] #12
Retrying 2/3 times for <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
info: 2023-04-22 22:25:06.1100662 +08:00 星期六 L MyJob[0] #12
<job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
warn: 2023-04-22 22:25:06.1785087 +08:00 星期六 L System.Logging.ScheduleService[0] #12
Retrying 3/3 times for <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888
fail: 2023-04-22 22:25:07.3754596 +08:00 星期六 L System.Logging.ScheduleService[0] #16
Error occurred executing <job1> [C] <job1 job1_trigger1> 3s 2ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:09.884.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.DivideByZeroException: Attempted to divide by zero.
at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 30
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 91
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 102
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
推荐使用 【26.1.9 作业执行器 IJobExecutor
】 配置全局异常重试策略。
26.1.4.8 作业执行异常回退策略
以下内容仅限 Furion 4.8.8.6 +
版本使用。
作业处理程序执行异常除了配置 重试次数
或配置 全局异常重试策略
以外,还可以实现 IJob.FallbackAsync
进行回退配置。
public class TestJob : IJob
{
private readonly ILogger<TestJob> _logger;
public TestJob(ILogger<TestJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogWarning($"{context}");
// 模拟运行第三次出异常
if (context.Trigger.NumberOfRuns == 3)
{
throw new Exception("假装出错");
}
await Task.CompletedTask;
}
public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken)
{
Console.WriteLine("调用了回退");
return Task.CompletedTask;
}
}
输出日志如下:
info: 2023-04-25 17:19:06.5448438 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-04-25 17:19:06.5523770 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-04-25 17:19:07.1156318 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-25 17:19:07.1293994 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-25 17:19:07.1360332 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-04-25 17:19:07.1614880 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
warn: 2023-04-25 17:19:11.1565118 +08:00 星期二 L Furion.Application.TestJob[0] #9
<job1> [C] <job1 job1_trigger2> 4s 1ts 2023-04-25 17:19:11.067 -> 2023-04-25 17:19:15.092
warn: 2023-04-25 17:19:15.1275434 +08:00 星期二 L Furion.Application.TestJob[0] #18
<job1> [C] <job1 job1_trigger2> 4s 2ts 2023-04-25 17:19:15.092 -> 2023-04-25 17:19:19.094
warn: 2023-04-25 17:19:19.1006636 +08:00 星期二 L Furion.Application.TestJob[0] #17
<job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067
fail: 2023-04-25 17:19:19.2554424 +08:00 星期二 L System.Logging.ScheduleService[0] #17
Error occurred executing in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.Exception: 假装出错
at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\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 D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
info: 2023-04-25 17:19:19.2589045 +08:00 星期二 L System.Logging.ScheduleService[0] #17
Fallback called in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067.
调用了回退
warn: 2023-04-25 17:19:23.0840895 +08:00 星期二 L Furion.Application.TestJob[0] #14
<job1> [C] <job1 job1_trigger2> 4s 4ts 2023-04-25 17:19:23.067 -> 2023-04-25 17:19:27.050
如果 FallbackAsync
发生二次异常,如:
public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken)
{
Console.WriteLine("调用了回退");
throw new Exception("回退了我还是出异常~");
return Task.CompletedTask;
}
输出日志将合并前面所有异常并输出:
info: 2023-04-25 17:24:46.0348224 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-04-25 17:24:46.0392736 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-04-25 17:24:46.4677115 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-25 17:24:46.4847108 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-25 17:24:46.4936590 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-04-25 17:24:46.6097957 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
warn: 2023-04-25 17:24:50.4988840 +08:00 星期二 L Furion.Application.TestJob[0] #17
<job1> [C] <job1 job1_trigger2> 4s 1ts 2023-04-25 17:24:50.419 -> 2023-04-25 17:24:54.436
warn: 2023-04-25 17:24:54.4704187 +08:00 星期二 L Furion.Application.TestJob[0] #15
<job1> [C] <job1 job1_trigger2> 4s 2ts 2023-04-25 17:24:54.436 -> 2023-04-25 17:24:58.436
warn: 2023-04-25 17:24:58.4441477 +08:00 星期二 L Furion.Application.TestJob[0] #15
<job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411
fail: 2023-04-25 17:24:58.5704807 +08:00 星期二 L System.Logging.ScheduleService[0] #15
Error occurred executing in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.Exception: 假装出错
at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\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 D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
info: 2023-04-25 17:24:58.5737508 +08:00 星期二 L System.Logging.ScheduleService[0] #15
Fallback called in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.
调用了回退
fail: 2023-04-25 17:24:58.5929688 +08:00 星期二 L System.Logging.ScheduleService[0] #15
Fallback called error in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.AggregateException: One or more errors occurred. (Error occurred executing in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.) (回退了我还是出异常~)
---> System.InvalidOperationException: Error occurred executing in <job1> [C] <job1 job1_trigger2> 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.
---> System.Exception: 假装出错
at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\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 D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---
---> (Inner Exception #1) System.Exception: 回退了我还是出异常~
at Furion.Application.TestJob.FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 32
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 309<---
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
warn: 2023-04-25 17:25:02.4212180 +08:00 星期二 L Furion.Application.TestJob[0] #15
<job1> [C] <job1 job1_trigger2> 4s 4ts 2023-04-25 17:25:02.411 -> 2023-04-25 17:25:06.388
26.1.4.9 作业调度器被取消处理
一般情况下,作业调度器意外关闭或手动关闭,但作业处理程序异步操作还未处理完成,这个时候我们可以选择取消还是继续执行,如果选择取消:
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}");
try
{
await SomeMethodAsync(stoppingToken);
}
catch (TaskCanceledException)
{
_logger.LogWarning("作业被取消了~");
}
catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
{
_logger.LogWarning("作业被取消了~");
}
catch {}
}
private async Task SomeMethodAsync(CancellationToken stoppingToken)
{
// 模拟耗时
await Task.Delay(1000 * 60 * 60, stoppingToken);
}
}
这样当作业调度器被关闭时,SomeMethodAsync
如果未处理完成也会取消操作。
info: 2022-12-04 12:49:00.2636929 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:49:00.2686096 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:49:00.4252737 +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:49:00.4266075 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:49:00.4468654 +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:49:05.4397629 +08:00 星期日 L MyJob[0] #4
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:49:05.390 -> 2022-12-04 12:49:10.393
info: 2022-12-04 12:49:08.6301592 +08:00 星期日 L Microsoft.Hosting.Lifetime[0] #14
Application is shutting down...
warn: 2022-12-04 12:49:08.7247004 +08:00 星期日 L MyJob[0] #6
作业被取消了~
warn: 2022-12-04 12:49:10.4257861 +08:00 星期日 L System.Logging.ScheduleService[0] #6
Schedule hosted service cancels hibernation and GC.Collect().
crit: 2022-12-04 12:49:10.4360088 +08:00 星期日 L System.Logging.ScheduleService[0] #6
Schedule hosted service is stopped.
26.1.4.10 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 = "{}"; // 设置请求报文体
// request.Headers.Add("framework", "Furion"); // Furion 4.8.8.46+ 支持
// request.GroupName = "group"; // Furion 4.8.8.46+ 支持
// request.Description = "作业请求描述"; // Furion 4.8.8.46+ 支持
}, Triggers.PeriodSeconds(5));
});
作业执行日志如下:
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>
❤️ 如何自定义 HTTP
作业
默认情况下,Furion
框架提供有限的 HTTP
配置参数,如果不能满足可自行定义。
- 自定义
Http
参数类:MyHttpJobMessage
namespace YourProject.Core;
/// <summary>
/// HTTP 作业消息
/// </summary>
public class MyHttpJobMessage
{
/// <summary>
/// 请求地址
/// </summary>
public string RequestUri { get; set; }
/// <summary>
/// 请求方法
/// </summary>
public HttpMethod HttpMethod { get; set; } = HttpMethod.Get;
/// <summary>
/// 请求报文体
/// </summary>
public string Body { get; set; }
}
- 自定义
Http
作业处理程序:MyHttpJob
/// <summary>
/// HTTP 请求作业处理程序
/// </summary>
public class MyHttpJob : IJob // 也可以继承内部的 HttpJob 类
{
/// <summary>
/// <see cref="HttpClient"/> 创建工厂
/// </summary>
private readonly IHttpClientFactory _httpClientFactory;
/// <summary>
/// 日志服务
/// </summary>
private readonly ILogger<MyHttpJob> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="httpClientFactory"><see cref="HttpClient"/> 创建工厂</param>
/// <param name="logger">日志服务</param>
public MyHttpJob(IHttpClientFactory httpClientFactory
, ILogger<MyHttpJob> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// 具体处理逻辑
/// </summary>
/// <param name="context">作业执行前上下文</param>
/// <param name="stoppingToken">取消任务 Token</param>
/// <returns><see cref="Task"/></returns>
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;
// 解析 HTTP 请求参数,键名称为类名
var httpJobMessage = Penetrates.Deserialize<MyHttpJobMessage>(jobDetail.GetProperty<string>(nameof(MyHttpJob)));
// 空检查
if (httpJobMessage == null || string.IsNullOrWhiteSpace(httpJobMessage.RequestUri))
{
return;
}
// 创建请求客户端
using var httpClient = _httpClientFactory.CreateClient(); // CreateClient 可以传入一个字符串进行全局配置 Client
// 添加请求报文头 User-Agent
httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47");
// 创建请求对象
var httpRequestMessage = new HttpRequestMessage(httpJobMessage.HttpMethod, httpJobMessage.RequestUri);
// 添加请求报文体,默认只支持发送 application/json 类型
if (httpJobMessage.HttpMethod != HttpMethod.Get
&& httpJobMessage.HttpMethod != HttpMethod.Head
&& !string.IsNullOrWhiteSpace(httpJobMessage.Body))
{
var stringContent = new StringContent(httpJobMessage.Body, Encoding.UTF8);
stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
httpRequestMessage.Content = stringContent;
}
// 更多自定义参数========================
// Your Code ....
// 发送请求并确保成功
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, stoppingToken);
// 解析返回值
var bodyString = await httpResponseMessage.Content.ReadAsStringAsync(stoppingToken);
// 输出日志
_logger.LogInformation($"Received HTTP response body with a length of <{bodyString.Length}> output as follows - {(int)httpResponseMessage.StatusCode}{Environment.NewLine}{bodyString}");
// 设置本次执行结果
context.Result = Penetrates.Serialize(new
{
httpResponseMessage.StatusCode,
Body = bodyString
});
}
}
- 注册自定义
Http
作业
services.AddSchedule(options =>
{
// 创建 HTTP 作业消息
var httpJobMessage = new YourHttpJobMessage();
var jobBuilder = JobBuilder.Create<MyHttpJob>()
// 添加作业附加信息
.AddProperty(nameof(MyHttpJob), Schedular.Serialize(httpJobMessage));
// 添加作业
options.AddJob(jobBuilder, Triggers.PeriodSeconds(5));
});
26.1.4.11 设置本次执行结果
以下内容仅限 Furion 4.8.7.7 +
版本使用。
有时候我们希望能够记录本次作业触发器触发返回结果,可通过 context.Result
进行设置。
也可以通过该值来判断作业是否成功执行,如设置了 Result
值但实际发现 trigger.Result
为 null
,那么也就是本次执行未成功。
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}");
context.Result = "设置本次执行的值";
return Task.CompletedTask;
}
}
Result
和 Properties
除了通过 context.Result
设置作业本次执行结果以外,还可以通过 jobDetail.AddOrUpdateProperty(key, value)
的方式设置。区别在于前者会将值同步到 Trigger
的 Result
中,后者会将值同步在 JobDetail
的 Properties
中。
26.1.5 作业触发器 Trigger
及构建器
26.1.5.1 关于作业触发器
框架提供了 Trigger
类型来描述作业具体的触发时间,Trigger
类型提供以下只读属性:
属性名 | 属性类型 | 默认值 | 说明 |
---|---|---|---|
TriggerId | string | 作业触发器 Id | |
JobId | string | 作业 Id | |
TriggerType | string | 作业触发器类型,存储的是类型的 FullName | |
AssemblyName | string | 作业触发器类型所在程序集,存储的是程序集 Name | |
Args | string | 作业触发器初始化参数,运行时将反序列化为 object[] 类型并作为构造函数参数 | |
Description | string | 描述信息 | |
Status | TriggerStatus | Ready | 作业触发器状态 |
StartTime | DateTime? | 起始时间 | |
EndTime | DateTime? | 结束时间 | |
LastRunTime | DateTime? | 最近运行时间 | |
NextRunTime | DateTime? | 下一次运行时间 | |
NumberOfRuns | long | 0 | 触发次数 |
MaxNumberOfRuns | long | 0 | 最大触发次数,0 :不限制,n :N 次 |
NumberOfErrors | long | 0 | 出错次数 |
MaxNumberOfErrors | long | 0 | 最大出错次数,0 :不限制,n :N 次 |
NumRetries | int | 0 | 重试次数 |
RetryTimeout | int | 1000 | 重试间隔时间,毫秒单位 |
StartNow | bool | true | 是否立即启动 |
RunOnStart | bool | false | 是否启动时执行一次 |
ResetOnlyOnce | bool | true | 是否在启动时重置最大触发次数等于一次的作业 |
Result | string | 本次执行返回结果,Furion 4.8.7.7+ | |
ElapsedTime | long | 0 | 本次执行耗时,单位 ms ,Furion 4.8.7.7+ |
UpdatedTime | DateTime? | 作业触发器更新时间 |
26.1.5.2 作业触发器状态
作业触发器状态指示了当前作业触发器的状态,使用 TriggerStatus
枚举类型(uint
),该类型包含以下枚举成员。
枚举名 | 枚举值 | 说明 |
---|---|---|
Backlog | 0 | 积压,起始时间大于当前时间 |
Ready | 1 | 就绪 |
Running | 2 | 正在运行 |
Pause | 3 | 暂停 |
Blocked | 4 | 阻塞,本该执行但是没有执行 |
ErrorToReady | 5 | 由失败进入就绪,运行错 误当并未超出最大错误数,进入下一轮就绪 |
Archived | 6 | 归档,结束时间小于当前时间 |
Panic | 7 | 崩溃,错误次数超出了最大错误数 |
Overrun | 8 | 超限,运行次数超出了最大限制 |
Unoccupied | 9 | 无触发时间,下一次执行时间为 null |
NotStart | 10 | 初始化时未启动 |
Unknown | 11 | 未知作业触发器,作业触发器运行时类型为 null |
Unhandled | 12 | 未知作业处理程序,作业处理程序类型运行时类型为 null |
26.1.5.3 关于作业触发器构建器
作业触发器 Trigger
是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 Trigger
对象呢?
TriggerBuilder
是作业调度模块提供可用来生成运行时 Trigger
的类型,这样做的好处可避免外部直接修改运行时 Trigger
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 TriggerBuilder
对象。
- 通过
Create
静态方法创建
// 根据作业触发器 Id 创建
var triggerBuilder = TriggerBuilder.Create("trigger1");
// 根据 Trigger 派生类类型创建
var triggerBuilder = TriggerBuilder.Create<PeriodTrigger>();
// 根据 Trigger 派生类类型 + 构造函数参数创建
var triggerBuilder = TriggerBuilder.Create<CronTrigger>("* * * * *", CronStringFormat.Default);
// 根据程序集名称和类型完全限定名(FullName)创建
var triggerBuilder = TriggerBuilder.Create("Furion", "Furion.Schedule.PeriodTrigger");
// 根据程序集名称和类型完全限定名(FullName) + 构造函数参数创建
var triggerBuilder = TriggerBuilder.Create("Furion", "Furion.Schedule.PeriodTrigger", 1000);
// 根据 Type 类型创建
var triggerBuilder = TriggerBuilder.Create(typeof(PeriodTrigger));
// 根据 Type 类型 + 构造函数参数创建
var triggerBuilder = TriggerBuilder.Create(typeof(CronTrigger), "* * * * *", CronStringFormat.Default);
- 通过
Trigger
类型创建
这种方式常用于在运行时更新作业触发器。
var triggerBuilder = TriggerBuilder.From(trigger);
//也可以通过以下方式
var triggerBuilder = trigger.GetBuilder();
- 通过
JSON
字符串创建
该方式非常灵活,可从配置文件,JSON
字符串,或其他能够返回 JSON
字符串的地方创建。
var triggerBuilder = Triggers.From(@"
{
""triggerId"": ""job1_trigger1"",
""jobId"": ""job1"",
""triggerType"": ""Furion.Schedule.CronTrigger"",
""assemblyName"": ""Furion"",
""args"": ""[\""* * * * *\"",0]"",
""description"": null,
""status"": 1,
""startTime"": null,
""endTime"": null,
""lastRunTime"": ""2022-12-04 16:13:00.000"",
""nextRunTime"": null,
""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-04 16:13:00.045""
}");
如果使用的是 .NET7
,可使用 """
避免转义,如:
var triggerBuilder = Triggers.From("""
{
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Furion.Schedule.CronTrigger",
"assemblyName": "Furion",
"args": "[\"* * * * *\",0]",
"description": null,
"status": 8,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-12-04 16:13:00.000",
"nextRunTime": null,
"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-04 16:13:00.045"
}
""");
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
命名方式。
不支持 UnderScoreCase(下划线命名法)
,如 "include_annotations": true
- 还可以通过
Clone
静态方法从一个TriggerBuilder
创建
var triggerBuilder = TriggerBuilder.Clone(fromTriggerBuilder);
克隆操作只会克隆 AssemblyName
,TriggerType
,Args
,Description
,StartTime
,EndTime
,MaxNumberOfRuns
,MaxNumberOfErrors
,NumRetries
,RetryTimeout
,StartNow
,RunOnStart
,ResetOnlyOnce
。
不会克隆 TriggerId
,JobId
,Status
,LastRunTime
,NextRunTime
,NumberOfRuns
,NumberOfErrors
,Result
,ElapsedTime
,PersistentConnectionUpdatedTime
。
- 还可以通过
LoadFrom
实例方法填充当前的TriggerBuilder
比如可以传递匿名类型, 类类型,字典 Dictionary<string, object>
类型:
// 会覆盖所有相同的值
triggerBuilder.LoadFrom(new
{
Description = "我是描述",
StartTime = DateTime.Now
});
// 支持多个填充,还可以配置跳过 null 值覆盖
triggerBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
StartTime = default(object),
}, ignoreNullValue: true);
// 支持忽略特定属性名映射
triggerBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
TriggerId = "trigger1"
}, ignorePropertyNames: new[]{ "description" });
// 支持字典类型
triggerBuilder.LoadFrom(new Dictionary<string, object>
{
{"Description", "这是新的描述" },
{"updatedTime", DateTime.Now }
});
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
和 UnderScoreCase(下划线命名法)
命名方式。
26.1.5.4 内置作业触发器构建器
为了方便快速实现作业触发器,作业调度模块内置了 Period(间隔)
和 Cron(表达式)
作业触发器,可通过 TriggerBuilder
类型或 Triggers
静态类创建。
TriggerBuilder
方式
// 创建毫秒周期(间隔)作业触发器构建器
var triggerBuilder = TriggerBuilder.Period(5000);
// 创建 Cron 表达式作业触发器构建器
var triggerBuilder = TriggerBuilder.Cron("* * * * *", CronStringFormat.Default);
Triggers
方式,❤️ 推荐
Triggers
静态类具备 TriggerBuilder
所有的静态方法同时还添加了不少更加便捷的静态方法。
// 间隔 Period 方式
// 创建毫秒周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.Period(5000);
// 创建秒周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.PeriodSeconds(5);
// 创建分钟周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.PeriodMinutes(5);
// 创建小时周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.PeriodHours(5);
// Cron 表达式方式
// 创建 Cron 表达式作业触发器构建器
var triggerBuilder = Triggers.Cron("* * * * *", CronStringFormat.Default);
// 创建每秒开始作业触发器构建器
var triggerBuilder = Triggers.Secondly();
// 创建每分钟开始作业触发器构建器
var triggerBuilder = Triggers.Minutely();
// 创建每小时开始作业触发器构建器
var triggerBuilder = Triggers.Hourly();
// 创建每天(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Daily();
// 创建每月1号(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Monthly();
// 创建每周日(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Weekly();
// 创建每年1月1号(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Yearly();
// 创建每周一至周五(午 夜)开始作业触发器构建器
var triggerBuilder = Triggers.Workday();
// Cron 表达式 Macro At 方式
// 每第 3 秒
var triggerBuilder = Triggers.SecondlyAt(3);
// 每第 3,5,6 秒
var triggerBuilder = Triggers.SecondlyAt(3, 5, 6);
// 每分钟第 3 秒
var triggerBuilder = Triggers.MinutelyAt(3);
// 每分钟第 3,5,6 秒
var triggerBuilder = Triggers.MinutelyAt(3, 5, 6);
// 每小时第 3 分钟
var triggerBuilder = Triggers.HourlyAt(3);
// 每小时第 3,5,6 分钟
var triggerBuilder = Triggers.HourlyAt(3, 5, 6);
// 每天第 3 小时正(点)
var triggerBuilder = Triggers.DailyAt(3);
// 每天第 3 ,5,6 小时正(点)
var triggerBuilder = Triggers.DailyAt(3, 5, 6);
// 每月第 3 天零点正
var triggerBuilder = Triggers.MonthlyAt(3);
// 每月第 3,5,6 天零点正
var triggerBuilder = Triggers.MonthlyAt(3, 5, 6);
// 每周星期 3 零点正
var triggerBuilder = Triggers.WeeklyAt(3);
var triggerBuilder = Triggers.WeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT
// 每周星期 3,5,6 零点正
var triggerBuilder = Triggers.WeeklyAt(3, 5, 6);
var triggerBuilder = Triggers.WeeklyAt("WED", "FRI", "SAT");
// 还支持混合
var triggerBuilder = Triggers.WeeklyAt(3, "FRI", 6);
// 每年第 3 月 1 日零点正
var triggerBuilder = Triggers.YearlyAt(3);
var triggerBuilder = Triggers.YearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
// 每年第 3,5,6 月 1 日零点正
var triggerBuilder = Triggers.YearlyAt(3);
var triggerBuilder = Triggers.YearlyAt(3, 5, 6);
var triggerBuilder = Triggers.YearlyAt("MAR", "MAY", "JUN");
// 还支持混合
var triggerBuilder = Triggers.YearlyAt(3, "MAY", 6);
26.1.5.5 自定义作业触发器
除了使用框架提供的 PeriodTrigger
和 CronTrigger
以外,还可以自定义作业触发器,只需要继承 Trigger
并重写 GetNextOccurrence
方法即可,如实现一个间隔两秒的作业触发器。
public class CustomTrigger : Trigger
{
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(2);
}
}
之后可通过 TriggerBuilder.Create
或 Triggers.Create
创建即可:
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<CustomTrigger>());
});
查看作业执行结果:
info: 2022-12-04 17:19:25.0980531 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:19:25.1027083 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:19:25.2702054 +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 17:19:25.2723418 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:19:25.2999295 +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 17:19:27.2849015 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> 1ts 2022-12-04 17:19:27.234 -> 2022-12-04 17:19:29.232
info: 2022-12-04 17:19:29.2604639 +08:00 星期日 L MyJob[0] #4
<job1> [C] <job1 job1_trigger1> 2ts 2022-12-04 17:19:29.232 -> 2022-12-04 17:19:31.225
info: 2022-12-04 17:19:31.2422514 +08:00 星期日 L MyJob[0] #10
<job1> [C] <job1 job1_trigger1> 3ts 2022-12-04 17:19:31.225 -> 2022-12-04 17:19:33.207
另外,自定义作业触发器还支持配置构造函数参数
如果自定义作业触发器包含参数,那么必须满足以下两个条件:
- 参数必须通过唯一的构造函数传入,有且最多只能拥有一个构造函数
- 参数的类型只能是
int
,string
,bool
,null
或由它们组成的数组类型
public class CustomTrigger : Trigger
{
public CustomTrigger(int seconds) // 可支持多个参数
{
Seconds = seconds;
}
private int Seconds { get; set; }
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}
}
之后可通过 TriggerBuilder.Create
或 Triggers.Create
创建并传入参数。
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<CustomTrigger>(3));
});
查看作业执行结果:
info: 2022-12-04 17:23:09.3029251 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:23:09.3205593 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:23:09.7081119 +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 17:23:09.7506504 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:23:09.9380816 +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 17:23:12.6291716 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1 job1_trigger1> 1ts 2022-12-04 17:23:12.590 -> 2022-12-04 17:23:15.582
info: 2022-12-04 17:23:15.6141563 +08:00 星期日 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> 2ts 2022-12-04 17:23:15.582 -> 2022-12-04 17:23:18.572
info: 2022-12-04 17:23:18.5857464 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> 3ts 2022-12-04 17:23:18.572 -> 2022-12-04 17:23:21.551
自定义作业触发器除了可重写 GetNextOccurrence
方法之后,还提供了 ShouldRun
和 ToString
方法可重写,如:
public class CustomTrigger : Trigger
{
public CustomTrigger(int seconds)
{
Seconds = seconds;
}
private int Seconds { get; set; }
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}
public override bool ShouldRun(JobDetail jobDetail, DateTime startAt)
{
// 在这里进一步控制,如果返回 false,则作业触发器跳过执行
return base.ShouldRun(jobDetail, startAt);
}
public override string ToString()
{
return $"<{TriggerId}> 自定义递增 {Seconds}s 触发器";
}
}
推荐重写 GetNextRunTime
和 ToString
方法即可,如:
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;
}
}
查看作业执行结果:
info: 2022-12-04 17:26:43.9120082 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:26:43.9166481 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:26:44.1786114 +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 17:26:44.1816154 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:26:44.2077386 +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 17:26:47.1904549 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:26:47.139 -> 2022-12-04 17:26:50.145
info: 2022-12-04 17:26:50.1652618 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:26:50.145 -> 2022-12-04 17:26:53.129
info: 2022-12-04 17:26:53.1426614 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:26:53.129 -> 2022-12-04 17:26:56.106
26.1.5.6 作业触发器特性及自定义
如果 JobBuilder
配置了 IncludeAnnotations
参数且为 true
,那么将会自动解析 IJob
的实现类型的所有继承 TriggerAttribute
的特性,目前作业调度模块内置了以下作业触发器特性:
[Period(5000)]
:毫秒周期(间隔)作业触发器特性[PeriodSeconds(5)]
:秒周期(间隔)作业触发器特性[PeriodMinutes(5)]
:分钟周期(间隔)作业触发器特性[PeriodHours(5)]
:小时周期(间隔)作业触发器特性[Cron("* * * * *", CronStringFormat.Default)]
:Cron 表达式作业触发器特性[Secondly]
:每秒开始作业触发器特性[Minutely]
:每分钟开始作业触发器特性[Hourly]
:每小时开始作业触发器特性[Daily]
:每天(午夜)开始作业触发器特性[Monthly]
:每月 1 号(午夜)开始作业触发器特性[Weekly]
:每周日(午夜)开始作业触发器特性[Yearly]
:每年 1 月 1 号(午夜)开始作业触发器特性[Workday]
:每周一至周五(午夜)开始触发器特性[SecondlyAt]
:特定秒开始作业触发器特性[MinutelyAt]
:每分钟特定秒开始作业触发器特性[HourlyAt]
:每小时特定分钟开始作业触发器特性[DailyAt]
:每天特定小时开始作业触发器特性[MonthlyAt]
:每月特定天(午夜)开始作业触发器特性[WeeklyAt]
:每周特定星期几(午夜)开始作业触发器特性[YearlyAt]
:每年特定月 1 号(午夜)开始作业触 发器特性
使用如下:
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true));
// 也支持自定义配置 + 特性扫描
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true)
, Triggers.PeriodSeconds(5));
// 或者通过类型扫描
options.AddJob(typeof(MyJobj).ScanToBuilder());
// 还可以批量扫描 Furion 4.8.2.4+
options.AddJob(App.EffectiveTypes.ScanToBuilders());
});
[Minutely]
[PeriodSeconds(5)]
[Cron("*/3 * * * * *", 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;
}
}
查看作业执行结果:
info: 2022-12-04 17:35:47.0211372 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:35:47.0267027 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:35:47.2906591 +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 17:35:47.2921849 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 17:35:47.2961669 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger3> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 17:35:47.2979859 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:35:47.3194555 +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 17:35:48.0588231 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger3> */3 * * * * * 1ts 2022-12-04 17:35:48.000 -> 2022-12-04 17:35:51.000
info: 2022-12-04 17:35:51.0240459 +08:00 星期日 L MyJob[0] #9
<job1> [C] <job1 job1_trigger3> */3 * * * * * 2ts 2022-12-04 17:35:51.000 -> 2022-12-04 17:35:54.000
info: 2022-12-04 17:35:52.2643935 +08:00 星期日 L MyJob[0] #12
<job1> [C] <job1 job1_trigger2> 5s 1ts 2022-12-04 17:35:52.246 -> 2022-12-04 17:35:57.227
info: 2022-12-04 17:35:54.0175524 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> */3 * * * * * 3ts 2022-12-04 17:35:54.000 -> 2022-12-04 17:35:57.000
info: 2022-12-04 17:35:57.0270544 +08:00 星期日 L MyJob[0] #9
<job1> [C] <job1 job1_trigger3> */3 * * * * * 4ts 2022-12-04 17:35:57.000 -> 2022-12-04 17:36:00.000
info: 2022-12-04 17:35:57.2433514 +08:00 星期日 L MyJob[0] #12
<job1> [C] <job1 job1_trigger2> 5s 2ts 2022-12-04 17:35:57.227 -> 2022-12-04 17:36:02.208
info: 2022-12-04 17:36:00.0151605 +08:00 星期日 L MyJob[0] #14
<job1> [C] <job1 job1_trigger3> */3 * * * * * 5ts 2022-12-04 17:36:00.000 -> 2022-12-04 17:36:03.000
info: 2022-12-04 17:36:00.0315972 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> * * * * * 1ts 2022-12-04 17:36:00.000 -> 2022-12-04 17:37:00.000
info: 2022-12-04 17:36:02.2203934 +08:00 星期日 L MyJob[0] #12
<job1> [C] <job1 job1_trigger2> 5s 3ts 2022-12-04 17:36:02.208 -> 2022-12-04 17:36:07.184
除了使用内置特性,我们还可以自定义作业触发器特性,如:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class CustomAttribute : TriggerAttribute
{
public CustomAttribute(int seconds)
: base(typeof(CustomTrigger), seconds)
{
}
}
- 必须继承
TriggerAttribute
特性类 - 至少包含一个构造函数且通过基类构造函数配置
:base(实际触发器类型, 构造函数参数)
- 推荐添加
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
特性
使用如下:
[Custom(3)]
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;
}
}
查看作业执行结果:
info: 2022-12-04 17:44:12.2702884 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:44:12.2872399 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:44:12.5730241 +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 17:44:12.5751444 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:44:12.6174459 +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 17:44:15.5850848 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:15.537 -> 2022-12-04 17:44:18.542
info: 2022-12-04 17:44:18.5693881 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:18.542 -> 2022-12-04 17:44:21.527
info: 2022-12-04 17:44:21.5396428 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:21.527 -> 2022-12-04 17:44:24.504
作业触发器特性还提供了多个属性配置,如:
TriggerId
:作业触发器Id
,string
类型Description
:描述信息,string
类型StartTime
:起始时间,string
类型EndTime
:结束时间,string
类型MaxNumberOfRuns
:最大触发次数,long
类型,0
:不限制;n
:N 次MaxNumberOfErrors
:最大出错次数,long
类型,0
:不限制;n
:N 次NumRetries
:重试次数,int
类型,默认值0
RetryTimeout
:重试间隔时间,int
类型,默认值1000
StartNow
:是否立即启动,bool
类型,默认值true
RunOnStart
:是否启动时执行一次,bool
类型,默认值false
ResetOnlyOnce
:是否在启动时重置最大触发次数等于一次的作业,bool
类型,默认值true
使用如下:
[PeriodSeconds(5, TriggerId = "trigger1", Description = "这是一段描述")]
public class MyJob : IJob
{
// ...
}
26.1.5.7 设置作业触发器构 建器
TriggerBuilder
提供了和 Trigger
完全匹配的 Set[属性名]
方法来配置作业触发器各个属性,如:
services.AddSchedule(options =>
{
var triggerBuilder = Triggers.Period(5000)
.SetTriggerId("trigger1") // 作业触发器 Id
.SetTriggerType("Furion", "Furion.Schedule.PeriodTrigger") // 作业触发器类型,支持多个重载
.SetTriggerType<PeriodTrigger>() // 作业触发器类型,支持多个重载
.SetTriggerType(typeof(PeriodTrigger)) // 作业触发器类型,支持多个重载
.SetArgs("[5000]") // 作业触发器参数 object[] 序列化字符串类型,支持多个重载
.SetArgs(5000) // 作业触发器参数,支持多个重载
.SetDescription("作业触发器描述") // 作业触发器描述
.SetStatus(TriggerStatus.Ready) // 作业触发器状态
.SetStartTime(DateTime.Now) // 作 业触发器起始时间
.SetEndTime(DateTime.Now.AddMonths(1)) // 作业触发器结束时间
.SetLastRunTime(DateTime.Now.AddSeconds(-5)) // 作业触发器最近运行时间
.SetNextRunTime(DateTime.Now.AddSeconds(5)) // 作业触发器下一次运行时间
.SetNumberOfRuns(1) // 作业触发器触发次数
.SetMaxNumberOfRuns(100) // 作业触发器最大触发器次数
.SetNumberOfErrors(1) // 作业触发器出错次数
.SetMaxNumberOfErrors(100) // 作业触发器最大出错次数
.SetNumRetries(3) // 作业触发器出错重试次数
.SetRetryTimeout(1000) // 作业触发器重试间隔时间
.SetStartNow(true) // 作业触发器是否立即启动
.SetRunOnStart(false) // 作业触发器是否启动时执行一次
.SetResetOnlyOnce(true) // 作业触发器是否在启动时重置最大触发次数等于一次的作业
.SetResult("本次返回结果") // 作业触发器本次执行返回结果,Furion 4.8.7.7+
.SetElapsedTime(100) // 作业触发器本次执行耗时,Furion 4.8.7.7+
;
options.AddJob<MyJob>(triggerBuilder);
});
26.1.5.8 作业触发器持久化方法
作业触发器构建器 TriggerBuilder
提供了三个标记作业持久化行为的方法:
Appended()
:标记作业触发器构建器是新增的,届时生成的SQL
是INSERT
语句Updated()
:标记作业触发器构建器已被更新,届时生成的SQL
是Updated
语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中Removed()
:标记作业触发器构建器已被删除,届时生成的SQL
是Deleted
语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至作业计划中
services.AddSchedule(options =>
{
options.AddJob<MyJob>(
Triggers.PeriodSeconds(5).SetTriggerId("trigger1").Appended()
, Triggers.PeriodSeconds(5).SetTriggerId("trigger2").Updated()
, Triggers.PeriodSeconds(5).SetTriggerId("trigger3").Removed());
});
查看作业调度器初始化日志:
info: 2022-12-04 18:29:22.3997873 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 18:29:22.4045304 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 18:29:22.5473237 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <trigger3> trigger for scheduler of <job1> successfully removed to the schedule.
info: 2022-12-04 18:29:22.5504289 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 18:29:22.5521396 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <trigger2> trigger for scheduler of <job1> successfully appended and updated to the schedule.
info: 2022-12-04 18:29:22.5535657 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 18:29:22.5896298 +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 18:29:27.5981907 +08:00 星期日 L MyJob[0] #14
<job1> [C] <job1 trigger2> 5s 1ts 2022-12-04 18:29:27.507 -> 2022-12-04 18:29:32.500
info: 2022-12-04 18:29:27.6002420 +08:00 星期日 L MyJob[0] #15
<job1> [C] <job1 trigger1> 5s 1ts 2022-12-04 18:29:27.507 -> 2022-12-04 18:29:32.500
info: 2022-12-04 18:29:32.5850223 +08:00 星期日 L MyJob[0] #12
<job1> [C] <job1 trigger2> 5s 2ts 2022-12-04 18:29:32.500 -> 2022-12-04 18:29:37.548
info: 2022-12-04 18:29:32.6034646 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 trigger1> 5s 2ts 2022-12-04 18:29:32.500 -> 2022-12-04 18:29:37.548