Skip to content

26.1 调度作业

📝 模块更新日志 新特性*

+ 新增 定时任务看板可自动识别是否使用 `UTC` 时间 4\.9\.4\.7 ⏱️2024\.07\.21 [9e3e3bb](https://gitee.com/dotnetchina/Furion/commit/9e3e3bb5b8da9a16b52511683cbe599ad9080433)
+ 新增 定时任务可配置是否打印 `HTTP` 作业的结果 `PrintResponseContent` 配置 4\.9\.4\.4 ⏱️2024\.07\.10 [93cb339](https://gitee.com/dotnetchina/Furion/commit/93cb339e3531fd7ee3a87ed0e32909363f580ba1)
+ 新增 改进定时任务看板,可直观辨别 `HTTP` 作业和其他作业 4\.9\.4\.3 ⏱️2024\.07\.01 [19e8a79](https://gitee.com/dotnetchina/Furion/commit/19e8a792209e5e2138014e5a9dfccd477ce8d8a0)
+ 新增 改进定时任务看板执行日志,支持查看执行异常信息 4\.9\.4\.3 ⏱️2024\.07\.01 [800a6e4](https://gitee.com/dotnetchina/Furion/commit/800a6e4bc0b1d85d12b5184d99c8f246a7e34f19)
+ 新增 定时任务 `Http` 作业支持配置 `Timeout` 超时时间 4\.9\.3\.13 ⏱️2024\.05\.28 [9514fa4](https://gitee.com/dotnetchina/Furion/commit/9514fa42d46c59a580f28d2cc98dd2fb4b871d83)
+ 新增 定时任务作业计划(含构建器)获取作业触发器数量 `TriggerCount` 属性 4\.9\.3\.6 ⏱️2024\.05\.20 [f9cb042](https://gitee.com/dotnetchina/Furion/commit/f9cb04204933d9ae3e82ca4343412365f539e67d)
+ 新增 定时任务执行上下文 `Mode` 属性,可标识作业触发器是定时触发还是手动触发 4\.9\.3\.1 ⏱️2024\.05\.15 [efe0739](https://gitee.com/dotnetchina/Furion/commit/efe07399da60280b8cc8bbc765ed10e84a54c4c6) [01a4003](https://gitee.com/dotnetchina/Furion/commit/01a40037ca535985650372c71e5f5f3faeca8f4d)
+ 新增 定时任务支持立即执行触发特定作业下的特定触发器 4\.9\.3 ⏱️2024\.05\.10 [3d83342](https://gitee.com/dotnetchina/Furion/commit/3d833428e6097fe991de0775c40aaf6fa6522c7d)
+ 新增 定时任务看板可配置是否默认展开所有作业触发器 `DefaultExpandAllJobs` 4\.9\.2\.40 ⏱️2024\.05\.07 [77c1e6f](https://gitee.com/dotnetchina/Furion/commit/77c1e6f933b2a56e554276cd7f46033f4a47bac5)
+ 新增 定时任务看板支持是否显示空触发器作业 `DisplayEmptyTriggerJobs` 和是否显示页头 `DisplayHead` 4\.9\.2\.39 ⏱️2024\.05\.07 [f64d45f](https://gitee.com/dotnetchina/Furion/commit/f64d45f658f1fca22cc624dfaa324e124b490fdd)
+ 新增 定时任务支持取消指定触发器正在执行的作业程序 4\.9\.2\.38 ⏱️2024\.05\.07 [5aa20b5](https://gitee.com/dotnetchina/Furion/commit/5aa20b583f1d2c11fe5ebce069e78985e7fb7ff5)
+ 新增 定时任务作业计划支持根据触发器 `Id` 集合做批量删除操作 4\.9\.2\.33 ⏱️2024\.04\.30 [d01a6e7](https://gitee.com/dotnetchina/Furion/commit/d01a6e7ee42e197f78b054e360c34b337fe3d902)
+ 新增 支持检查作业信息额外数据的键是否定义 `ContainsProperty(key)` 方法 4\.9\.2\.32 ⏱️2024\.04\.28 [71f97f0](https://gitee.com/dotnetchina/Furion/commit/71f97f0304c5d5048b09c87d9d2928be8ff9dc86)
+ 新增 定时任务支持配置作业触发器 `RunOnStart` 的处理逻辑 `options.RunOnStartProvider` 4\.9\.2\.29 ⏱️2024\.04\.23 [c9e0e3e](https://gitee.com/dotnetchina/Furion/commit/c9e0e3eb0bde08cb6e12a4ae0cbf834f70e44768)
+ 新增 定时任务作业计划工厂 `ISchedulerFactory` 启停作业 `StartJob` 和 `PauseJob` 方法 4\.9\.2\.16 ⏱️2024\.04\.11 [89061ef](https://gitee.com/dotnetchina/Furion/commit/89061ef4679378dd76e162323927f7654a5e1390)
+ 新增 定时任务批量设置作业组名称 `.GroupSet` 方法 4\.9\.2\.9 ⏱️2024\.04\.09 [9e08278](https://gitee.com/dotnetchina/Furion/commit/9e08278e95ba0ec98a3ffaeb54c46b598d7d6c81)
+ 新增 定时任务启动时检查不合法的作业触发器配置并打印警告日志 4\.9\.2 ⏱️2024\.03\.28 [3190f4c](https://gitee.com/dotnetchina/Furion/commit/3190f4c705bf7fb84b210e1239254626253c9e94)
+ 新增 定时任务 `IJobPersistence` 持久化接口 `OnExecutionRecord` 方法实现作业运行记录持久化 4\.9\.1\.16 ⏱️2024\.01\.01 [4d4d492](https://gitee.com/dotnetchina/Furion/commit/4d4d492fc91fe312ca30770ec30a6f692fccbf0e)
+ 新增 定时任务作业调度计划 `OnExecutionRecord` 事件,可监听作业运行记录 4\.9\.1\.16 ⏱️2024\.01\.01 [4d4d492](https://gitee.com/dotnetchina/Furion/commit/4d4d492fc91fe312ca30770ec30a6f692fccbf0e)
+ 新增 定时任务支持取消正在运行的作业程序 4\.9\.1\.9 ⏱️2023\.12\.03 [3cc9be0](https://gitee.com/dotnetchina/Furion/commit/3cc9be0da1eabc0f3baaa7801b115bb2909bb085)
+ 新增 定时任务 `Http` 作业请求头 `Headers` 和作业分组 `Group` 和描述 `Description` 支持 4\.8\.8\.46 ⏱️2023\.10\.09 [\#I85Z7S](https://gitee.com/dotnetchina/Furion/issues/I85Z7S)
+ 新增 定时任务看板列表支持作业分组名排序 4\.8\.8\.43 ⏱️2023\.09\.14 [\#I7YQ9V](https://gitee.com/dotnetchina/Furion/issues/I7YQ9V)
+ 新增 定时任务作业计划 `OnChanged` 事件处理 4\.8\.8\.29 ⏱️2023\.06\.25 [e4c4cf1](https://gitee.com/dotnetchina/Furion/commit/e4c4cf1d418f3cc2291eca7d7dd1c8b62d17b0e9)
+ 新增 定时任务支持二级虚拟目录 `VisualPath` 配置部署 4\.8\.8\.20 ⏱️2023\.05\.18 [\#I740IA](https://gitee.com/dotnetchina/Sundial/issues/I740IA)
+ 新增 定时任务作业处理程序工厂 `IJobFactory` 支持 4\.8\.8\.13 ⏱️2023\.05\.08 [ad58dd3](https://gitee.com/dotnetchina/Furion/commit/ad58dd3141ed40e58cd486895ac6c1f21803797c)
+ 新增 定时任务 `Schedular.CompileCSharpClassCode(code)` 支持动态编译作业处理程序代码 4\.8\.8\.7 ⏱️2023\.04\.30 [fe1e8a1](https://gitee.com/dotnetchina/Furion/commit/fe1e8a1768c7020477684689b35a2a1349ec2b01)
+ 新增 定时任务支持配置 `IJob` 执行异常 `FallbackAsync` 回退策略 4\.8\.8\.6 ⏱️2023\.04\.25 [7671489](https://gitee.com/dotnetchina/Furion/commit/7671489a46ec7c957e92b7fbf9836e27f9077e24)
+ 新增 定时任务支持在非 `IOC/DI` 项目类型中使用 4\.8\.8\.5 ⏱️2023\.04\.24 [\#I6YJNB](https://gitee.com/dotnetchina/Sundial/issues/I6YJNB)
+ 新增 定时任务看板支持自定义刷新频率 `SyncRate` 功能 4\.8\.7\.43 ⏱️2023\.04\.12 [703b465](https://gitee.com/dotnetchina/Furion/commit/703b465f41510d86976d325cd31d7f8eba3a31ec)
+ 新增 定时任务看板支持完全自定义 `RequestPath` 入口地址功能 4\.8\.7\.34 ⏱️2023\.04\.04 [24736f6](https://gitee.com/dotnetchina/Furion/commit/24736f6421dd5aa90289fbb9bc519e6ef55e667f)
+ 新增 **定时任务一系列 `.AlterTo` 修改作业触发器触发时间便捷方法** 4\.8\.7\.31 ⏱️2023\.03\.31 [0349017](https://gitee.com/dotnetchina/Furion/commit/0349017902835bed91041fb3ea1ee987b0a81bbb)
+ 新增 定时任务看板 `UI` 作业列表 `最近执行时间` 列和优化显示效果 4\.8\.7\.12 ⏱️2023\.03\.15 [26462a8](https://gitee.com/dotnetchina/Furion/commit/26462a84e553e39ce4cddd5128833ff732c85f3e) [cb5dd17](https://gitee.com/dotnetchina/Furion/commit/cb5dd17969244987b847fcd96825d28b243a5b9f)
+ 新增 定时任务作业计划/工厂立即执行 `RunJob` 方法 4\.8\.7\.11 ⏱️2023\.03\.15 [\#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X)
+ 新增 定时任务看板 `UI` 提供立即执行功能 4\.8\.7\.11 ⏱️2023\.03\.15 [\#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X)
+ 新增 定时任务作业执行上下文 `JobExecutionContext` 服务提供器 `ServiceProvider` 属性 4\.8\.7\.10 ⏱️2023\.03\.14 [02586f8](https://gitee.com/dotnetchina/Furion/commit/02586f83edb4f98e4801ae65080c2d6aa5545763)
+ 新增 **定时任务 `HTTP` 作业,支持定时请求互联网 `URL` 地址** 4\.8\.7\.7 ⏱️2023\.03\.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15)

查看变化

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

    • 新增 定时任务作业触发器 Trigger 执行结果 Result 和执行耗时 ElapsedTime 属性 4.8.7.7 ⏱️2023.03.11 01d4466
    • 新增 定时任务作业看板支持查看作业触发器执行结果 Result 和执行耗时 ElapsedTime 属性 4.8.7.7 ⏱️2023.03.11 01d4466
    • 新增 定时任务休眠时长和唤醒时机日志输出 4.8.7.6 ⏱️2023.03.08 #I6LANE
    • 新增 定时任务 IScheduler.[Try]UpdateDetail(builder => {})IScheduler.[Try]UpdateTrigger(triggerId, builder => {}) 重载方法 4.8.6 ⏱️2023.02.08 6e43a54

查看变化* 更新作业信息

// 返回 ScheduleResult 类型  
var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder =>  
{  
      jobBuilder.SetDescription("~~~");  
}, out var jobDetail);  

// 无返回值  
scheduler.UpdateDetail(jobBuilder =>  
{  
      jobBuilder.SetDescription("~~~");  
});  

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

// 无返回值  
scheduler.UpdateTrigger("triggerId", triggerBuilder =>  
{  
      triggerBuilder.SetDescription("~~");  
});  

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

    • 调整 定时任务持久化 IJobPersistence.OnExecutionRecordAsync 方法签名和 ISchedulerFactory.OnExecutionRecord 事件参数 4.9.4.2 ⏱️2024.06.21 9d6def5

查看变化 IJobPersistence 接口*

- // 旧版本  
- Task OnExecutionRecordAsync(TriggerTimeline timeline);  

+ // 新版本  
+ Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context);  

  • ISchedulerFactory 接口
- // 旧版本  
- schedulerFactory.OnExecutionRecord += (sender, args) =>  
- {  
-     var timeline = args.Timeline;  
- }  

+ // 新版本  
+ schedulerFactory.OnExecutionRecord += (sender, args) =>  
+ {  
+     var timeline = args.Context.Timeline;    // 通过 .Context  获取更多信息  
+ }  

    • 调整 定时任务作业执行上下文 RunId 类型,由 Guid 改为 string 类型 4.9.2.38 ⏱️2024.05.07 5aa20b5
    • 调整 定时任务作业计划工厂 TryRunJob 方法签名,追加 out IScheduler scheduler 参数 4.9.2.16 ⏱️2024.04.11 89061ef
    • 调整 定时任务持久化 IJobPersistence 接口方法为异步方法 4.9.1.59 ⏱️2024.03.25 c6af42d
    • 新增 定时任务 IJobPersistence 持久化接口 OnExecutionRecord 方法实现作业运行记录持久化 4.9.1.16 ⏱️2024.01.01 4d4d492
    • 移除 定时任务看板 SyncRate 配置,前后端采用最新的 SSE 推送技术替代 4.8.8.29 ⏱️2023.06.25 e4c4cf1
    • 调整 定时任务动态作业 DynamicJob 委托/方法签名 4.8.7.10 ⏱️2023.03.14 6d56b53

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

由:

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

调整为:

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

    • 调整 定时任务底层所有代码,日志,注释,文档 4.8.1.10 ⏱️2022.12.05
  • 问题修复

    • 修复 使用 Nginx 部署项目时定时任务看板数据被缓存问题 4.9.5.9 ⏱️2024.09.18 375dfdd
    • 修复 定时任务看板 DateTime 类型格式化可能受操作系统时区影响导致异常问题 4.9.5.2 ⏱️2024.08.12 0e13cd6
    • 修复 定时任务看板自定义入口地址必须严格以 / 开头且不以 / 结尾的问题 4.9.5 ⏱️2024.08.09 5798317
    • 修复 定时任务因 f9cb042 提交导致的异常 4.9.3.13 ⏱️2024.05.28 da4e6fb #I9SPOL
    • 修复 定时任务看板点击作业信息列表的操作按钮也会触发展开/收缩作业触发器 bug 4.9.2.40 ⏱️2024.05.07 77c1e6f
    • 修复 定时任务间隔触发器获取下一周期时间缺少了 RunOnStartStartTime 考虑场景 4.9.2.30 ⏱️2024.04.23 2595379 7ac6a54
    • 修复 定时任务看板作业触发器类型文字过多出现超出布局情况 4.9.2.29 ⏱️2024.04.23 f9dd33b
    • 修复 定时任务毫秒级间隔触发器存在严重的误差问题 4.9.2.26 ⏱️2024.04.22 9c8210c
    • 修复 定时任务创建作业处理程序存在内存溢出风险 4.9.2.25 ⏱️2024.04.19 #I9D0RH
    • 修复 定时任务因新增 GroupSet 功能影响到了原有的 SetGroupName 逻辑 4.9.2.15 ⏱️2024.04.11 #I9FOU0 9e08278
    • 修复 定时任务生成 PostgreSQL 数据库 SQL 语句的字段名缺少 " 双引号 4.9.2.13 ⏱️2024.04.10 #I9FD9Y
    • 修复 定时任务使用 JobBuilder 构建委托作业永远无法执行问题 4.9.2.10 ⏱️2024.04.10 Sundial#I7KU7K
    • 修复 定时任务持久化单个作业触发器订阅执行器出现异常导致持久化服务宕机问题 4.9.1.60 ⏱️2024.03.26 a1014db
    • 修复 定时任务设置作业触发器 RunOnStart 在配置了 StartTime 属性后出现启动时机错误问题 4.9.1.42 ⏱️2024.03.08 c1e2eb3
    • 修复 定时任务设置作业触发器 StartTime 后导致获取间隔触发器 NextRunTime 不正确问题 4.9.1.41 ⏱️2024.03.07 734a8c3 1756ab4
    • 修复 定时任务设置触发器 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
    • 修复 定时任务作业触发器配置了 EndTimeStartTime 之后 Status 没有对应上 4.8.3.1 ⏱️2022.12.09 52a5506
    • 修复 定时任务通过 scheduler.AddTrigger(triggerBuilder) 无效的问题 4.8.3.1 ⏱️2022.12.09 #I65EQ1
    • 修复 作业拥有多个触发器时暂停作业后依然存在个别未暂停的清空(并发问题) 4.8.2.12 ⏱️2022.12.07 #I655W9
    • 修复 作业触发器不符合下一次执行规律但 NextRunTime 不为 null 情况 4.8.1.5 ⏱️2022.11.25 a8be728
    • 修复 运行时启动/暂停作业无效问题 4.8.1.6 ⏱️2022.11.25 #I6368M
    • 修复 定时任务生成的 SQL 语句不支持 MySQL 问题 4.8.1.7 ⏱️2022.11.26 #I638ZC
    • 其他更改

    • 调整 定时任务 GC 垃圾回收器回收时机,降低长时间内存占用 4.9.1.23 ⏱️2024.01.16 f43fc25

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

    • 新增 作业执行器实现超时文档 4.8.3.8 ⏱️2022.12.20

    • 新增 作业触发器 ResetOnlyOnce 文档 4.8.1.5 ⏱️2022.11.25 a8be728
    • 新增 通过 Roslyn 动态编译代码创建 IJob 类型文档 4.8.1.5 ⏱️2022.11.25 2c5e5be
    • 新增 自定义 JobDetailTrigger 输出 SQL 文档 4.8.2 ⏱️2022.11.27 0bb9d8f

v4.8.0 之前版本说明Furion 4.8.0+ 版本采用 Sundial 定时任务替换原有的 TaskScheduler查看旧文档

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

26.1.1 关于调度作业

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

26.1.1.1 使用场景

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

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

26.1.2 快速入门

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

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

  1. Startup.cs 注册 Schedule 服务:
services.AddSchedule(options =>  
{  
    // 注册作业,并配置作业触发器  
    options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行  
});  

  1. 查看作业执行结果
info: 2022-12-02 16:51:33.5032989 +08:00 星期五 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-02 16:51:33.5180669 +08:00 星期五 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-02 16:51:34.1452041 +08:00 星期五 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-02 16:51:34.1541701 +08:00 星期五 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-02 16:51:34.1748401 +08:00 星期五 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-02 16:51:35.0712571 +08:00 星期五 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 16:51:35.000 -> 2022-12-02 16:51:36.000  
info: 2022-12-02 16:51:36.0317375 +08:00 星期五 L MyJob[0] #14  
      <job1> [C] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 16:51:36.000 -> 2022-12-02 16:51:37.000  
info: 2022-12-02 16:51:37.0125007 +08:00 星期五 L MyJob[0] #9  
      <job1> [C] <job1 job1_trigger1> * * * * * * 3ts 2022-12-02 16:51:37.000 -> 2022-12-02 16:51:38.000  
info: 2022-12-02 16:51:38.0179920 +08:00 星期五 L MyJob[0] #8  
      <job1> [C] <job1 job1_trigger1> * * * * * * 4ts 2022-12-02 16:51:38.000 -> 2022-12-02 16:51:39.000  

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

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

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

26.1.2.1 指定作业 Id

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

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

查看作业执行结果:

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

26.1.2.2 多个作业触发器

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

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

查看作业执行结果:

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

26.1.2.3 串行 执行

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

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

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

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

查看作业执行结果:

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

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

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

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

查看作业执行结果:

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

26.1.2.4 打印作业完整信息

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

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

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

查看作业打印结果:

info: 2022-12-02 18:00:59.4140802 +08:00 星期五 L MyJob[0] #13  
      {  
        "jobDetail": {  
        "jobId": "job1",  
        "groupName": null,  
        "jobType": "MyJob",  
        "assemblyName": "ConsoleApp32",  
        "description": null,  
        "concurrent": true,  
        "includeAnnotations": false,  
        "properties": "{}",  
        "updatedTime": "2022-12-02 18:00:59.390"  
      },  
        "trigger": {  
        "triggerId": "job1_trigger1",  
        "jobId": "job1",  
        "triggerType": "Furion.Schedule.PeriodSecondsTrigger",  
        "assemblyName": "Furion",  
        "args": "[5]",  
        "description": null,  
        "status": 2,  
        "startTime": null,  
        "endTime": null,  
        "lastRunTime": "2022-12-02 18:00:59.326",  
        "nextRunTime": "2022-12-02 18:01:04.358",  
        "numberOfRuns": 1,  
        "maxNumberOfRuns": 0,  
        "numberOfErrors": 0,  
        "maxNumberOfErrors": 0,  
        "numRetries": 0,  
        "retryTimeout": 1000,  
        "startNow": true,  
        "runOnStart": false,  
        "resetOnlyOnce": true,  
        "result": null,  
        "elapsedTime": 100,  
        "updatedTime": "2022-12-02 18:00:59.390"  
      }  
      }  

  • 第二种:输出单独的作业 JSON 信息:jobDetail.ConvertToJSON()trigger.ConvertToJSON()
public class MyJob : IJob  
{  
    private readonly ILogger<MyJob> _logger;  
    public MyJob(ILogger<MyJob> logger)  
    {  
        _logger = logger;  
    }  

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

        await Task.CompletedTask;  
    }  
}  

查看作业打印结果:

info: 2022-12-02 18:02:10.7923360 +08:00 星期五 L MyJob[0] #8  
      {  
        "jobId": "job1",  
        "groupName": null,  
        "jobType": "MyJob",  
        "assemblyName": "ConsoleApp32",  
        "description": null,  
        "concurrent": true,  
        "includeAnnotations": false,  
        "properties": "{}",  
        "updatedTime": "2022-12-02 18:02:10.774"  
      }  
info: 2022-12-02 18:02:10.8008708 +08:00 星期五 L MyJob[0] #8  
      {  
        "trigger_id": "job1_trigger1",  
        "job_id": "job1",  
        "trigger_type": "Furion.Schedule.PeriodSecondsTrigger",  
        "assembly_name": "Furion",  
        "args": "[5]",  
        "description": null,  
        "status": 2,  
        "start_time": null,  
        "end_time": null,  
        "last_run_time": "2022-12-02 18:02:10.727",  
        "next_run_time": "2022-12-02 18:02:15.733",  
        "number_of_runs": 1,  
        "max_number_of_runs": 0,  
        "number_of_errors": 0,  
        "max_number_of_errors": 0,  
        "num_retries": 0,  
        "retry_timeout": 1000,  
        "start_now": true,  
        "run_on_start": false,  
        "reset_only_once": true,  
        "result": null,  
        "elapsed_time": 100,  
        "updated_time": "2022-12-02 18:02:10.774"  
      }  

  • 第三种:输出单独的作业 SQL 信息:jobDetail.ConvertToSQL()trigger.ConvertToSQL()
public class MyJob : IJob  
{  
    private readonly ILogger<MyJob> _logger;  
    public MyJob(ILogger<MyJob> logger)  
    {  
        _logger = logger;  
    }  

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

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

        await Task.CompletedTask;  
    }  
}  

查看作业打印结果:

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

  • 第四种:输出单独的作业 Monitor 信息:jobDetail.ConvertToMonitor()trigger.ConvertToMonitor()
public class MyJob : IJob  
{  
    private readonly ILogger<MyJob> _logger;  
    public MyJob(ILogger<MyJob> logger)  
    {  
        _logger = logger;  
    }  

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

        await Task.CompletedTask;  
    }  
}  

查看作业打印结果:

info: 2022-12-02 18:04:06.2833095 +08:00 星期五 L MyJob[0] #8  
      ┏━━━━━━━━━━━  JobDetail ━━━━━━━━━━━  
      ┣ MyJob  
      ┣  
      ┣ jobId:                     job1  
      ┣ groupName:  
      ┣ jobType:                   MyJob  
      ┣ assemblyName:              ConsoleApp32  
      ┣ description:  
      ┣ concurrent:                True  
      ┣ includeAnnotations:        False  
      ┣ properties:                {}  
      ┣ updatedTime:               2022-12-02 18:04:06.254  
      ┗━━━━━━━━━━━  JobDetail ━━━━━━━━━━━  
info: 2022-12-02 18:04:06.2868205 +08:00 星期五 L MyJob[0] #8  
      ┏━━━━━━━━━━━  Trigger ━━━━━━━━━━━  
      ┣ Furion.Schedule.PeriodSecondsTrigger  
      ┣  
      ┣ triggerId:                job1_trigger1  
      ┣ jobId:                    job1  
      ┣ triggerType:              Furion.Schedule.PeriodSecondsTrigger  
      ┣ assemblyName:             Furion  
      ┣ args:                     [5]  
      ┣ description:  
      ┣ status:                   Running  
      ┣ startTime:  
      ┣ endTime:  
      ┣ lastRunTime:              2022-12-02 18:04:06.189  
      ┣ nextRunTime:              2022-12-02 18:04:11.212  
      ┣ numberOfRuns:             1  
      ┣ maxNumberOfRuns:          0  
      ┣ numberOfErrors:           0  
      ┣ maxNumberOfErrors:        0  
      ┣ numRetries:               0  
      ┣ retryTimeout:             1000  
      ┣ startNow:                 True  
      ┣ runOnStart:               False  
      ┣ resetOnlyOnce:            True  
      ┣ result:  
      ┣ elapsedTime:              100  
      ┣ updatedTime:              2022-12-02 18:04:06.254  
      ┗━━━━━━━━━━━  Trigger ━━━━━━━━━━━  

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

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

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

// 也可以部分静态,部分动态注册  
services.AddSchedule(options =>  
{  
    options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));  
});  

  1. 注入 ISchedulerFactory 服务
public class YourService: IYourService  
{  
    private readonly ISchedulerFactory _schedulerFactory;  
    public YourService(ISchedulerFactory schedulerFactory)  
    {  
        _schedulerFactory = schedulerFactory;  
    }  

    public void AddJob()  
    {  
        _schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());  
    }  
}  

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

26.1.2.6 作业触发器特性

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

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

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

    // 还可以批量新增 Furion 4.8.2.4+  
    options.AddJob(App.EffectiveTypes.ScanToBuilders());  
});  

  1. MyJob 中添加多个作业触发器特性
[Minutely]  
[Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)]  
public class MyJob : IJob  
{  
    private readonly ILogger<MyJob> _logger;  
    public MyJob(ILogger<MyJob> logger)  
    {  
        _logger = logger;  
    }  

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

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

26.1.2.7 HTTP 请求作业

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

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

services.AddSchedule(options =>  
{  
      options.AddHttpJob(request =>  
      {  
            request.RequestUri = "https://www.chinadot.net";  
            request.HttpMethod = HttpMethod.Get;  
            // request.Body = "{}"; // 设置请求报文体  
            // 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+ 支持  
            // request.Timeout = 100_000; // 设置超时时间,Furion 4.9.3.13+ 支持  
            // request.PrintResponseContent = true; // 是否打印响应结果,Furion 4.9.4.4+ 支持  
      }, 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 设置作业组名称

版本说明以下内容仅限 Furion 4.9.2.9 + 版本使用。

默认情况下,若需为单个或多个作业批量设置 GroupName,以往需采用以下的方式,这在代码层面显得较为冗长

旧方式

services.AddSchedule(options =>  
{  
    // 为 group1 添加作业,并手动设置该作业组名称  
    options.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group1"), Triggers.Secondly());  
    options.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group1"), Triggers.Hourly());  

    // 为 group2 添加作业,并手动设置该作业组名称  
    options.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group2"), Triggers.Secondly());  
    options.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group2"), Triggers.Hourly());  

    // 添加未设置作业组名称的作业  
    options.AddJob<MyJob>(Triggers.Secondly());  
});  

在最新版本中,我们提供了更为简洁的方式来实现:

新方式

services.AddSchedule(options =>  
{  
    // 为 group1 添加作业,并自动应用该作业组名称  
    options.GroupSet("group1", () =>  
    {  
        options.AddJob<MyJob>(Triggers.Secondly());  
        options.AddJob<MyJob>(Triggers.Hourly());  
    });  

    // 为 group2 添加作业,并自动应用该作业组名称  
    options.GroupSet("group2", () =>  
    {  
        options.AddJob<MyJob>(Triggers.Secondly());  
        options.AddJob<MyJob>(Triggers.Hourly());  
    });  

    // 添加未设置作业组名称的作业  
    options.AddJob<MyJob>(Triggers.Secondly());  
});  

通过options.GroupSet方法,我们能够为指定作业组添加多个作业,且这些作业会自动应用该组的名称,大大简化了代码书写过程。同时,未在此方法内添加的作业将默认不设置作业组名称。这样的设计既提升了代码的可读性,也方便了作业组的管理与配置。

26.1.2.10 非 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 对象。

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

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

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

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

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

  1. 通过 JobDetail 类型创建

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

var jobBuilder = JobBuilder.From(jobDetail);  

//也可以通过以下方式  
var jobBuilder = jobDetail.GetBuilder();  

  1. 通过 JSON 字符串创建

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

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

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

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

关于属性名匹配规则支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法) 命名方式。

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

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

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

  • 不会克隆 JobIdUpdatedTime

  • 还可以通过 LoadFrom 实例方法填充当前的 JobBuilder

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

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

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

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

// 支持字典类型  
jobBuilder.LoadFrom(new Dictionary<string, object>  
{  
      {"Description", "这是新的描述" },  
      {"include_annotations", false },  
      {"updatedTime", DateTime.Now }  
});  

关于属性名匹配规则支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法)UnderScoreCase(下划线命名法) 命名方式。

26.1.3.3 设置作业信息构建器

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

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

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

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

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

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

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

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

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

        await Task.CompletedTask;  
    }  
}  

查看作业运行日志:

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

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

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

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

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

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

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

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

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

// 清空所有额外数据  
jobDetail.ClearProperties();  

// 检查额外数据键是否定义,Furion 4.9.2.32+ 版本支持  
var isDefine = jobDetail.ContainsProperty("key");  

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

26.1.3.5 作业信息特性

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

[JobDetail("job1", "这是一段描述")]  
[PeriodSeconds(5, TriggerId = "trigger1")]  
public class MyJob : IJob  
{  
}  

  • 启动 IncludeAnnotations 属性自动填充
services.AddSchedule(options =>  
{  
    options.AddJob(JobBuilder.Create<MyJob>()  
      .SetIncludeAnnotations(true));      // 此时 [JobDetail] 配置的非空属性将自动复制给 JobBuilder,[PeriodSeconds] 也会自动创建 TriggerBuilder  
});  

  • 手动扫描并创建作业计划构建器
var schedulerBuilder = typeof(MyJob).ScanToBuilder();  

  • 通过程序集类型扫描批量创建作业计划构建器

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

public Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)   // Furion 4.9.1.59 之前版本为 IEnumerable<SchedulerBuilder> Preload()  
{  
      // 扫描所有类型并创建  
      return Task.FromResult(App.EffectiveTypes.Where(t => t.IsJobType())     // Furion 4.9.1.59 之前直接返回  
                              .Select(t => t.ScanToBuilder()));  

      // 还可以更简单~~  
      return Task.FromResult(App.EffectiveTypes.ScanToBuilders());      // Furion 4.9.1.59 之前直接返回  
}  


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

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

多种用法使用如下:

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

26.1.3.6 多种格式字符串输出

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

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

字符串打印如下:

{  
  "jobId": "job1",  
  "groupName": null,  
  "jobType": "MyJob",  
  "assemblyName": "ConsoleApp13",  
  "description": null,  
  "concurrent": true,  
  "includeAnnotations": false,  
  "properties": "{}",  
  "updatedTime": "2022-12-04 11:51:00.483"  
}  

  1. 转换成 SQL 字符串
// 输出新增 SQL,使用 CamelCase 属性命名  
var insertSql = jobDetail.ConvertToSQL("tbName"  
      , PersistenceBehavior.Appended  
      , NamingConventions.CamelCase);  
// 更便捷拓展  
var insertSql = jobDetail.ConvertToInsertSQL("tbName", NamingConventions.CamelCase);  

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

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

字符串打印如下:

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

  1. 转换成 Monitor 字符串
var monitor = jobDetail.ConvertToMonitor();  

字符串打印如下:

┏━━━━━━━━━━━  JobDetail ━━━━━━━━━━━  
┣ MyJob  
┣  
┣ jobId:                     job1  
┣ groupName:  
┣ jobType:                   MyJob  
┣ assemblyName:              ConsoleApp13  
┣ description:  
┣ concurrent:                True  
┣ includeAnnotations:        False  
┣ properties:                {}  
┣ updatedTime:               2022-12-04 11:55:11.186  
┗━━━━━━━━━━━  JobDetail ━━━━━━━━━━━  

  1. 简要字符串输出
var str = jobDetail.ToString();  

字符串打印如下:

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

26.1.3.7 自定义 SQL 输出配置

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

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

      return string.Empty;  
    };  
});  

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

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

26.1.3.8 启用作业执行日志输出

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

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

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

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

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

配置启用如下:

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

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

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

作业执行日志如下:

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

26.1.4 作业处理程序 IJob

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

26.1.4.1 如何定义

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

26.1.4.2 JobExecutingContext 上下文

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

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

26.1.4.3 作业处理程序实例

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

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

var jobHandler = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, jobType);  

其中 serviceProvider 对象会在作业计划每次触发时通过 _serviceProvider.CreateScope() 提供,作业计划触发完成时释放。

由于作业处理程序默认没有注册为服务,所以无法准确的捕获其被 GC 垃圾回收器回收的时机,因此建议总是将作业处理程序注册为服务。另外,如果无需在构造函数中注入瞬时或范围作用域服务的需求,那么可以将 IJob 注册为单例服务,这样就可以避免每次重复创建 IJob 实例,对性能和减少内存占用有不小优化。 如:

// 瞬时服务,每次都会进去构造函数  
services.AddTransient<YourJob>();  

// 范围服务,每次都会进去构造函数  
services.AddScoped<YourJob>();  

// 单例服务,构造函数只会进入一次(推荐)  
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.GetServiceOrCreateInstance(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 依赖注入

默认情况下,作业处理程序可以通过构造函数注入任何生存周期的服务,如: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;  
    }  
}  

但如果作业处理程序被注册为单例服务,如:services.AddSingleton<MyJob>();,那么在构造函数中就不能直接注入瞬时或范围作用域服务,而是通过 IServiceScopeFactory 创建。

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;  
    }  
}  

  • 针对高频定时任务,比如每秒执行一次,或者更频繁的任务

这类作业建议将作业处理程序注册为单例服务,这样可以避免作业处理程序高频的创建和销毁。如:

services.AddSingleton<MyJob>();  

26.1.4.5 动态作业 DynamicJob

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

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

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

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

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

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

动态作业执行结果:

info: 2022-12-04 12:26:18.6562296 +08:00 星期日 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-04 12:26:18.6618404 +08:00 星期日 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-04 12:26:18.8727764 +08:00 星期日 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-04 12:26:18.8745765 +08:00 星期日 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-04 12:26:18.9013540 +08:00 星期日 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-04 12:26:23.8753926 +08:00 星期日 L System.Logging.DynamicJob[0] #6  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:26:23.837 -> 2022-12-04 12:26:28.835  
info: 2022-12-04 12:26:28.8686474 +08:00 星期日 L System.Logging.DynamicJob[0] #6  
      <job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:26:28.835 -> 2022-12-04 12:26:33.823  
info: 2022-12-04 12:26:33.8531796 +08:00 星期日 L System.Logging.DynamicJob[0] #13  
      <job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:26:33.823 -> 2022-12-04 12:26:38.820  

动态作业和普通作业的区别 动态作业处理程序类型是:DynamicJob 类型 * 动态作业提供的 .GetLogger() 拓展输出日志类别是:System.Logging.DynamicJob * 如果普通作业同时设置了 SetJobTypeSetDynamicExecuteAsync,那么优先作为动态作业执行。 * 动态作业无法将 Func<..> 进行序列化持久化存储*

26.1.4.6 使用 Roslyn 动态创建

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

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

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

namespace YourProject;  

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

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

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

// 生成运行时 MyJob 类型  
var jobType = jobAssembly.GetType("YourProject.MyJob");  

  1. 注册作业
// 可以在启动的时候添加  
services.AddSchedule(options =>  
{  
     options.AddJob(jobType  
            , Triggers.PeriodSeconds(5));  
});  

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

查看作业执行日志:

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

惊不惊喜,意外意外\~

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

26.1.4.7 作业执行异常处理

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

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

services.AddSchedule(options =>  
{  
    options.AddJob<MyJob>(Triggers.PeriodSeconds(3));  
});  

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

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

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

        return Task.CompletedTask;  
    }  
}  

输出日志如下:

info: 2023-04-22 22:18:04.2149071 +08:00 星期六 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2023-04-22 22:18:04.2189082 +08:00 星期六 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2023-04-22 22:18:04.3216571 +08:00 星期六 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2023-04-22 22:18:04.3230110 +08:00 星期六 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2023-04-22 22:18:04.3521056 +08:00 星期六 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2023-04-22 22:18:07.3782666 +08:00 星期六 L MyJob[0] #17  
      <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308  
fail: 2023-04-22 22:18:07.6652239 +08:00 星期六 L System.Logging.ScheduleService[0] #17  
      Error occurred executing <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308.  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.DivideByZeroException: Attempted to divide by zero.  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 29  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233  
      --- End of stack trace from previous location ---  
         at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 79  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
info: 2023-04-22 22:18:10.3507729 +08:00 星期六 L MyJob[0] #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+ 支持  
            // request.Timeout = 100_000; // 设置超时时间,Furion 4.9.3.13+ 支持  
            // request.PrintResponseContent = true; // 是否打印响应结果,Furion 4.9.4.4+ 支持  
      }, 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 配置参数,如果不能满足可自行定义。

  1. 自定义 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; }  
}  

  1. 自定义 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 = Schedular.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 = Schedular.Serialize(new  
        {  
            httpResponseMessage.StatusCode,  
            Body = bodyString  
        });  
    }  
}  

  1. 注册自定义 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.Resultnull,那么也就是本次执行未成功。

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;  
    }  
}  

ResultProperties除了通过 context.Result 设置作业本次执行结果以外,还可以通过 jobDetail.AddOrUpdateProperty(key, value) 的方式设置。区别在于前者会将值同步到 TriggerResult 中,后者会将值同步在 JobDetailProperties 中。

26.1.4.12 并发操作处理

有时候需要频繁地触发作业,并在作业处理程序中进行大量的数据库操作如批量插入,批量删除等耗时操作,因此可能会出现很多意外的异常情况,这时推荐使用 lock 进行资源锁操作。如:

public class MyJob : IJob  
{  
    static object _lockObj = new object();  

    public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)  
    {  
        lock(_lockObj)  
        {  
            // 高频耗时操作  
        }  

        return Task.CompletedTask;  
    }  
}  

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 是否立即启动,设置 false 将手动启动
RunOnStart bool false 是否启动时执行一次,可通过 StartTime 控制起始时间,若 StartTimenull 或小于当前时间,则在程序启动时执行,否则在 StartTime 时间执行。Furion 4.9.2.29+ 版本支持自定义 RunOnStart 逻辑,可查看 26.1.7.2 ScheduleOptionsBuilder 配置选项 小节。
ResetOnlyOnce bool true 是否在启动时重置最大触发次数等于一次的作业
Result string 本次执行返回结果,Furion 4.8.7.7+
ElapsedTime long 0 本次执行耗时,单位 msFurion 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 对象。

  1. 通过 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);  

  1. 通过 Trigger 类型创建

这种方式常用于在运行时更新作业触发器。

var triggerBuilder = TriggerBuilder.From(trigger);  

//也可以通过以下方式  
var triggerBuilder = trigger.GetBuilder();  

  1. 通过 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

  1. 还可以通过 Clone 静态方法从一个 TriggerBuilder 创建
var triggerBuilder = TriggerBuilder.Clone(fromTriggerBuilder);  

克隆说明克隆操作只会克隆 AssemblyNameTriggerTypeArgsDescriptionStartTimeEndTimeMaxNumberOfRunsMaxNumberOfErrorsNumRetriesRetryTimeoutStartNowRunOnStartResetOnlyOnce

不会克隆 TriggerIdJobIdStatusLastRunTimeNextRunTimeNumberOfRunsNumberOfErrorsResultElapsedTime,PersistentConnectionUpdatedTime

  1. 还可以通过 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 自定义作业触发器

除了使用框架提供的 PeriodTriggerCronTrigger 以外,还可以自定义作业触发器,只需要继承 Trigger 并重写 GetNextOccurrence 方法即可,如实现一个间隔两秒的作业触发器。

public class CustomTrigger : Trigger  
{  
    public override DateTime GetNextOccurrence(DateTime startAt)  
    {  
        return startAt.AddSeconds(2);  
    }  
}  

之后可通过 TriggerBuilder.CreateTriggers.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  

另外,自定义作业触发器还支持配置构造函数参数

参数特别说明如果自定义作业触发器包含参数,那么必须满足以下两个条件

  • 参数必须通过唯一的构造函数传入,有且最多只能拥有一个构造函数
  • 参数的类型只能是 intstringboolnull 或由它们组成的数组类型
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.CreateTriggers.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 方法之后,还提供了 ShouldRunToString 方法可重写,如:

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 触发器";  
    }  
}  

推荐重写 GetNextRunTimeToString 方法即可,如:

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:作业触发器 Idstring 类型
  • 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():标记作业触发器构建器是新增的,届时生成的 SQLINSERT 语句
  • Updated():标记作业触发器构建器已被更新,届时生成的 SQLUpdated 语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中
  • Removed():标记作业触发器构建器已被删除,届时生成的 SQLDeleted 语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至作业计划中
 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  

26.1.5.9 多种格式字符串输出

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

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

字符串打印如下:

{  
  "triggerId": "job1_trigger1",  
  "jobId": "job1",  
  "triggerType": "Furion.Schedule.PeriodTrigger",  
  "assemblyName": "Furion",  
  "args": "[5000]",  
  "description": null,  
  "status": 2,  
  "startTime": null,  
  "endTime": null,  
  "lastRunTime": "2022-12-04 17:52:34.768",  
  "nextRunTime": "2022-12-04 17:52:39.769",  
  "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 17:52:34.803"  
}  

  1. 转换成 SQL 字符串
// 输出新增 SQL,使用 CamelCase 属性命名  
var insertSql = trigger.ConvertToSQL("tbName"  
      , PersistenceBehavior.Appended  
      , NamingConventions.CamelCase);  
// 更便捷拓展  
var insertSql = trigger.ConvertToInsertSQL("tbName", NamingConventions.CamelCase);  

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

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

字符串打印如下:

-- 新增操作  
INSERT INTO tbName(  
      triggerId,  
      jobId,  
      triggerType,  
      assemblyName,  
      args,  
      description,  
      status,  
      startTime,  
      endTime,  
      lastRunTime,  
      nextRunTime,  
      numberOfRuns,  
      maxNumberOfRuns,  
      numberOfErrors,  
      maxNumberOfErrors,  
      numRetries,  
      retryTimeout,  
      startNow,  
      runOnStart,  
      resetOnlyOnce,  
      result,  
      elapsedTime,  
      updatedTime  
)  
VALUES(  
      'job1_trigger1',  
      'job1',  
      'Furion.Schedule.PeriodTrigger',  
      'Furion',  
      '[5000]',  
      NULL,  
      2,  
      NULL,  
      NULL,  
      '2022-12-04 17:54:42.693',  
      '2022-12-04 17:54:47.721',  
      1,  
      0,  
      0,  
      0,  
      0,  
      1000,  
      1,  
      0,  
      1,  
      NULL,  
      100,  
      '2022-12-04 17:54:42.754'  
);  
-- 删除操作  
DELETE FROM tbName  
WHERE TriggerId = 'job1_trigger1' AND JobId = 'job1';  
-- 更新操作  
UPDATE tbName  
SET  
      trigger_id = 'job1_trigger1',  
      job_id = 'job1',  
      trigger_type = 'Furion.Schedule.PeriodTrigger',  
      assembly_name = 'Furion',  
      args = '[5000]',  
      description = NULL,  
      status = 2,  
      start_time = NULL,  
      end_time = NULL,  
      last_run_time = '2022-12-04 17:54:42.693',  
      next_run_time = '2022-12-04 17:54:47.721',  
      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,  
      elapsedTime = 100,  
      updated_time = '2022-12-04 17:54:42.754'  
WHERE trigger_id = 'job1_trigger1' AND job_id = 'job1';  

  1. 转换成 Monitor 字符串
var monitor = trigger.ConvertToMonitor();  

字符串打印如下:

┏━━━━━━━━━━━  Trigger ━━━━━━━━━━━  
┣ Furion.Schedule.PeriodTrigger  
┣  
┣ triggerId:                job1_trigger1  
┣ jobId:                    job1  
┣ triggerType:              Furion.Schedule.PeriodTrigger  
┣ assemblyName:             Furion  
┣ args:                     [5000]  
┣ description:  
┣ status:                   Running  
┣ startTime:  
┣ endTime:  
┣ lastRunTime:              2022-12-04 17:56:55.384  
┣ nextRunTime:              2022-12-04 17:57:00.379  
┣ numberOfRuns:             1  
┣ maxNumberOfRuns:          0  
┣ numberOfErrors:           0  
┣ maxNumberOfErrors:        0  
┣ numRetries:               0  
┣ retryTimeout:             1000  
┣ startNow:                 True  
┣ runOnStart:               False  
┣ resetOnlyOnce:            True  
┣ result:  
┣ elapsedTime:              100  
┣ updatedTime:              2022-12-04 17:56:55.413  
┗━━━━━━━━━━━  Trigger ━━━━━━━━━━━  

  1. 简要字符串输出
var str = trigger.ToString();  

字符串打印如下:

<job1 job1_trigger1> 5s 这是一段描述 1ts  

26.1.5.10 自定义 SQL 输出配置

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

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

      return string.Empty;  
    };  
});  

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

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

26.1.5.11 查看最近运行记录

版本说明以下内容仅限 Furion 4.8.4.3 + 版本使用。

Furion 4.8.4.3+ 版本新增了 GetTimelines() 方法,可获取内存中作业触发器最近运行的 5 条记录,如:

var timelines = trigger.GetTimelines();   // => [{numberOfRuns: 2, lastRunTime: "2023-01-03 14:00:08"}, {numberOfRuns: 1, lastRunTime: "2023-01-03 14:00:03"}, ...]  

timelines 返回值为 IEnumerable<TriggerTimeline> 类型,其中 TriggerTimeline 类型提供以下属性:

  • TriggerTimeline
    • NumberOfRuns:当前运行次数,long 类型
    • LastRunTime:最近运行时间,DateTime? 类型
    • NextRunTime:下一次运行时间,DateTime? 类型
    • Status:作业触发器状态,TriggerStatus 枚举类型
    • Result:本次执行结果,string 类型
    • ElapsedTime:本次执行耗时,long 类型,单位 ms
    • CreatedTime:记录时间,DateTime 类型
    • TriggerId:作业触发器 Idstring 类型,Furion 4.9.1.16+ 支持
    • JobId:作业 Idstring 类型,Furion 4.9.1.16+ 支持

26.1.5.12 更改作业触发器触发时间

版本说明以下内容仅限 Furion 4.8.7.31 + 版本使用。

如果需要更改作业触发器触发时间通常需要程序员自行组合 .SetTriggerType<TTrigger>().SetArgs(args),但在实际开发中代码非常不直观,所以提供了一些列的 .AlterTo 方法,如:

// 间隔 Period 方式  
// 设置毫秒周期(间隔)作业触发器  
triggerBuilder.AlterToPeriod(5000);  
// 设置秒周期(间隔)作业触发器  
triggerBuilder.AlterToPeriodSeconds(5);  
// 设置分钟周期(间隔)作业触发器  
triggerBuilder.AlterToPeriodMinutes(5);  
// 设置小时周期(间隔)作业触发器  
triggerBuilder.AlterToPeriodHours(5);  

// Cron 表达式方式  
// 设置 Cron 表达式作业触发器  
triggerBuilder.AlterToCron("* * * * *", CronStringFormat.Default);  
// 设置每秒开始作业触发器  
triggerBuilder.AlterToSecondly();  
// 设置每分钟开始作业触发器  
triggerBuilder.AlterToMinutely();  
// 设置每小时开始作业触发器  
triggerBuilder.AlterToHourly();  
// 设置每天(午夜)开始作业触发器  
triggerBuilder.AlterToDaily();  
// 设置每月1号(午夜)开始作业触发器  
triggerBuilder.AlterToMonthly();  
// 设置每周日(午夜)开始作业触发器  
triggerBuilder.AlterToWeekly();  
// 设置每年1月1号(午夜)开始作业触发器  
triggerBuilder.AlterToYearly();  
// 设置每周一至周五(午夜)开始作业触发器  
triggerBuilder.AlterToWorkday();  

// Cron 表达式 Macro At 方式  
// 每第 3 秒  
triggerBuilder.AlterToSecondlyAt(3);  
// 每第 3,5,6 秒  
triggerBuilder.AlterToSecondlyAt(3, 5, 6);  

// 每分钟第 3 秒  
triggerBuilder.AlterToMinutelyAt(3);  
// 每分钟第 3,5,6 秒  
triggerBuilder.AlterToMinutelyAt(3, 5, 6);  

// 每小时第 3 分钟  
triggerBuilder.AlterToHourlyAt(3);  
// 每小时第 3,5,6 分钟  
triggerBuilder.AlterToHourlyAt(3, 5, 6);  

// 每天第 3 小时正(点)  
triggerBuilder.AlterToDailyAt(3);  
// 每天第 3,5,6 小时正(点)  
triggerBuilder.AlterToDailyAt(3, 5, 6);  

// 每月第 3 天零点正  
triggerBuilder.AlterToMonthlyAt(3);  
// 每月第 3,5,6 天零点正  
triggerBuilder.AlterToMonthlyAt(3, 5, 6);  

// 每周星期 3 零点正  
triggerBuilder.AlterToWeeklyAt(3);  
triggerBuilder.AlterToWeeklyAt("WED");  // SUN(星期天),MON,TUE,WED,THU,FRI,SAT  
// 每周星期 3,5,6 零点正  
triggerBuilder.AlterToWeeklyAt(3, 5, 6);  
triggerBuilder.AlterToWeeklyAt("WED", "FRI", "SAT");  
// 还支持混合  
triggerBuilder.AlterToWeeklyAt(3, "FRI", 6);  

// 每年第 3 月 1 日零点正  
triggerBuilder.AlterToYearlyAt(3);  
triggerBuilder.AlterToYearlyAt("MAR");  // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC  
// 每年第 3,5,6 月 1 日零点正  
triggerBuilder.AlterToYearlyAt(3);  
triggerBuilder.AlterToYearlyAt(3, 5, 6);  
triggerBuilder.AlterToYearlyAt("MAR", "MAY", "JUN");  
// 还支持混合  
triggerBuilder.AlterToYearlyAt(3, "MAY", 6);  

// 设置自定义作业触发器  
// 泛型方式  
trigger.AlterTo<CustomTrigger>();  
trigger.AlterTo<CustomTrigger>(30);  
// 程序集方式  
trigger.AlterTo("YourAssembly", "YourAssembly.CustomTrigger");  
trigger.AlterTo("YourAssembly", "YourAssembly.CustomTrigger", 30);  
//类型方式  
trigger.AlterTo(typeof(CustomTrigger));  
trigger.AlterTo(typeof(CustomTrigger), 30);  

26.1.6 作业计划 Scheduler 及构建器

26.1.6.1 关于作业计划

所谓的作业计划(Scheduler)是将作业信息(JobDetail),作业触发器(Trigger)和作业处理程序(IJob)关联起来,并添加到作业调度器中等待调度执行。

作业计划(Scheduler)类型对外是不公开的,但提供了对应的 IScheduler 接口进行操作。

26.1.6.2 关于作业计划构建器

作业计划 Scheduler 是框架提供运行时的内部只读类型,那么我们该如何创建或变更 Scheduler 对象呢?

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

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

  1. 通过 Create 静态方法创建
// 通过作业 Id 创建  
var schedulerBuilder = SchedulerBuilder.Create("job1");  

// 通过泛型方式  
var schedulerBuilder = SchedulerBuilder.Create<MyJob>(Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create<MyJob>(true, Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create<MyJob>("job1", Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create<MyJob>("job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely());  

// 通过类型方式  
var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), true, Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), "job1", Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), "job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely());  

// 通过委托方式  
var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, true, Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, "job1", Triggers.PeriodSeconds(5), Triggers.Minutely());  
var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, "job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely());  

// 通过 JobBuilder 和 0 或 N 个 TriggerBuilder 创建  
var schedulerBuilder = SchedulerBuilder.Create(  
            JobBuilder.Create<MyJob>()  
            , Triggers.PeriodSeconds(5), Triggers.Minutely());  

  1. 通过 IScheduler 接口创建

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

var schedulerBuilder = SchedulerBuilder.From(scheduler);  

//也可以通过以下方式  
var schedulerBuilder = scheduler.GetBuilder();  

  1. 通过 JSON 字符串创建

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

var schedulerBuilder = SchedulerBuilder.From(@"  
{  
    ""jobDetail"": {  
        ""jobId"": ""job1"",  
        ""groupName"": null,  
        ""jobType"": ""MyJob"",  
        ""assemblyName"": ""ConsoleApp32"",  
        ""description"": null,  
        ""concurrent"": true,  
        ""includeAnnotations"": false,  
        ""properties"": ""{}"",  
        ""updatedTime"": ""2022-12-04 11:51:00.483""  
    },  
    ""triggers"": [{  
        ""triggerId"": ""job1_trigger1"",  
        ""jobId"": ""job1"",  
        ""triggerType"": ""Furion.Schedule.PeriodTrigger"",  
        ""assemblyName"": ""Furion"",  
        ""args"": ""[5000]"",  
        ""description"": null,  
        ""status"": 2,  
        ""startTime"": null,  
        ""endTime"": null,  
        ""lastRunTime"": ""2022-12-04 17:52:34.768"",  
        ""nextRunTime"": ""2022-12-04 17:52:39.769"",  
        ""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 17:52:34.803""  
    }]  
}  
");  

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

var schedulerBuilder = SchedulerBuilder.From("""  
{  
      "jobDetail": {  
            "jobId": "job1",  
            "groupName": null,  
            "jobType": "MyJob",  
            "assemblyName": "ConsoleApp32",  
            "description": null,  
            "concurrent": true,  
            "includeAnnotations": false,  
            "properties": "{}",  
            "updatedTime": "2022-12-04 11:51:00.483"  
      },  
      "triggers": [{  
            "triggerId": "job1_trigger1",  
            "jobId": "job1",  
            "triggerType": "Furion.Schedule.PeriodTrigger",  
            "assemblyName": "Furion",  
            "args": "[5000]",  
            "description": null,  
            "status": 2,  
            "startTime": null,  
            "endTime": null,  
            "lastRunTime": "2022-12-04 17:52:34.768",  
            "nextRunTime": "2022-12-04 17:52:39.769",  
            "numberOfRuns": 1,  
            "maxNumberOfRuns": 0,  
            "numberOfErrors": 0,  
            "maxNumberOfErrors": 0,  
            "numRetries": 0,  
            "retryTimeout": 1000,  
            "startNow": true,  
            "runOnStart": false,  
            "resetOnlyOnce": true,  
            "result": null,  
            "elapsedTime": 10,  
            "updatedTime": "2022-12-04 17:52:34.803"  
      }]  
}  
""");  

关于属性名匹配规则支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法) 命名方式。

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

  1. 还可以通过 Clone 静态方法从一个 SchedulerBuilder 创建
var schedulerBuilder = SchedulerBuilder.Clone(fromSchedulerBuilder);  

克隆说明克隆操作将克隆 JobBuilderTriggerBuilders,同时持久化行为会被标记为 Appended

26.1.6.3 设置作业计划构建器

SchedulerBuilder 提供了多个方法操作 JobBuilderTriggerBuilder,如:

// 获取作业信息构建器  
var jobBuilder = schedulerBuilder.GetJobBuilder();  

// 获取所有作业触发器构建器  
var triggerBuilders = schedulerBuilder.GetTriggerBuilders();  

// 获取单个作业触发器构建器  
var triggerBuilder = schedulerBuilder.GetTriggerBuilder("job1_trigger1");  
var triggerBuilder = schedulerBuilder.GetTriggerBuilder("not_found_trigger_id"); // => null  

// 更新作业信息构建器  
schedulerBuilder.UpdateJobBuilder(jobBuilder);  
// 如果通过 .GetJobBuilder() 方式获取,那么可直接更新,无需调用 .UpdateJobBuilder(jobBuilder);  
schedulerBuilder.UpdateJobBuilder(newJobBuilder, replace: true);  

// 添加作业触发器构建器  
schedulerBuilder.AddTriggerBuilder(triggerBuilder1, triggerBuilder2, ...);  

// 更新作业触发器构建器  
schedulerBuilder.UpdateTriggerBuilder(triggerBuilder1, triggerBuilder2, ...);  
// 还可以选择覆盖更新还是不覆盖  
schedulerBuilder.UpdateTriggerBuilder(new[] { triggerBuilder1, triggerBuilder2, ... }, replace: true);  

// 删除作业触发器构建器,注意不是真的删除,而是标记为 Removed 删除状态  
schedulerBuilder.RemoveTriggerBuilder("trigger1", "trigger2", ...);  

// 清除所有作业触发器构建器,注意不是真的删除,而是标记为 Removed 删除状态  
schedulerBuilder.ClearTriggerBuilders();  

// 输出为 JSON 格式  
var json = schedulerBuilder.ConvertToJSON();  
var json = schedulerBuilder.ConvertToJSON(NamingConventions.CamelCase);  

// 将作业计划构建器转换成可枚举的 Dictionary<JobBuilder, TriggerBuilder>  
foreach(var (jobBuilder, triggerBuilder) in schedulerBuilder.GetEnumerable())  
{  
      // ....  
}  

26.1.6.4 作业计划构建器持久化方法

作业计划构建器 SchedulerBuilder 提供了三个标记作业持久化行为的方法:

  • Appended():标记作业计划构建器是新增的,届时生成的 SQLINSERT 语句
  • Updated():标记作业计划构建器已被更新,届时生成的 SQLUpdated 语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中
  • Removed():标记作业计划构建器已被删除,届时生成的 SQLDeleted 语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至调度器中
services.AddSchedule(options =>  
{  
    options.AddJob(SchedulerBuilder.Create<MyJob>("job1").Appended()  
        , SchedulerBuilder.Create<MyJob>("job2").Updated()  
        , SchedulerBuilder.Create<MyJob>("job3").Removed());  
});  

查看作业调度器初始化日志:

info: 2022-12-05 12:14:42.8481157 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-05 12:14:42.8597028 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-05 12:14:42.9360896 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-05 12:14:42.9471072 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job2> successfully appended and updated to the schedule.  
info: 2022-12-05 12:14:42.9562673 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job3> successfully removed to the schedule.  
warn: 2022-12-05 12:14:42.9748930 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <2> schedulers are appended.  

26.1.6.5 多种格式字符串输出

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

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

字符串打印如下:

{  
  "jobDetail": {  
    "jobId": "job1",  
    "groupName": null,  
    "jobType": "MyJob",  
    "assemblyName": "ConsoleApp32",  
    "description": null,  
    "concurrent": true,  
    "includeAnnotations": false,  
    "properties": "{}",  
    "updatedTime": "2022-12-04 11:51:00.483"  
  },  
  "triggers": [  
    {  
      "triggerId": "job1_trigger1",  
      "jobId": "job1",  
      "triggerType": "Furion.Schedule.PeriodTrigger",  
      "assemblyName": "Furion",  
      "args": "[5000]",  
      "description": null,  
      "status": 2,  
      "startTime": null,  
      "endTime": null,  
      "lastRunTime": "2022-12-04 17:52:34.768",  
      "nextRunTime": "2022-12-04 17:52:39.769",  
      "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 17:52:34.803"  
    }  
  ]  
}  

26.1.7 作业调度器 ScheduleOptionsBuilder 配置选项

26.1.7.1 关于 ScheduleOptionsBuilder

ScheduleOptionsBuilder 配置选项主要是用来初始化作业调度器及相关服务配置的。只作为 services.AddSchedule 服务注册的配置参数,如:

// 通过委托的方式配置  
services.AddSchedule(options =>  
{  
      // options 类型为 ScheduleOptionsBuilder  
});  

// 自行创建对象实例方式配置  
var scheduleOptionsBuilder = new ScheduleOptionsBuilder();  
services.AddSchedule(scheduleOptionsBuilder);  

26.1.7.2 ScheduleOptionsBuilder 内置属性和方法

  • 内置属性配置
services.AddSchedule(options =>  
{  
      // 是否使用 UTC 时间,该配置主要用来作为作业调度器检查时间格式的依据  
      options.UseUtcTimestamp = false;  

      // 是否输出作业调度器日志  
      options.LogEnabled = true;  

      // 配置集群 Id,默认值为启动程序集的名称  
      options.ClusterId = "cluster1";  

      // 配置输出 SQL 的数据库类型,Furion 4.8.2.3+  
      options.BuildSqlType = SqlTypes.SqlServer;  

      // 配置作业信息 JobDetail 相关配置,如配置自定义 SQL 输出  
      options.JobDetail.ConvertToSQL((tableName, columnNames, jobDetail, behavior, naming) =>  
      {  
      });  

      // 启用作业执行日志输出,Furion 4.8.3.7+ 版本支持  
      options.JobDetail.LogEnabled = true;      // 默认 false  

      // 配置作业触发器 Trigger 相关配置,如配置自定义 SQL 输出  
      options.Trigger.ConvertToSQL((tableName, columnNames, trigger, behavior, naming) =>  
      {  
      });  

      // 定义未捕获的异常,通常是 Task 异常  
      options.UnobservedTaskExceptionHandler = (obj, args) =>  
      {  
      };  

      // 自定义作业触发器 RunOnStart 处理逻辑 https://gitee.com/dotnetchina/Furion/commit/c9e0e3eb0bde08cb6e12a4ae0cbf834f70e44768#note_26877003  
      options.RunOnStartProvider = (trigger, checkTime) =>  
      {  
            // 如果用户未设置 StartTime,则返回一个比当前时间早一秒的时间  
            // 如果设置了 StartTime,则返回 StartTime 的下一个发生时间  
            return (trigger.StartTime == null)  
              ? checkTime.AddSeconds(-1)  
              : trigger.GetNextOccurrence(checkTime);  
      };  
});  

  • 内置方法配置
services.AddSchedule(options =>  
{  
      // 添加作业  
      options.AddJob(schedulerBuilder);  
      options.AddJob(schedulerBuilder, schedulerBuilder1, ...); // Furion 4.8.2.4+  
      options.AddJob(jobBuilder, triggerBuilder, ...);  
      options.AddJob<MyJob>(triggerBuilder, ...);  
      options.AddJob<MyJob>("作业 Id", triggerBuilder, ...);  
      options.AddJob<MyJob>("作业 Id", concurrent: true, triggerBuilder, ...);  
      options.AddJob<MyJob>(concurrent: true, triggerBuilder, ...);  
      options.AddJob(typeof(MyJob), triggerBuilder, ...);  
      options.AddJob(typeof(MyJob), "作业 Id", triggerBuilder, ...);  
      options.AddJob(typeof(MyJob), "作业 Id", concurrent: true, triggerBuilder, ...);  
      options.AddJob(typeof(MyJob), concurrent: true, triggerBuilder, ...);  
      options.AddJob((context, stoppingToken) => {}, triggerBuilder, ...);  
      options.AddJob((context, stoppingToken) => {}, "作业 Id", triggerBuilder, ...);  
      options.AddJob((context, stoppingToken) => {}, "作业 Id", concurrent: true, triggerBuilder, ...);  
      options.AddJob((context, stoppingToken) => {}, concurrent: true, triggerBuilder, ...);  

      // 添加 HTTP Job,Furion 4.8.7.7+  
      options.AddHttpJob(request => {}, triggerBuilder, ...);  
      options.AddHttpJob(request => {}, "作业 Id", triggerBuilder, ...);  
      options.AddHttpJob(request => {}, "作业 Id", concurrent: true, triggerBuilder, ...);  
      options.AddHttpJob(request => {}, concurrent: true, triggerBuilder, ...);  
      options.AddHttpJob<YourHttpJob>(request => {}, triggerBuilder, ...);  
      options.AddHttpJob<YourHttpJob>(request => {}, "作业 Id", triggerBuilder, ...);  
      options.AddHttpJob<YourHttpJob>(request => {}, "作业 Id", concurrent: true, triggerBuilder, ...);  
      options.AddHttpJob<YourHttpJob>(request => {}, concurrent: true, triggerBuilder, ...);  

      // 添加作业执行监视器  
      options.AddMonitor<YourJobMonitor>();  

      // 添加作业执行器  
      options.AddExecutor<YourJobMonitor>();  

      // 添加作业持久化器  
      options.AddPersistence<YourJobPersistence>();  

      // 注册作业集群服务  
      options.AddClusterServer<YourClusterServer>();  
});  

26.1.8 作业监视器 IJobMonitor

调度作业服务提供了 IJobMonitor 监视器接口,实现该接口可以监视所有作业处理程序执行事件,包括 执行之前、执行之后,执行异常

通过作业监视器可以实现作业完整生命周期控制,还能实现作业执行异常发送短信或邮件通知管理员或项目维护者。

如添加 YourJobMonitor

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

    public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken)  
    {  
        _logger.LogInformation("执行之前:{context}", context);  
        return Task.CompletedTask;  
    }  

    public Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken)  
    {  
        _logger.LogInformation("执行之后:{context}", context);  

        if (context.Exception != null)  
        {  
            _logger.LogError(context.Exception, "执行出错啦:{context}", context);  
        }  

        return Task.CompletedTask;  
    }  
}  

最后,在注册 Schedule 服务中注册 YourJobMonitor

services.AddSchedule(options =>  
{  
    // 添加作业执行监视器  
    options.AddMonitor<YourJobMonitor>();  
});  

执行结果如下:

info: 2022-12-05 14:09:47.2337395 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-05 14:09:47.2401561 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-05 14:09:47.2780446 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-05 14:09:47.2810119 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-05 14:09:47.2941716 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-05 14:09:52.3190129 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行之前:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260  
info: 2022-12-05 14:09:52.3240208 +08:00 星期一 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260  
fail: 2022-12-05 14:09:52.5253398 +08:00 星期一 L System.Logging.ScheduleService[0] #4  
      Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260.  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.Exception: 模拟出错  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220  
      --- 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 87  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
info: 2022-12-05 14:09:52.5288429 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行之后:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260  
fail: 2022-12-05 14:09:52.5318526 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行出错啦:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.InvalidOperationException: Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260.  
       ---> System.Exception: 模拟出错  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220  
      --- 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 87  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218  
         --- End of inner exception stack trace ---  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  

还可以设置执行失败重试,如:

services.AddSchedule(options =>  
{  
      options.AddJob<MyJob>(Triggers.PeriodSeconds(5).SetNumRetries(3));      // 重试 3 次  
      options.AddMonitor<YourJobMonitor>();  
});  

执行结果如下:

info: 2022-12-05 14:25:15.9316915 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-05 14:25:15.9391765 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-05 14:25:15.9737767 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-05 14:25:15.9754882 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-05 14:25:15.9892059 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-05 14:25:21.0056685 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行之前:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
info: 2022-12-05 14:25:21.0140485 +08:00 星期一 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
warn: 2022-12-05 14:25:21.0754973 +08:00 星期一 L System.Logging.ScheduleService[0] #4  
      Retrying 1/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
info: 2022-12-05 14:25:22.0935914 +08:00 星期一 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
warn: 2022-12-05 14:25:22.1574937 +08:00 星期一 L System.Logging.ScheduleService[0] #4  
      Retrying 2/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
info: 2022-12-05 14:25:23.1666732 +08:00 星期一 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
warn: 2022-12-05 14:25:23.2213212 +08:00 星期一 L System.Logging.ScheduleService[0] #4  
      Retrying 3/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
info: 2022-12-05 14:25:24.2337356 +08:00 星期一 L MyJob[0] #4  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
fail: 2022-12-05 14:25:24.3832385 +08:00 星期一 L System.Logging.ScheduleService[0] #4  
      Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949.  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.Exception: 模拟出错  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220  
      --- 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 99  
         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 110  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
info: 2022-12-05 14:25:24.3857991 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行之后:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
fail: 2022-12-05 14:25:24.3888126 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4  
      执行出错啦:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.InvalidOperationException: Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949.  
       ---> System.Exception: 模拟出错  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220  
      --- 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 99  
         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 110  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218  
         --- End of inner exception stack trace ---  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  

26.1.8.1 关于参数 JobExecutionContext

IJobMonitor 提供的 OnExecutingAsyncOnExecutedAsync 接口方法都包含一个 context 参数,前者是 JobExecutingContext,后者是 JobExecutedContext,它们都有一个共同的基类 JobExecutionContext

JobExecutionContext 提供了以下公共属性和公共方法:

  • JobExecutionContext 属性列表
    • JobId:作业 Id
    • TriggerId:当前触发器 Id
    • JobDetail:作业信息
    • Trigger:作业触发器
    • OccurrenceTime作业计划触发时间,最准确的记录时间
    • RunId:本次作业执行唯一 IdFurion 4.8.5.1+ 提供
    • Result:设置/读取本次作业执行结果,Furion 4.8.7.7+ 提供
    • ServiceProvider:服务提供器,Furion 4.8.7.10+ 提供
    • Mode:触发方式,默认为 定时,可选 手动Furion 4.9.3.1+ 提供
  • JobExecutionContext 方法列表

    • .ConvertToJSON(naming):将作业计划转换成 JSON 字符串
    • .ToString():将作业执行信息输出为简要字符串
  • JobExecutingContext 在基类基础上拓展了 ExecutingTime 属性:

    • ExecutingTime:执行前时间
  • JobExecutedContext 则在基类基础上拓展了 ExecutedTimeException 属性:
    • ExecutedTime:执行后时间
    • Exception:执行异常

26.1.9 作业执行器 IJobExecutor

调度作业服务提供了 IJobExecutor 执行器接口,可以让开发者自定义作业处理函数执行策略,如 超时控制,失败重试等等

26.1.9.1 实现重试策略

如添加 YourJobExecutor

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

    public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken)  
    {  
        // 实现失败重试策略,如失败重试 3 次  
        await Retry.InvokeAsync(async () =>  
        {  
            await jobHandler.ExecuteAsync(context, stoppingToken);  
        }, 3, 1000  
        // 每次重试输出日志  
        , retryAction: (total, times) =>  
        {  
            _logger.LogWarning("Retrying {current}/{times} times for {context}", times, total, context);  
        });  
    }  
}  

接着模拟 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}");  

        throw new Exception("模拟出错");  
        return Task.CompletedTask;  
    }  
}  

最后,在注册 Schedule 服务中注册 YourJobExecutor

services.AddSchedule(options =>  
{  
      // 添加作业执行器  
      options.AddExecutor<YourJobExecutor>();  
});  

执行结果如下:

info: 2022-12-05 14:36:41.2085688 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-05 14:36:41.2162510 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-05 14:36:41.2885816 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-05 14:36:41.2912130 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-05 14:36:41.3102057 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-05 14:36:46.3329097 +08:00 星期一 L MyJob[0] #13  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
warn: 2022-12-05 14:36:46.3910063 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13  
      Retrying 1/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
info: 2022-12-05 14:36:47.4014898 +08:00 星期一 L MyJob[0] #13  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
warn: 2022-12-05 14:36:47.4471172 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13  
      Retrying 2/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
info: 2022-12-05 14:36:48.4539737 +08:00 星期一 L MyJob[0] #13  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
warn: 2022-12-05 14:36:48.4880918 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13  
      Retrying 3/3 times for <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
info: 2022-12-05 14:36:49.4984333 +08:00 星期一 L MyJob[0] #13  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274  
fail: 2022-12-05 14:36:49.6714485 +08:00 星期一 L System.Logging.ScheduleService[0] #13  
      Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274.  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.Exception: 模拟出错  
         at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 31  
         at ConsoleApp32.YourJobExecutor.<>c__DisplayClass2_0.<<ExecuteAsync>b__0>d.MoveNext() in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\YourJobExecutor.cs:line 20  
      --- 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 99  
         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 110  
         at ConsoleApp32.YourJobExecutor.ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\YourJobExecutor.cs:line 18  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 232  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  

26.1.9.2 实现超时控制

如添加 YourJobExecutor

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

    public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken)  
    {  
        await jobHandler.ExecuteAsync(context, stoppingToken)  
            .WaitAsync(TimeSpan.FromMilliseconds(3000));    // 设置 3 秒超时  
    }  
}  

接着模拟 MyJob 执行超时:

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.Delay(6000);     // 模拟耗时 6 秒  
    }  
}  

执行结果如下:

info: 2022-12-20 13:57:01.7251541 +08:00 星期二 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-20 13:57:01.7336016 +08:00 星期二 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is preloading...  
info: 2022-12-20 13:57:02.2846096 +08:00 星期二 L System.Logging.ScheduleService[0] #1  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-20 13:57:02.3448819 +08:00 星期二 L System.Logging.ScheduleService[0] #1  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-20 13:57:02.3800053 +08:00 星期二 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-20 13:57:07.3261111 +08:00 星期二 L MyJob[0] #14  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-20 13:57:07.240 -> 2022-12-20 13:57:12.260  
fail: 2022-12-20 13:57:10.5743871 +08:00 星期二 L System.Logging.ScheduleService[0] #14  
      Error occurred executing <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-20 13:57:07.240 -> 2022-12-20 13:57:12.260.  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
      System.TimeoutException: The operation has timed out.  
         at YourJobExecutor.ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 41  
         at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 234  
      ++++++++++++++++++++++++++++++++++++++++++++++++++++++++  

关于 WaitAsync 说明WaitAsync.NET6+ 新增的 Task 拓展方法,如需在 .NET5 中支持,可添加以下拓展:

public static async Task WaitAsync(this Task task, TimeSpan timeout)  
{  
      using var timeoutCancellationTokenSource = new CancellationTokenSource();  
      var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token);  

      if(await Task.WhenAny(task, delayTask) == task)  
      {  
            timeoutCancellationTokenSource.Cancel();  
            await task;  
      }  
      else  
      {  
            throw new TimeoutException("The operation has timed out.")  
      }  
}  

26.1.9.3 更多控制

作业执行器功能远不止于此,通过自定义作业执行器还可以实现分片作业,关联子作业,故障转移,集群等控制。

26.1.10 作业计划工厂 ISchedulerFactory

作业计划工厂提供了程序运行时操作作业调度器,作业计划等诸多方法。

ISchedulerFactory 被注册为 单例 服务,允许在任何可依赖注入的服务获取,如:

public class YourService: IYourService  
{  
    private readonly ISchedulerFactory _schedulerFactory;  
    public YourService(ISchedulerFactory schedulerFactory)  
    {  
        _schedulerFactory = schedulerFactory;  

        // 也可以通过 App.GetService<ISchedulerFactory>() 获取  
    }  

    public void SomeMethod([FromServices]ISchedulerFactory schedulerFactory)  
    {  
    }  

    // .NET7+ 或 Furion 4.8.0+  
    public void SomeMethod(ISchedulerFactory schedulerFactory)  
    {  
    }  
}  

26.1.10.1 查找所有作业

// 查找所有作业,包括 JobType == null 的非有效作业  
var jobs = _schedulerFactory.GetJobs();  
var jobsOfModels = _schedulerFactory.GetJobsOfModels();  

// 查找特定分组的作业,包括 JobType == null 的非有效作业  
var jobs = _schedulerFactory.GetJobs("group1");  
var jobsOfModels = _schedulerFactory.GetJobsOfModels("group1");  

// 查找所有作业,仅 JobType != null 有效作业  
var jobs = _schedulerFactory.GetJobs(active: true);  
var jobsOfModels = _schedulerFactory.GetJobsOfModels(active: true);  

// 查找特定分组的作业,仅 JobType != null 有效作业  
var jobs = _schedulerFactory.GetJobs("group1", true);  
var jobsOfModels = _schedulerFactory.GetJobsOfModels("group1", true);  

26.1.10.2 查找下一批触发的作业

// 查找下一批触发的作业  
var nextRunJobs = _schedulerFactory.GetNextRunJobs(DateTime.Now);  
var nextRunJobsOfModels = _schedulerFactory.GetNextRunJobsOfModels(DateTime.Now);  

// 查找特定分组下一批触发的作业  
var nextRunJobs = _schedulerFactory.GetNextRunJobs(DateTime.Now, "group1");  
var nextRunJobsOfModels = _schedulerFactory.GetNextRunJobsOfModels(DateTime.Now, "group1");  

26.1.10.3 获取单个作业

// 返回 ScheduleResult 类型  
var scheduleResult = _schedulerFactory.TryGetJob("job1", out var scheduler); // 如果存在返回 => ScheduleResult.Succeed  
var scheduleResult = _schedulerFactory.TryGetJob("not_found", out var scheduler); // => ScheduleResult.NotFound  
var scheduleResult = _schedulerFactory.TryGetJob("", out var scheduler); // => ScheduleResult.NotIdentify  

// 返回 IScheduler 类型  
var scheduler = _schedulerFactory.GetJob("job1"); // 如果存在返回 IScheduler  
var scheduler = _schedulerFactory.GetJob("not_found"); // => null  
var scheduler = _schedulerFactory.GetJob(""); // => null  

26.1.10.4 保存作业

保存作业是框架提供强大且简单的方法,支持 新增编辑删除 作业,也就是三大操作都可以直接通过此方法直接操作。

// 返回 ScheduleResult 类型  
var scheduleResult = _schedulerFactory.TrySaveJob(schedulerBuilder, out var scheduler);  

// 无返回值,支持多个  
_schedulerFactory.SaveJob(schedulerBuilder1, schedulerBuilder2, ...)  

关于保存作业的背后行为默认情况下,保存作业需要传递 SchedulerBuilder 对象,这个对象可通过 GetJob(jobId) 获取,如:

var schedulerBuilder = _schedulerFactory.GetJob("jobId")?.GetBuilder();  

此时它的内部 Behavior 属性被标记为 PersistenceBehavior.Updated,也就是更新状态,那么对于这个构建器的任何操作都会标记为 更新 操作。

如果通过 .Appended().Removed() 方法标记之后,那么它的操作行为就发生变化了。

  • 如果被标记为 .Appended(),那么它将进行 新增 操作。如:
schedulerBuilder.Appended();  

  • 如果被标记为 .Removed(),那么它将进行 删除 操作。如:
schedulerBuilder.Removed();  

比如以下的代码实则是 新增删除更新 操作:

// 实际做新增操作  
var scheduleResult = _schedulerFactory.TrySaveJob(SchedulerBuilder.Create<MyJob>(), out var scheduler); // Create 方法默认标记为 Appended  

// 实际做删除操作  
var schedulerBuilder = _schedulerFactory.GetJob("jobId")?.GetBuilder();  
var scheduleResult = _schedulerFactory.TrySaveJob(schedulerBuilder?.Removed(), out var scheduler); // 标记为 Removed  

// 实际做更新操作  
var scheduleResult = _schedulerFactory.TrySaveJob(SchedulerBuilder.Create<MyJob>().Updated(), out var scheduler); // Create 方法默认标记为 Appended,但调用 Updated() 方法  

另外,作业触发器 Trigger 也具备相同的行为。

26.1.10.5 添加作业

框架提供了非常多的重载方法添加作业,如:

// SchedulerBuilder 方式  
var scheduleResult = _schedulerFactory.TryAddJob(schedulerBuilder, out var scheduler);  
_schedulerFactory.AddJob(schedulerBuilder1, schedulerBuilder2, ...);  

// JobBuilder + TriggerBuilders 方式  
var scheduleResult = _schedulerFactory.TryAddJob(jobBuilder, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.TryAddJob(jobBuilder, triggerBuilder1, triggerBuilder2, ...);  

// 泛型方式  
var scheduleResult = _schedulerFactory.TryAddJob<MyJob>(new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob<MyJob>(triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id  
var scheduleResult = _schedulerFactory.TryAddJob<MyJob>("job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob<MyJob>("job1", triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id + 串行/并行  
var scheduleResult = _schedulerFactory.TryAddJob<MyJob>("job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob<MyJob>("job1", true, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置 串行/ 并行  
var scheduleResult = _schedulerFactory.TryAddJob<MyJob>(true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob<MyJob>(true, triggerBuilder1, triggerBuilder2, ...);  

// 类型方式  
var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob(typeof(MyJob), triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id  
var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob(typeof(MyJob), "job1", triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id + 串行/并行  
var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob(typeof(MyJob), "job1", true, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置 串行/ 并行  
var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob(typeof(MyJob), true, triggerBuilder1, triggerBuilder2, ...);  

// 动态作业委托方式  
var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob((context, stoppingToken) => { }, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id  
var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob((context, stoppingToken) => { }, "job1", triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id + 串行/并行  
var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob((context, stoppingToken) => { }, "job1", true, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置 串行/ 并行  
var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddJob((context, stoppingToken) => { }, true, triggerBuilder1, triggerBuilder2, ...);  

// HTTP 作业,Furion 4.8.7.7+  
// 泛型方式  
var scheduleResult = _schedulerFactory.TryAddHttpJob<MyHttpJob>(request => {}, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob<MyHttpJob>(request => {}, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id  
var scheduleResult = _schedulerFactory.TryAddHttpJob<MyHttpJob>(request => {}, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob<MyHttpJob>(request => {}, "job1", triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id + 串行/并行  
var scheduleResult = _schedulerFactory.TryAddHttpJob<MyHttpJob>(request => {}, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob<MyHttpJob>(request => {}, "job1", true, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置 串行/ 并行  
var scheduleResult = _schedulerFactory.TryAddHttpJob<MyHttpJob>(request => {}, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob<MyHttpJob>(true, triggerBuilder1, triggerBuilder2, ...);  

// 默认方式  
var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob(request => {}, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id  
var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob(request => {}, "job1", triggerBuilder1, triggerBuilder2, ...);  
// 支持配置作业 Id + 串行/并行  
var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob(request => {}, "job1", true, triggerBuilder1, triggerBuilder2, ...);  
// 支持配置 串行/ 并行  
var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler);  
_schedulerFactory.AddHttpJob(request => {}, true, triggerBuilder1, triggerBuilder2, ...);  

26.1.10.6 更新作业

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryUpdateJob(schedulerBuilder, out var scheduler);  

// 无返回值方式  
_schedulerFactory.UpdateJob(schedulerBuilder1, schedulerBuilder2, ...);  

26.1.10.7 删除作业

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryRemoveJob("job1", out var scheduler);  
var scheduleResult = _schedulerFactory.TryRemoveJob(scheduler);  

// 无返回值方式  
_schedulerFactory.RemoveJob("job1", "job2", ...);  
_schedulerFactory.RemoveJob(scheduler1, scheduler2, ...);  

26.1.10.8 检查作业是否存在

var isExist = _schedulerFactory.ContainsJob("job1");  

// 还可以通过 group 查找  
var isExist = _schedulerFactory.ContainsJob("job1", "group1");  

26.1.10.9 启动所有作业

_schedulerFactory.StartAll();  

// 还可以通过 group 启动  
_schedulerFactory.StartAll("group1");  

26.1.10.10 暂停所有作业

 _schedulerFactory.PauseAll();  

// 还可以通过 group 操作  
 _schedulerFactory.PauseAll("group1");  

26.1.10.11 删除所有作业

 _schedulerFactory.RemoveAll();  

// 还可以通过 group 操作  
 _schedulerFactory.RemoveAll("group1");  

26.1.10.12 强制触发所有作业持久化操作

 _schedulerFactory.PersistAll();  

// 还可以通过 group 操作  
 _schedulerFactory.PersistAll("group1");  

26.1.10.13 校对所有作业

 _schedulerFactory.CollateAll();  

// 还可以通过 group 操作  
 _schedulerFactory.CollateAll("group1");  

26.1.10.14 强制唤醒作业调度器

正常情况下,作业调度器会自动管理 CPU 休眠和唤醒,但一些特殊情况下需要强制唤醒作业调度器(比如调度器假死了,被回收了...),可通过以下方式:

_schedulerFactory.CancelSleep();  

26.1.10.15 立即执行作业

版本说明以下内容仅限 Furion 4.8.7.11 + 版本使用。

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryRunJob("job1", out var scheduler);  
var scheduleResult = _schedulerFactory.TryRunJob(scheduler);  
var scheduleResult = _schedulerFactory.TryRunJob("job1", out var scheduler, "triggerId"); // Furion 4.9.3+ 支持设置作业触发器 Id  
var scheduleResult = _schedulerFactory.TryRunJob(scheduler, "triggerId");     // Furion 4.9.3+ 支持设置作业触发器 Id  

// 无返回值方式  
_schedulerFactory.RunJob("job1", "job2", ...);  
_schedulerFactory.RunJob(scheduler1, scheduler2, ...);  

注意事项如果作业本身处于 (Pause)暂停(NotStart)初始化时未启动(Unoccupied)无触发时间 状态,那么点击 立即执行 后将自动转至 就绪 状态。

26.1.10.16 取消正在执行作业

版本说明以下内容仅限 Furion 4.9.1.9 + 版本使用。

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryCancelJob("job1", out var scheduler);  
var scheduleResult = _schedulerFactory.TryCancelJob(scheduler);  
var scheduleResult = _schedulerFactory.TryCancelJob("job1", out var scheduler, "triggerId");    // Furion 4.9.2.38+ 支持设置作业触发器 Id  
var scheduleResult = _schedulerFactory.TryCancelJob(scheduler, "triggerId");    // Furion 4.9.2.38+ 支持设置作业触发器 Id  

// 无返回值方式  
_schedulerFactory.CancelJob("job1", "job2", ...);  
_schedulerFactory.CancelJob(scheduler1, scheduler2, ...);  

注意事项如果作业处理程序任务 Task 订阅了 stoppingToken 且正在执行,那么将被取消同时抛出 TaskCanceledException 异常。

26.1.10.17 启动作业

版本说明以下内容仅限 Furion 4.9.2.16 + 版本使用。

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryStartJob("job1", out var scheduler);  
var scheduleResult = _schedulerFactory.TryStartJob(scheduler);  

// 无返回值方式  
_schedulerFactory.StartJob("job1", "job2", ...);  
_schedulerFactory.StartJob(scheduler1, scheduler2, ...);  

26.1.10.18 暂停作业

版本说明以下内容仅限 Furion 4.9.2.16 + 版本使用。

// 返回 ScheduleResult 方式  
var scheduleResult = _schedulerFactory.TryPauseJob("job1", out var scheduler);  
var scheduleResult = _schedulerFactory.TryPauseJob(scheduler);  

// 无返回值方式  
_schedulerFactory.PauseJob("job1", "job2", ...);  
_schedulerFactory.PauseJob(scheduler1, scheduler2, ...);  

26.1.10.19 作业信息变更事件

有时候我们需要监听作业调度器中作业是否有变化,这时可通过 OnChanged 事件进行监听:

schedulerFactory.OnChanged += (sender, args) =>  
{  
      var jobDetail = args.JobDetail;  
}  

26.1.10.20 作业触发记录通知

版本说明以下内容仅限 Furion 4.9.1.16 + 版本使用。

有时候需要监听作业运行记录进行额外操作,可通过以下方式实现:

schedulerFactory.OnExecutionRecord += (sender, args) =>  
{  
      var timeline = args.Context.Timeline;     // Furion 4.9.4.2+ 支持  
      // var timeline = args.Timeline;     // Furion 4.9.4.2 之前版本支持  
}  

26.1.10.21 触发 GC 回收器回收

版本说明以下内容仅限 Furion 4.9.1.23 + 版本使用。

schedulerFactory.GCCollect();  

26.1.10.22 设置作业组名称

版本说明以下内容仅限 Furion 4.9.2.9 + 版本使用。

默认情况下,若需为单个或多个作业批量设置 GroupName,以往需采用以下的方式,这在代码层面显得较为冗长

旧方式

// 为 group1 添加作业,并手动设置该作业组名称  
schedulerFactory.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group1"), Triggers.Secondly());  
schedulerFactory.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group1"), Triggers.Hourly());  

// 为 group2 添加作业,并手动设置该作业组名称  
schedulerFactory.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group2"), Triggers.Secondly());  
schedulerFactory.AddJob(JobBuilder.Create<MyJob>().SetGroupName("group2"), Triggers.Hourly());  

// 添加未设置作业组名称的作业  
schedulerFactory.AddJob<MyJob>(Triggers.Secondly());  

在最新版本中,我们提供了更为简洁的方式来实现:

新方式

// 为 group1 添加作业,并自动应用该作业组名称  
schedulerFactory.GroupSet("group1", () =>  
{  
      schedulerFactory.AddJob<MyJob>(Triggers.Secondly());  
      schedulerFactory.AddJob<MyJob>(Triggers.Hourly());  
});  

// 为 group2 添加作业,并自动应用该作业组名称  
schedulerFactory.GroupSet("group2", () =>  
{  
      schedulerFactory.AddJob<MyJob>(Triggers.Secondly());  
      schedulerFactory.AddJob<MyJob>(Triggers.Hourly());  
});  

// 添加未设置作业组名称的作业  
schedulerFactory.AddJob<MyJob>(Triggers.Secondly());  

通过schedulerFactory.GroupSet方法,我们能够为指定作业组添加多个作业,且这些作业会自动应用该组的名称,大大简化了代码书写过程。同时,未在此方法内添加的作业将默认不设置作业组名称。这样的设计既提升了代码的可读性,也方便了作业组的管理与配置。

26.1.11 作业计划 IScheduler

作业计划 Scheduler 的默认实现接口是 IScheduler,该接口主要用来操作当前(单个)作业。

26.1.11.1 获取 SchedulerModel 实例

获取 SchedulerModel 之后可直接访问 JobDetailTrigger 对象。

var schedulerModel = scheduler.GetModel();  

26.1.11.2 获取 SchedulerBuilder

var schedulerBuilder = scheduler.GetBuilder();  

26.1.11.3 获取 JobBuilder

var jobBuilder = scheduler.GetJobBuilder();  

26.1.11.4 获取 TriggerBuilder 集合

var triggerBuilders = scheduler.GetTriggerBuilders();  

26.1.11.5 获取单个 TriggerBuilder

var triggerBuilder = scheduler.GetTriggerBuilder("trigger1");  

26.1.11.6 获取作业信息

var jobDetail = scheduler.GetJobDetail();  

26.1.11.7 获取作业触发器集合

var triggers = scheduler.GetTriggers();  

26.1.11.8 获取单个作业触发器

// 返回 ScheduleResult 方式  
var scheduleResult = scheduler.TryGetTrigger("trigger1", out var trigger);    // 如果存在返回 ScheduleResult.Succeed  
var scheduleResult = scheduler.TryGetTrigger("not_found", out var trigger);    // => ScheduleResult.NotFound  
var scheduleResult = scheduler.TryGetTrigger("", out var trigger);    // => ScheduleResult.NotIdentify  

// 返回 Trigger 方式  
var trigger = scheduler.GetTrigger("trigger1"); // 如果存在返回 Trigger  
var trigger = scheduler.GetTrigger("not_found"); // => null  
var trigger = scheduler.GetTrigger(""); // => null  

26.1.11.9 保存作业触发器

保存作业触发器是框架提供强大且简单的方法,支持 新增编辑删除 作业触发器,也就是三大操作都可以直接通过此方法直接操作。

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TrySaveTrigger(triggerBuilder, out var trigger);  

// 无返回值,支持多个  
scheduler.SaveTrigger(triggerBuilder1, triggerBuilder2, ...)  

关于保存作业触发器的背后行为默认情况下,保存作业触发器需要传递 TriggerBuilder 对象,这个对象可通过 GetTriggerBuilder(triggerId) 获取,如:

var triggerBuilder = scheduler.GetTriggerBuilder("trigger1");  

此时它的内部 Behavior 属性被标记为 PersistenceBehavior.Updated,也就是更新状态,那么对于这个构建器的任何操作都会标记为 更新 操作。

如果通过 .Appended().Removed() 方法标记之后,那么它的操作行为就发生变化了。

  • 如果被标记为 .Appended(),那么它将进行 新增 操作。如:
triggerBuilder.Appended();  

  • 如果被标记为 .Removed(),那么它将进行 删除 操作。如:
triggerBuilder.Removed();  

比如以下的代码实则是 新增删除更新 操作:

// 实际做新增操作  
var scheduleResult = scheduler.TrySaveTrigger(Triggers.PeriodSeconds(5), out var trigger); // Create 方法默认标记为 Appended  

// 实际做删除操作  
var triggerBuilder = scheduler.GetTriggerBuilder("trigger1");  
var scheduleResult = scheduler.TrySaveTrigger(triggerBuilder?.Removed(), out var trigger); // 标记为 Removed  

// 实际做更新操作  
var scheduleResult = scheduler.TrySaveTrigger(Trigggers.PeriodSeconds(5).Updated(), out var trigger); // Create 方法默认标记为 Appended,但调用 Updated() 方法  

26.1.11.10 更新作业信息

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryUpdateDetail(jobBuilder, out var jobDetail);  

// 无返回值  
scheduler.UpdateDetail(jobBuilder);  

// Furion 4.8.6+ 支持  
// 返回 ScheduleResult 类型  
var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder =>  
{  
      jobBuilder.SetDescription("~~~");  
}, out var jobDetail);  

// Furion 4.8.6+ 支持  
// 无返回值  
scheduler.UpdateDetail(jobBuilder =>  
{  
      jobBuilder.SetDescription("~~~");  
});  

26.1.11.11 添加作业触发器

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryAddTrigger(triggerBuilder, out var trigger);  

// 无返回值,支持多个  
scheduler.AddTrigger(triggerBuilder1, triggerBuilder2, ...);  

26.1.11.12 更新作业触发器

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryUpdateTrigger(triggerBuilder, out var trigger);  

// 无返回值,支持多个  
scheduler.UpdateTrigger(triggerBuilder1, triggerBuilder2, ...);  

// Furion 4.8.6+ 支持  
// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder =>  
{  
      triggerBuilder.SetDescription("~~");  
}, out var trigger);  

// Furion 4.8.6+ 支持  
// 无返回值  
scheduler.UpdateTrigger("triggerId", triggerBuilder =>  
{  
      triggerBuilder.SetDescription("~~");  
});  

26.1.11.13 删除作业触发器

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryRemoveTrigger("trigger1", out var trigger);  

// 无返回值,支持多个  
scheduler.RemoveTrigger("trigger1", "trigger2", ...);  

26.1.11.14 删除当前作业

// 返回 ScheduleResult 类型  
var scheduleResult = scheduler.TryRemove();  

// 无返回值  
scheduler.Remove();  

26.1.11.15 判断作业触发器是否存在

bool isExist = scheduler.ContainsTrigger("trigger1");  

26.1.11.16 启动作业触发器

bool succeed = scheduler.StartTrigger("trigger1");  

26.1.11.17 暂停作业触发器

bool succeed = scheduler.PauseTrigger("trigger1");  

26.1.11.18 强制触发作业持久化操作

scheduler.Persist();  

26.1.11.19 启动当前作业

scheduler.Start();  

26.1.11.20 暂停当前作业

scheduler.Pause();  

26.1.11.21 校对当前作业

scheduler.Collate();  

26.1.11.22 强制刷新当前作业

版本说明以下内容仅限 Furion 4.8.3.3 + 版本使用。

通常情况下我们通过 _schedulerFactory.GetJob("jobId") 获取到作业之后,然后对这个作业进行操作,但操作之后这个对象并不能同步更改,需要反复调用 GetJob 方法。

所以在 Furion 4.8.3.3+ 版本之后,IScheduler 任何操作都将自动调用 Reload() 方法刷新变量

// 也可以自己手动强制刷新(通常不需要调用下面代码~~)  
scheduler.Reload();  

26.1.11.23 转换成 JSON 格式

var json = scheduler.ConvertToJSON();  
var json = scheduler.ConvertToJSON(NamingConventions.CamelCase);  

26.1.11.24 转换成可枚举字典

版本说明以下内容仅限 Furion 4.8.4 + 版本使用。

通常我们在开发应用时,需要将作业计划信息进行拆解,比如一个作业计划包含两个作业触发器,那么可以通过 scheduler.GetEnumerable() 方法生成可枚举字典对象,字典中的项数量等于作业触发器数量。

foreach (var (jobDetail, trigger) in scheduler.GetEnumerable())  
{  
      // ....  
}  

26.1.11.25 立即执行作业

版本说明以下内容仅限 Furion 4.8.7.11 + 版本使用。

scheduler.Run();  
scheduler.Run("triggerId");   // Furion 4.9.3+ 支持指定作业触发器 Id  

注意事项如果作业本身处于 (Pause)暂停(NotStart)初始化时未启动(Unoccupied)无触发时间 状态,那么点击 立即执行 后将自动转至 就绪 状态。

26.1.11.26 取消正在执行作业

版本说明以下内容仅限 Furion 4.9.1.9 + 版本使用。

// 取消作业下所有触发器正在执行的程序  
scheduler.Cancel();  
// 取消作业下特定触发器正在执行的程序  
scheduler.Cancel("triggerId");      // Furion 4.9.2.38+ 支持设置作业触发器 Id  

注意事项如果作业处理程序任务 Task 订阅了 stoppingToken 且正在执行,那么将被取消同时抛出 TaskCanceledException 异常。

26.1.12 作业持久化器 IJobPersistence

26.1.12.1 关于作业持久化器

作业持久化器指的是可以通过存储介质如数据库中加载作业信息到内存中,又可以将内存中作业调度器的作业信息实时同步回存储介质中。

26.1.12.2 实现作业持久化器

调度作业服务提供了非常简单的 IJobPersistence 接口,只需实现该接口即可实现持久化,如实现数据库持久化:

public class DbJobPersistence : IJobPersistence  
{  
    public Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)    // Furion 4.9.1.59 之前为 public IEnumerable<SchedulerBuilder> Preload()  
    {  
        // 作业调度服务启动时运行时初始化,可通过数据库加载,或者其他方式  
        return Task.FromResult(Enumerable.Empty<SchedulerBuilder>());  
    }  

    public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) // Furion 4.9.1.59 之前为 public SchedulerBuilder OnLoading(SchedulerBuilder builder)  
    {  
        // 如果是更新操作,则 return builder.Updated(); 将生成 UPDATE 语句  
        // 如果是新增操作,则 return builder.Appended(); 将生成 INSERT 语句  
        // 如果是删除操作,则 return builder.Removed(); 将生成 DELETE 语句  
        // 如果无需标记操作,返回 builder 默认值即可  
        return Task.FromResult(builder);  
    }  

    public Task OnChangedAsync(PersistenceContext context)  // Furion 4.9.1.59 之前为 public void OnChanged(PersistenceContext context)  
    {  
        var sql = context.ConvertToSQL("job_detail");  
        // 这里执行 sql 即可 💖  
    }  

    public Task OnTriggerChangedAsync(PersistenceTriggerContext context)     // Furion 4.9.1.59 之前为 public void OnTriggerChanged(PersistenceTriggerContext context)  
    {  
        var sql = context.ConvertToSQL("job_trigger");  
        // 这里执行 sql 即可 💖  
    }  

    public Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context) // Furion 4.9.1.16+ 支持,Furion 4.9.1.59 之前为 public void OnExecutionRecord(TriggerTimeline timeline)  
    {  
        // 作业运行记录,写入数据库即可  
    }  
}  

之后在 Startup.cs 中注册:

services.AddSchedule(options =>  
{  
      options.AddPersistence<DbJobPersistence>();  
});  

可能有些开发者看到这里一脸不解,持久化不应该这么简单啊!其实就是这么简单....

26.1.12.3 IJobPersistence 详细说明

IJobPersistence 接口提供了以下四个方法:

  • PreloadAsync/Preload:作业调度服务启动时调用,可在这里动态创建作业计划构建器并返回。
public Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)    // Furion 4.9.1.59 之前为 public IEnumerable<SchedulerBuilder> Preload()  
{  
      // 可以这里查询数据库返回  

      // 这里可以扫描程序集动态创建返回  
      return Task.FromResult(App.EffectiveTypes.Where(t => t.IsJobType())  
                               .Select(t => SchedulerBuilder.Create(JobBuilder.Create(t), t.ScanTriggers())));  

      // 如果类型贴有 [JobDetail] 特性,还可以一键扫描返回  
      return Task.FromResult(App.EffectiveTypes.Where(t => t.IsJobType())  
                               .Select(t => t.ScanToBuilder()));  

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

      // 也可以手动返回  
      return Task.FromResult<IEnumerable<SchedulerBuilder>>(new[]  
      {  
            SchedulerBuilder.Create(JobBuilder.Create<MyJob>(), Triggers.Minutely()))  
      }  
}  

查看 EFCoreSqlSugar 使用示例:

  • EFCore
  • SqlSugar
/// <summary>  
/// 作业持久化(数据库)  
/// </summary>  
public class DbJobPersistence : IJobPersistence, IDisposable  
{  
    private readonly IServiceScope _serviceScope;  
    private readonly IRepository<SysJobDetail> _jobRepository;  
    private readonly IRepository<SysJobTrigger> _triggerRepository;  

    public DbJobPersistence(IServiceScopeFactory scopeFactory)  
    {  
        _serviceScope = scopeFactory.CreateScope();  
        var services = _serviceScope.ServiceProvider;  

        _jobRepository = services.GetService<IRepository<SysJobDetail>>();  
        _triggerRepository = services.GetService<IRepository<SysJobTrigger>>();  
    }  

    /// <summary>  
    /// 作业调度服务启动时  
    /// </summary>  
    /// <returns></returns>  
    public async Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)    // Furion 4.9.1.59 之前为 public IEnumerable<SchedulerBuilder> Preload()  
    {  
        // 获取所有定义的作业  
        var allJobs = App.EffectiveTypes.ScanToBuilders();  

        // 若数据库不存在任何作业,则直接返回  
        if (!(await _jobRepository.AnyAsync(u => true, stoppingToken))) return allJobs;  

        // 遍历所有定义的作业  
        foreach (var schedulerBuilder in allJobs)  
        {  
            // 获取作业信息构建器  
            var jobBuilder = schedulerBuilder.GetJobBuilder();  

            // 加载数据库数据  
            var dbDetail = await _jobRepository.FirstOfDefaultAsync(u => u.JobId == jobBuilder.JobId, stoppingToken);  
            if (dbDetail == null) continue;  

            // 同步数据库数据  
            jobBuilder.LoadFrom(dbDetail);  

            // 遍历所有作业触发器  
            foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable())  
            {  
                // 加载数据库数据  
                var dbTrigger = await _triggerRepository.FirstOrDefaultAsync(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId, stoppingToken);  
                if (dbTrigger == null) continue;  

                triggerBuilder.LoadFrom(dbTrigger)  
                              .Updated();   // 标记更新  
            }  

            // 标记更新  
            schedulerBuilder.Updated();  
        }  

        return allJobs;  
    }  

    // ...  

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

/// <summary>  
/// 作业持久化(数据库)  
/// </summary>  
public class DbJobPersistence : IJobPersistence  
{  
    private readonly IServiceScopeFactory _scopeFactory;  

    public DbJobPersistence(IServiceScopeFactory scopeFactory)  
    {  
        _scopeFactory = scopeFactory;  
    }  

    /// <summary>  
    /// 作业调度服务启动时  
    /// </summary>  
    /// <returns></returns>  
    public async Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)    // Furion 4.9.1.59 之前为 public IEnumerable<SchedulerBuilder> Preload()  
    {  
        // 创建服务作用域  
        using var serviceScope = scopeFactory.CreateScope();  
        var services = serviceScope.ServiceProvider;  

        // 解析仓储  
        var jobRepository = services.GetService<Repository<SysJobDetails>>();  
        var triggerRepository = services.GetService<Repository<SysJobTriggers>>();  

        // 获取所有定义的作业  
        var allJobs = App.EffectiveTypes.ScanToBuilders();  

        // 若数据库不存在任何作业,则直接返回  
        if (!(await jobRepository.IsAnyAsync(u => true))) return allJobs;  

        // 遍历所有定义的作业  
        foreach (var schedulerBuilder in allJobs)  
        {  
            // 获取作业信息构建器  
            var jobBuilder = schedulerBuilder.GetJobBuilder();  

            // 加载数据库数据  
            var dbDetail = await jobRepository.GetFirstAsync(u => u.JobId == jobBuilder.JobId);  
            if (dbDetail == null) continue;  

            // 同步数据库数据  
            jobBuilder.LoadFrom(dbDetail);  

            // 遍历所有作业触发器  
            foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable())  
            {  
                // 加载数据库数据  
                var dbTrigger = await triggerRepository.GetFirstAsync(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId);  
                if (dbTrigger == null) continue;  

                triggerBuilder.LoadFrom(dbTrigger)  
                              .Updated();   // 标记更新  
            }  

            // 标记更新  
            schedulerBuilder.Updated();  
        }  

        return allJobs;  
    }  

    // ...  
}  

  • OnLoadingAsync/OnLoading:作业计划初始化通知,通常在这里进一步修改初始化作业计划构建器。

在作业调度器服务启动时会遍历程序中所有作业计划构建器,然后逐条调用该方法,开发者可以在这里进一步修改作业计划构建器数据,之后选择性返回 SchedulerBuilder 的持久化行为

SchedulerBuilder 提供 Updated()Appended()Removed() 来作为持久化行为标记。此标记将决定最终生成的 SQLUPDATE 还是 INSERT 还是 UPDATE

public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) // Furion 4.9.1.59 之前为 public SchedulerBuilder OnLoading(SchedulerBuilder builder)  
{  
      // 比如这里修改作业信息描述  
      builder.GetJobBuilder()  
             .SetDescription("这是描述~~");  

      // 还可以修改触发器  
      builder.GetTriggerBuilder("trigger1")  
             .SetDescription("这是触发器描述~~");  

      // 还可以通过数据库查询返回填充 😎  
      builder.GetJobBuilder()  
             .LoadFrom(dbJobDetail); // dbJobDetail 表示根据 jobId 查询数据库返回的对象  

      // 还可以获取枚举对象逐条更新  
      foreach(var (jobBuilder, triggerBuilder) in builder.GetEnumerable())  
      {  
            jobBuilder.SetDescription("....");  
            triggerBuilder.Updated();     // 标记该触发器已被更新,并生成 UPDATE 语句  
            triggerBuilder.Removed();     // 标记该触发器已被删除,并生成 DELETE 语句  
      }  

      // 标记从其他地方更新,比如数据库  
      return Task.FromResult(builder);  
}  

如果存储介质(如数据库)已经删除该作业,开发者可以标记为 Removed(),这样该作业会从内存中移除。

public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) // Furion 4.9.1.59 之前为 public SchedulerBuilder OnLoading(SchedulerBuilder builder)  
{  
      // 比如这里根据 jobId 查询数据库已经确认数据不存在了  

      // 标记从其他地方移除  
      return Task.FromResult(builder.Removed());  
}  

如果存储介质(如数据库)新增了新作业但内存中不存在,开发者可以标记为 Append(),这样该作业会添加到内存中,但原有的 builder 就会被丢弃

public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) // Furion 4.9.1.59 之前为 public SchedulerBuilder OnLoading(SchedulerBuilder builder)  
{  
      // 比如在这里动态创建作业计划构建器  
      var newBuilder = SchedulerBuilder.Create<MyJob>(Triggers.Minutely());  
      // 还可以克隆一个  
      var newBuilder = SchedulerBuilder.Clone(builder);  
      // 还可以读取配置文件/JSON  
      var newBuilder = SchedulerBuilder.From(json);  

      // 返回新的作业计划构建器并标记为新增  
      return Task.FromResult(newBuilder.Appended());  
}  

  • OnChangedAsync/OnChanged:作业计划 SchedulerJobDetail 变化时调用。

只要作业计划有任何变化都将触发该方法,该方法有一个 PersistenceContext 类型的参数 contextPersistenceContext 包含以下成员:

  • PersistenceContext 属性列表
    • JobId:作业 Idstring 类型
    • JobDetail:作业信息,JobDetail 类型
    • Behavior:持久化行为,PersistenceBehavior 枚举类型,包含 AppendedUpdatedRemoved 三个枚举成员
  • PersistenceContext 方法列表
    • ConvertToSQL:将 PersistenceContext 转换成 SQL 字符串,Behavior 属性值不同,生成的 SQL 不同
    • ConvertToJSON:将 PersistenceContext 转换成 JSON 字符串
    • ConvertToMonitor:将 PersistenceContext 转换成 Monitor 字符串
    • ToString:将 PersistenceContext 转换成 简要 字符串
    • GetNaming:提供将特定字符串输出不同的命名规则字符串
public Task OnChangedAsync(PersistenceContext context)  // Furion 4.9.1.59 之前为 public void OnChanged(PersistenceContext context)  
{  
      // 输出 CamelCase(驼峰命名法)SQL 语句,默认值  
      var sql = context.ConvertToSQL("job_detail");  
      // 输出 Pascal(帕斯卡命名法) SQL 语句  
      var sql = context.ConvertToSQL("job_detail", NamingConventions.Pascal);  
      // 输出 UnderScoreCase(下划线命名法) SQL 语句  
      var sql = context.ConvertToSQL("job_detail", NamingConventions.UnderScoreCase);  

      // 你要做的只是执行 SQL 了!!! 😎  
}  

  • OnTriggerChangedAsync/OnTriggerChanged:作业计划 Scheduler 的触发器 Trigger 变化时调用。

只要作业计划触发器有任何变化都将触发该方法,该方法有一个 PersistenceTriggerContext 类型的参数 contextPersistenceTriggerContext 继承自 PersistenceContext

  • PersistenceTriggerContext 属性列表
    • JobId:作业 Idstring 类型
    • JobDetail:作业信息,JobDetail 类型
    • TriggerId:作业触发器 Idstring 类型
    • Trigger:作业触发器,Trigger 类型
    • Behavior:持久化行为,PersistenceBehavior 枚举类型,包含 AppendedUpdatedRemoved 三个枚举成员
  • PersistenceTriggerContext 方法列表
    • ConvertToSQL:将 PersistenceTriggerContext 转换成 SQL 字符串,Behavior 属性值不同,生成的 SQL 不同
    • ConvertToJSON:将 PersistenceTriggerContext 转换成 JSON 字符串,只包含 Trigger
    • ConvertAllToJSON:将 PersistenceTriggerContext 转换成 JSON 字符串,包含 JobDetailTrigger
    • ConvertToMonitor:将 PersistenceTriggerContext 转换成 Monitor 字符串
    • ToString:将 PersistenceTriggerContext 转换成 简要 字符串
    • GetNaming:提供将特定字符串输出不同的命名规则字符串
public Task OnTriggerChangedAsync(PersistenceTriggerContext context)     // Furion 4.9.1.59 之前为 public void OnTriggerChanged(PersistenceTriggerContext context)  
{  
      // 输出 CamelCase(驼峰命名法)SQL 语句,默认值  
      var sql = context.ConvertToSQL("job_trigger");  
      // 输出 Pascal(帕斯卡命名法) SQL 语句  
      var sql = context.ConvertToSQL("job_trigger", NamingConventions.Pascal);  
      // 输出 UnderScoreCase(下划线命名法) SQL 语句  
      var sql = context.ConvertToSQL("job_trigger", NamingConventions.UnderScoreCase);  

      // 你要做的只是执行 SQL 了!!! 😎  
}  

小知识默认情况下,生成的 SQL 属于标准 SQL 语句,但未必适合所有数据库类型,所以我们可以指定 BuildSqlType 来生成特定数据库的语句,如:

services.AddSchedule(options =>  
{  
      // 配置输出 SQL 的数据库类型,Furion 4.8.2.3+  
      options.BuildSqlType = SqlTypes.SqlServer;  
});  

  • OnExecutionRecordAsync/OnExecutionRecord:作业计划 Scheduler 的触发器 Trigger 触发时调用(运行记录)。

版本说明以下内容仅限 Furion 4.9.1.16 + 版本使用。

只要作业计划触发器触发时都将触发该方法,该方法有一个 PersistenceExecutionRecordContext 类型的参数 contextPersistenceExecutionRecordContext 包含以下成员:

  • PersistenceExecutionRecordContext 属性列表
    • JobId:作业 Idstring 类型
    • JobDetail:作业信息,JobDetail 类型
    • TriggerId:作业触发器 Idstring 类型
    • Trigger:作业触发器,Trigger 类型
    • Mode:执行方式,int 类型,0:定时1:手动
    • Timeline:作业触发器运行记录,TriggerTimeline 类型,具体说明见下文
  • PersistenceExecutionRecordContext 方法列表
    • ToString:将 PersistenceExecutionRecordContext 转换成 简要 字符串

另外 context 包含一个 TriggerTimeline 类型的属性 TimelineTriggerTimeline 包含以下成员:

  • TriggerTimeline 属性列表
    • NumberOfRuns:当前运行次数,long 类型
    • LastRunTime:最近运行时间,DateTime? 类型
    • NextRunTime:下一次运行时间,DateTime? 类型
    • Status:作业触发器状态,TriggerStatus 枚举类型
    • Result:本次执行结果,string 类型
    • ElapsedTime:本次执行耗时,long 类型,单位 ms
    • CreatedTime:记录时间,DateTime 类型
    • TriggerId:作业触发器 Idstring 类型,Furion 4.9.1.16+ 支持
    • JobId:作业 Idstring 类型,Furion 4.9.1.16+ 支持
    • Mode:执行方式,int 类型,0:定时1:手动Furion 4.9.1.16+ 支持
    • Exception:异常信息,string 类型,Furion 4.9.4.3+ 支持
public Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context) // Furion 4.9.1.59 之前为 public void OnExecutionRecord(TriggerTimeline timeline)  
{  
      // 写入数据库作业运行记录表  
}  

26.1.12.4 各类数据库创建作业持久化表语句

  • Sqlite
  • SqlServer
  • MySQL
  • PostgreSQL
  • Oracle
  • Firebird
  • EFCore
  • SqlSugar

可自行调整列命名规则。

CREATE TABLE "JobDetails" (  
    "Id" INTEGER NOT NULL CONSTRAINT "PK_JobDetails" PRIMARY KEY AUTOINCREMENT,  
    "JobId" TEXT NOT NULL,  
    "GroupName" TEXT NULL,  
    "JobType" TEXT NULL,  
    "AssemblyName" TEXT NULL,  
    "Description" TEXT NULL,  
    "Concurrent" INTEGER NOT NULL,  
    "IncludeAnnotations" INTEGER NOT NULL,  
    "Properties" TEXT NULL,  
    "UpdatedTime" TEXT NULL  
);  

CREATE TABLE "JobTriggers" (  
    "Id" INTEGER NOT NULL CONSTRAINT "PK_JobTriggers" PRIMARY KEY AUTOINCREMENT,  
    "TriggerId" TEXT NOT NULL,  
    "JobId" TEXT NOT NULL,  
    "TriggerType" TEXT NULL,  
    "AssemblyName" TEXT NULL,  
    "Args" TEXT NULL,  
    "Description" TEXT NULL,  
    "Status" INTEGER NOT NULL,  
    "StartTime" TEXT NULL,  
    "EndTime" TEXT NULL,  
    "LastRunTime" TEXT NULL,  
    "NextRunTime" TEXT NULL,  
    "NumberOfRuns" INTEGER NOT NULL,  
    "MaxNumberOfRuns" INTEGER NOT NULL,  
    "NumberOfErrors" INTEGER NOT NULL,  
    "MaxNumberOfErrors" INTEGER NOT NULL,  
    "NumRetries" INTEGER NOT NULL,  
    "RetryTimeout" INTEGER NOT NULL,  
    "StartNow" INTEGER NOT NULL,  
    "RunOnStart" INTEGER NOT NULL,  
    "ResetOnlyOnce" INTEGER NOT NULL,  
    "Result" TEXT NULL,  
    "ElapsedTime" INTEGER NOT NULL,  
    "UpdatedTime" TEXT NULL  
);  

可自行调整列命名规则。

CREATE TABLE [JobDetails] (  
    [Id] int NOT NULL IDENTITY,  
    [JobId] nvarchar(max) NOT NULL,  
    [GroupName] nvarchar(max) NULL,  
    [JobType] nvarchar(max) NULL,  
    [AssemblyName] nvarchar(max) NULL,  
    [Description] nvarchar(max) NULL,  
    [Concurrent] bit NOT NULL,  
    [IncludeAnnotations] bit NOT NULL,  
    [Properties] nvarchar(max) NULL,  
    [UpdatedTime] datetime2 NULL,  
    CONSTRAINT [PK_JobDetails] PRIMARY KEY ([Id])  
);  
GO  

CREATE TABLE [JobTriggers] (  
    [Id] int NOT NULL IDENTITY,  
    [TriggerId] nvarchar(max) NOT NULL,  
    [JobId] nvarchar(max) NOT NULL,  
    [TriggerType] nvarchar(max) NULL,  
    [AssemblyName] nvarchar(max) NULL,  
    [Args] nvarchar(max) NULL,  
    [Description] nvarchar(max) NULL,  
    [Status] bigint NOT NULL,  
    [StartTime] datetime2 NULL,  
    [EndTime] datetime2 NULL,  
    [LastRunTime] datetime2 NULL,  
    [NextRunTime] datetime2 NULL,  
    [NumberOfRuns] bigint NOT NULL,  
    [MaxNumberOfRuns] bigint NOT NULL,  
    [NumberOfErrors] bigint NOT NULL,  
    [MaxNumberOfErrors] bigint NOT NULL,  
    [NumRetries] int NOT NULL,  
    [RetryTimeout] int NOT NULL,  
    [StartNow] bit NOT NULL,  
    [RunOnStart] bit NOT NULL,  
    [ResetOnlyOnce] bit NOT NULL,  
    [Result] nvarchar(max) NULL,  
    [ElapsedTime] bigint NOT NULL,  
    [UpdatedTime] datetime2 NULL,  
    CONSTRAINT [PK_JobTriggers] PRIMARY KEY ([Id])  
);  

可自行调整列命名规则。

ALTER DATABASE CHARACTER SET utf8mb4;  

CREATE TABLE `JobDetails` (  
    `Id` int NOT NULL AUTO_INCREMENT,  
    `JobId` longtext CHARACTER SET utf8mb4 NOT NULL,  
    `GroupName` longtext CHARACTER SET utf8mb4 NULL,  
    `JobType` longtext CHARACTER SET utf8mb4 NULL,  
    `AssemblyName` longtext CHARACTER SET utf8mb4 NULL,  
    `Description` longtext CHARACTER SET utf8mb4 NULL,  
    `Concurrent` tinyint(1) NOT NULL,  
    `IncludeAnnotations` tinyint(1) NOT NULL,  
    `Properties` longtext CHARACTER SET utf8mb4 NULL,  
    `UpdatedTime` datetime(6) NULL,  
    CONSTRAINT `PK_JobDetails` PRIMARY KEY (`Id`)  
) CHARACTER SET=utf8mb4;  

CREATE TABLE `JobTriggers` (  
    `Id` int NOT NULL AUTO_INCREMENT,  
    `TriggerId` longtext CHARACTER SET utf8mb4 NOT NULL,  
    `JobId` longtext CHARACTER SET utf8mb4 NOT NULL,  
    `TriggerType` longtext CHARACTER SET utf8mb4 NULL,  
    `AssemblyName` longtext CHARACTER SET utf8mb4 NULL,  
    `Args` longtext CHARACTER SET utf8mb4 NULL,  
    `Description` longtext CHARACTER SET utf8mb4 NULL,  
    `Status` int unsigned NOT NULL,  
    `StartTime` datetime(6) NULL,  
    `EndTime` datetime(6) NULL,  
    `LastRunTime` datetime(6) NULL,  
    `NextRunTime` datetime(6) NULL,  
    `NumberOfRuns` bigint NOT NULL,  
    `MaxNumberOfRuns` bigint NOT NULL,  
    `NumberOfErrors` bigint NOT NULL,  
    `MaxNumberOfErrors` bigint NOT NULL,  
    `NumRetries` int NOT NULL,  
    `RetryTimeout` int NOT NULL,  
    `StartNow` tinyint(1) NOT NULL,  
    `RunOnStart` tinyint(1) NOT NULL,  
    `ResetOnlyOnce` tinyint(1) NOT NULL,  
    `Result` longtext CHARACTER SET utf8mb4 NULL,  
    `ElapsedTime` bigint NOT NULL,  
    `UpdatedTime` datetime(6) NULL,  
    CONSTRAINT `PK_JobTriggers` PRIMARY KEY (`Id`)  
) CHARACTER SET=utf8mb4;  

可自行调整列命名规则。

CREATE TABLE "JobDetails" (  
    "Id" integer GENERATED BY DEFAULT AS IDENTITY,  
    "JobId" text NOT NULL,  
    "GroupName" text NULL,  
    "JobType" text NULL,  
    "AssemblyName" text NULL,  
    "Description" text NULL,  
    "Concurrent" boolean NOT NULL,  
    "IncludeAnnotations" boolean NOT NULL,  
    "Properties" text NULL,  
    "UpdatedTime" timestamp with time zone NULL,  
    CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id")  
);  

CREATE TABLE "JobTriggers" (  
    "Id" integer GENERATED BY DEFAULT AS IDENTITY,  
    "TriggerId" text NOT NULL,  
    "JobId" text NOT NULL,  
    "TriggerType" text NULL,  
    "AssemblyName" text NULL,  
    "Args" text NULL,  
    "Description" text NULL,  
    "Status" bigint NOT NULL,  
    "StartTime" timestamp with time zone NULL,  
    "EndTime" timestamp with time zone NULL,  
    "LastRunTime" timestamp with time zone NULL,  
    "NextRunTime" timestamp with time zone NULL,  
    "NumberOfRuns" bigint NOT NULL,  
    "MaxNumberOfRuns" bigint NOT NULL,  
    "NumberOfErrors" bigint NOT NULL,  
    "MaxNumberOfErrors" bigint NOT NULL,  
    "NumRetries" integer NOT NULL,  
    "RetryTimeout" integer NOT NULL,  
    "StartNow" boolean NOT NULL,  
    "RunOnStart" boolean NOT NULL,  
    "ResetOnlyOnce" boolean NOT NULL,  
    "Result" text NULL,  
    "ElapsedTime" bigint NOT NULL,  
    "UpdatedTime" timestamp with time zone NULL,  
    CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id")  
);  

可自行调整列命名规则。

CREATE TABLE "JobDetails" (  
    "Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL,  
    "JobId" NVARCHAR2(2000) NOT NULL,  
    "GroupName" NVARCHAR2(2000),  
    "JobType" NVARCHAR2(2000),  
    "AssemblyName" NVARCHAR2(2000),  
    "Description" NVARCHAR2(2000),  
    "Concurrent" NUMBER(1) NOT NULL,  
    "IncludeAnnotations" NUMBER(1) NOT NULL,  
    "Properties" NVARCHAR2(2000),  
    "UpdatedTime" TIMESTAMP(7),  
    CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id")  
);  

CREATE TABLE "JobTriggers" (  
    "Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL,  
    "TriggerId" NVARCHAR2(2000) NOT NULL,  
    "JobId" NVARCHAR2(2000) NOT NULL,  
    "TriggerType" NVARCHAR2(2000),  
    "AssemblyName" NVARCHAR2(2000),  
    "Args" NVARCHAR2(2000),  
    "Description" NVARCHAR2(2000),  
    "Status" NUMBER(10) NOT NULL,  
    "StartTime" TIMESTAMP(7),  
    "EndTime" TIMESTAMP(7),  
    "LastRunTime" TIMESTAMP(7),  
    "NextRunTime" TIMESTAMP(7),  
    "NumberOfRuns" NUMBER(19) NOT NULL,  
    "MaxNumberOfRuns" NUMBER(19) NOT NULL,  
    "NumberOfErrors" NUMBER(19) NOT NULL,  
    "MaxNumberOfErrors" NUMBER(19) NOT NULL,  
    "NumRetries" NUMBER(10) NOT NULL,  
    "RetryTimeout" NUMBER(10) NOT NULL,  
    "StartNow" NUMBER(1) NOT NULL,  
    "RunOnStart" NUMBER(1) NOT NULL,  
    "ResetOnlyOnce" NUMBER(1) NOT NULL,  
    "Result" NVARCHAR2(2000),  
    "ElapsedTime" NUMBER(19) NOT NULL,  
    "UpdatedTime" TIMESTAMP(7),  
    CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id")  
);  

可自行调整列命名规则。

CREATE TABLE "JobDetails" (  
    "Id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,  
    "JobId" BLOB SUB_TYPE TEXT NOT NULL,  
    "GroupName" BLOB SUB_TYPE TEXT,  
    "JobType" BLOB SUB_TYPE TEXT,  
    "AssemblyName" BLOB SUB_TYPE TEXT,  
    "Description" BLOB SUB_TYPE TEXT,  
    "Concurrent" BOOLEAN NOT NULL,  
    "IncludeAnnotations" BOOLEAN NOT NULL,  
    "Properties" BLOB SUB_TYPE TEXT,  
    "UpdatedTime" TIMESTAMP,  
    CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id")  
);  

CREATE TABLE "JobTriggers" (  
    "Id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,  
    "TriggerId" BLOB SUB_TYPE TEXT NOT NULL,  
    "JobId" BLOB SUB_TYPE TEXT NOT NULL,  
    "TriggerType" BLOB SUB_TYPE TEXT,  
    "AssemblyName" BLOB SUB_TYPE TEXT,  
    "Args" BLOB SUB_TYPE TEXT,  
    "Description" BLOB SUB_TYPE TEXT,  
    "Status" BIGINT NOT NULL,  
    "StartTime" TIMESTAMP,  
    "EndTime" TIMESTAMP,  
    "LastRunTime" TIMESTAMP,  
    "NextRunTime" TIMESTAMP,  
    "NumberOfRuns" BIGINT NOT NULL,  
    "MaxNumberOfRuns" BIGINT NOT NULL,  
    "NumberOfErrors" BIGINT NOT NULL,  
    "MaxNumberOfErrors" BIGINT NOT NULL,  
    "NumRetries" INTEGER NOT NULL,  
    "RetryTimeout" INTEGER NOT NULL,  
    "StartNow" BOOLEAN NOT NULL,  
    "RunOnStart" BOOLEAN NOT NULL,  
    "ResetOnlyOnce" BOOLEAN NOT NULL,  
    "Result" BLOB SUB_TYPE TEXT,  
    "ElapsedTime" BIGINT NOT NULL,  
    "UpdatedTime" TIMESTAMP,  
    CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id")  
);  

可自行调整列命名规则。

public class JobDetail  
{  
    [Key]  
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]  
    public int Id { get; set; }  

    /// <summary>  
    /// 作业 Id  
    /// </summary>  
    public string JobId { get; set; }  

    /// <summary>  
    /// 作业组名称  
    /// </summary>  
    public string? GroupName { get; set; }  

    /// <summary>  
    /// 作业处理程序类型  
    /// </summary>  
    /// <remarks>存储的是类型的 FullName</remarks>  
    public string? JobType { get; set; }  

    /// <summary>  
    /// 作业处理程序类型所在程序集  
    /// </summary>  
    /// <remarks>存储的是程序集 Name</remarks>  
    public string? AssemblyName { get; set; }  

    /// <summary>  
    /// 描述信息  
    /// </summary>  
    public string? Description { get; set; }  

    /// <summary>  
    /// 是否采用并行执行  
    /// </summary>  
    /// <remarks>如果设置为 false,那么使用串行执行</remarks>  
    public bool Concurrent { get; set; } = true;  

    /// <summary>  
    /// 是否扫描 IJob 实现类 [Trigger] 特性触发器  
    /// </summary>  
    public bool IncludeAnnotations { get; set; } = false;  

    /// <summary>  
    /// 作业信息额外数据  
    /// </summary>  
    public string? Properties { get; set; } = "{}";  

    /// <summary>  
    /// 作业更新时间  
    /// </summary>  
    public DateTime? UpdatedTime { get; set; }  
}  

public class JobTrigger  
{  
    [Key]  
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]  
    public int Id { get; set; }  

    /// <summary>  
    /// 作业触发器 Id  
    /// </summary>  
    public string TriggerId { get; set; }  

    /// <summary>  
    /// 作业 Id  
    /// </summary>  
    public string JobId { get; set; }  

    /// <summary>  
    /// 作业触发器类型  
    /// </summary>  
    /// <remarks>存储的是类型的 FullName</remarks>  
    public string? TriggerType { get; set; }  

    /// <summary>  
    /// 作业触发器类型所在程序集  
    /// </summary>  
    /// <remarks>存储的是程序集 Name</remarks>  
    public string? AssemblyName { get; set; }  

    /// <summary>  
    /// 作业触发器参数  
    /// </summary>  
    /// <remarks>运行时将反序列化为 object[] 类型并作为构造函数参数</remarks>  
    public string? Args { get; set; }  

    /// <summary>  
    /// 描述信息  
    /// </summary>  
    public string? Description { get; set; }  

    /// <summary>  
    /// 作业触发器状态  
    /// </summary>  
    public TriggerStatus Status { get; set; } = TriggerStatus.Ready;  

    /// <summary>  
    /// 起始时间  
    /// </summary>  
    public DateTime? StartTime { get; set; }  

    /// <summary>  
    /// 结束时间  
    /// </summary>  
    public DateTime? EndTime { get; set; }  

    /// <summary>  
    /// 最近运行时间  
    /// </summary>  
    public DateTime? LastRunTime { get; set; }  

    /// <summary>  
    /// 下一次运行时间  
    /// </summary>  
    public DateTime? NextRunTime { get; set; }  

    /// <summary>  
    /// 触发次数  
    /// </summary>  
    public long NumberOfRuns { get; set; }  

    /// <summary>  
    /// 最大触发次数  
    /// </summary>  
    /// <remarks>  
    /// <para>0:不限制</para>  
    /// <para>n:N 次</para>  
    /// </remarks>  
    public long MaxNumberOfRuns { get; set; }  

    /// <summary>  
    /// 出错次数  
    /// </summary>  
    public long NumberOfErrors { get; set; }  

    /// <summary>  
    /// 最大出错次数  
    /// </summary>  
    /// <remarks>  
    /// <para>0:不限制</para>  
    /// <para>n:N 次</para>  
    /// </remarks>  
    public long MaxNumberOfErrors { get; set; }  

    /// <summary>  
    /// 重试次数  
    /// </summary>  
    public int NumRetries { get; set; } = 0;  

    /// <summary>  
    /// 重试间隔时间  
    /// </summary>  
    /// <remarks>默认1000毫秒</remarks>  
    public int RetryTimeout { get; set; } = 1000;  

    /// <summary>  
    /// 是否立即启动  
    /// </summary>  
    public bool StartNow { get; set; } = true;  

    /// <summary>  
    /// 是否启动时执行一次  
    /// </summary>  
    public bool RunOnStart { get; set; } = false;  

    /// <summary>  
    /// 是否在启动时重置最大触发次数等于一次的作业  
    /// </summary>  
    /// <remarks>解决因持久化数据已完成一次触发但启动时不再执行的问题</remarks>  
    public bool ResetOnlyOnce { get; set; } = true;  

    /// <summary>  
    /// 本次执行结果  
    /// </summary>  
    public string? Result { get; set; }  

    /// <summary>  
    /// 本次执行耗时  
    /// </summary>  
    public long ElapsedTime { get; set; }  

    /// <summary>  
    /// 作业触发器更新时间  
    /// </summary>  
    public DateTime? UpdatedTime { get; set; }  
}  

可自行调整列命名规则。

[SugarTable("JobDetail", "作业信息表")]  
public class JobDetail  
{  
    /// <summary>  
    /// Id  
    /// </summary>  
    [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true, IsIdentity = true)]  
    public virtual long Id { get; set; }  

    /// <summary>  
    /// 作业 Id  
    /// </summary>  
    [SugarColumn(ColumnDescription = "作业Id")]  
    public virtual string JobId { get; set; }  

    /// <summary>  
    /// 组名称  
    /// </summary>  
    [SugarColumn(ColumnDescription = "组名称")]  
    public string? GroupName { get; set; }  

    /// <summary>  
    /// 作业类型 FullName  
    /// </summary>  
    [SugarColumn(ColumnDescription = "作业类型")]  
    public string? JobType { get; set; }  

    /// <summary>  
    /// 程序集 Name  
    /// </summary>  
    [SugarColumn(ColumnDescription = "程序集")]  
    public string? AssemblyName { get; set; }  

    /// <summary>  
    /// 描述信息  
    /// </summary>  
    [SugarColumn(ColumnDescription = "描述信息")]  
    public string? Description { get; set; }  

    /// <summary>  
    /// 是否并行执行  
    /// </summary>  
    [SugarColumn(ColumnDescription = "是否并行执行")]  
    public bool Concurrent { get; set; } = true;  

    /// <summary>  
    /// 是否扫描特性触发器  
    /// </summary>  
    [SugarColumn(ColumnDescription = "是否扫描特性触发器")]  
    public bool IncludeAnnotations { get; set; } = false;  

    /// <summary>  
    /// 额外数据  
    /// </summary>  
    [SugarColumn(ColumnDescription = "额外数据", ColumnDataType = "longtext,text,clob")]  
    public string? Properties { get; set; } = "{}";  

    /// <summary>  
    /// 更新时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "更新时间")]  
    public DateTime? UpdatedTime { get; set; }  
}  

[SugarTable("JobTrigger", "作业触发器表")]  
public class JobTrigger  
{  
    /// <summary>  
    /// Id  
    /// </summary>  
    [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true, IsIdentity = true)]  
    public virtual long Id { get; set; }  

    /// <summary>  
    /// 触发器 Id  
    /// </summary>  
    [SugarColumn(ColumnDescription = "触发器Id")]  
    public virtual string TriggerId { get; set; }  

    /// <summary>  
    /// 作业 Id  
    /// </summary>  
    [SugarColumn(ColumnDescription = "作业Id")]  
    public virtual string JobId { get; set; }  

    /// <summary>  
    /// 触发器类型 FullName  
    /// </summary>  
    [SugarColumn(ColumnDescription = "触发器类型")]  
    public string? TriggerType { get; set; }  

    /// <summary>  
    /// 程序集 Name  
    /// </summary>  
    [SugarColumn(ColumnDescription = "程序集")]  
    public string? AssemblyName { get; set; }  

    /// <summary>  
    /// 参数  
    /// </summary>  
    [SugarColumn(ColumnDescription = "参数")]  
    public string? Args { get; set; }  

    /// <summary>  
    /// 描述信息  
    /// </summary>  
    [SugarColumn(ColumnDescription = "描述信息")]  
    public string? Description { get; set; }  

    /// <summary>  
    /// 状态  
    /// </summary>  
    [SugarColumn(ColumnDescription = "状态")]  
    public TriggerStatus Status { get; set; } = TriggerStatus.Ready;  

    /// <summary>  
    /// 起始时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "起始时间")]  
    public DateTime? StartTime { get; set; }  

    /// <summary>  
    /// 结束时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "结束时间")]  
    public DateTime? EndTime { get; set; }  

    /// <summary>  
    /// 最近运行时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "最近运行时间")]  
    public DateTime? LastRunTime { get; set; }  

    /// <summary>  
    /// 下一次运行时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "下一次运行时间")]  
    public DateTime? NextRunTime { get; set; }  

    /// <summary>  
    /// 触发次数  
    /// </summary>  
    [SugarColumn(ColumnDescription = "触发次数")]  
    public long NumberOfRuns { get; set; }  

    /// <summary>  
    /// 最大触发次数(0:不限制,n:N次)  
    /// </summary>  
    [SugarColumn(ColumnDescription = "最大触发次数")]  
    public long MaxNumberOfRuns { get; set; }  

    /// <summary>  
    /// 出错次数  
    /// </summary>  
    [SugarColumn(ColumnDescription = "出错次数")]  
    public long NumberOfErrors { get; set; }  

    /// <summary>  
    /// 最大出错次数(0:不限制,n:N次)  
    /// </summary>  
    [SugarColumn(ColumnDescription = "最大出错次数")]  
    public long MaxNumberOfErrors { get; set; }  

    /// <summary>  
    /// 重试次数  
    /// </summary>  
    [SugarColumn(ColumnDescription = "重试次数")]  
    public int NumRetries { get; set; }  

    /// <summary>  
    /// 重试间隔时间(ms)  
    /// </summary>  
    [SugarColumn(ColumnDescription = "重试间隔时间(ms)")]  
    public int RetryTimeout { get; set; } = 1000;  

    /// <summary>  
    /// 是否立即启动  
    /// </summary>  
    [SugarColumn(ColumnDescription = "是否立即启动")]  
    public bool StartNow { get; set; } = true;  

    /// <summary>  
    /// 是否启动时执行一次  
    /// </summary>  
    [SugarColumn(ColumnDescription = "是否启动时执行一次")]  
    public bool RunOnStart { get; set; } = false;  

    /// <summary>  
    /// 是否在启动时重置最大触发次数等于一次的作业  
    /// </summary>  
    /// <remarks>解决因持久化数据已完成一次触发但启动时不再执行的问题</remarks>  
    [SugarColumn(ColumnDescription = "是否在启动时重置最大触发次数等于一次的作业")]  
    public bool ResetOnlyOnce { get; set; } = true;  

    /// <summary>  
    /// 本次执行结果  
    /// </summary>  
    [SugarColumn(ColumnDescription = "本次执行结果")]  
    public string? Result { get; set; }  

    /// <summary>  
    /// 本次执行耗时  
    /// </summary>  
    [SugarColumn(ColumnDescription = "本次执行耗时")]  
    public long ElapsedTime { get; set; }  

    /// <summary>  
    /// 更新时间  
    /// </summary>  
    [SugarColumn(ColumnDescription = "更新时间")]  
    public DateTime? UpdatedTime { get; set; }  
}  

26.1.13 作业集群控制

框架提供简单的集群功能,但并不能达到负载均衡的效果,而仅仅提供了故障转移的功能,当一个服务的作业调度器宕机时,另一个服务的作业调度器会启动。

26.1.13.1 实现集群故障转移

  1. 创建 JobClusterServer 类并实现 IJobClusterServer
public class JobClusterServer : IJobClusterServer  
{  
    /// <summary>  
    /// 当前作业调度器启动通知  
    /// </summary>  
    /// <param name="context">作业集群服务上下文</param>  
    public void Start(JobClusterContext context)  
    {  
        // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting  
    }  

    /// <summary>  
    /// 等待被唤醒  
    /// </summary>  
    /// <param name="context">作业集群服务上下文</param>  
    /// <returns><see cref="Task"/></returns>  
    public async Task WaitingForAsync(JobClusterContext context)  
    {  
        var clusterId = context.ClusterId;  

        while (true)  
        {  
            try  
            {  
                // 在这里查询数据库,根据以下两种情况处理  
                // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环  
                // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;  
                // 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return;  

                await WorkNowAsync(clusterId);  
                return;  
            }  
            catch { }  

            // 控制集群心跳频率  
            await Task.Delay(3000);  
        }  
    }  

    /// <summary>  
    /// 当前作业调度器停止通知  
    /// </summary>  
    /// <param name="context">作业集群服务上下文</param>  
    public void Stop(JobClusterContext context)  
    {  
        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed  
    }  

    /// <summary>  
    /// 当前作业调度器宕机  
    /// </summary>  
    /// <param name="context">作业集群服务上下文</param>  
    public void Crash(JobClusterContext context)  
    {  
        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed  
    }  

    /// <summary>  
    /// 指示集群可以工作  
    /// </summary>  
    /// <param name="clusterId">集群 Id</param>  
    /// <returns></returns>  
    private Task WorkNowAsync(string clusterId)  
    {  
        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working  

        // 模拟数据库更新操作(耗时)  
        await Task.Delay(3000);  
    }  
}  

  1. 注册集群服务
services.AddSchedule(options =>  
{  
      options.ClusterId = "cluster1";  
      options.AddClusterServer<JobClusterServer>();  
});  

  1. 作业集群输出日志
info: 2022-12-05 18:26:11.4045753 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      Schedule hosted service is running.  
info: 2022-12-05 18:26:11.4126431 +08:00 星期一 L System.Logging.ScheduleService[0] #1  
      The job cluster of <cluster1> service has been enabled, and waiting for instructions.  
warn: 2022-12-05 18:26:14.4333100 +08:00 星期一 L System.Logging.ScheduleService[0] #6  
      The job cluster of <cluster1> service worked now, and the current schedule hosted service will be preloading.  
info: 2022-12-05 18:26:14.4411758 +08:00 星期一 L System.Logging.ScheduleService[0] #6  
      Schedule hosted service is preloading...  
info: 2022-12-05 18:26:14.4684974 +08:00 星期一 L System.Logging.ScheduleService[0] #6  
      The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.  
info: 2022-12-05 18:26:14.4701128 +08:00 星期一 L System.Logging.ScheduleService[0] #6  
      The scheduler of <job1> successfully appended to the schedule.  
warn: 2022-12-05 18:26:14.4765709 +08:00 星期一 L System.Logging.ScheduleService[0] #6  
      Schedule hosted service preload completed, and a total of <1> schedulers are appended.  
info: 2022-12-05 18:26:19.5089541 +08:00 星期一 L MyJob[0] #16  
      <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-05 18:26:19.434 -> 2022-12-05 18:26:24.441  

26.1.13.2 作业集群数据库表设计

只需包含 IdClusterIdDescriptionStatusUpdatedTime 字段即可,其中 StatusClusterStatus 枚举类型。

  • ClusterStatus 包含以下枚举成员
    • Crashed:宕机
    • Working:正常工作
    • Waiting:等待被唤醒,默认值

26.1.13.3 如何实现负载均衡

框架只提供了简单的故障转移的集群功能,如需实现负载均衡,可通过 TCP/IP 套接字实现。

26.1.14 ScheduleServe 静态类

该功能 建议 仅限不能通过 services.AddXXX 方式使用,比如控制台,Winform/WPF 等。

IDisposable dispose =  ScheduleServe.Run(options =>  
{  
    options.AddJob<MyJob>(Triggers.Secondly());  
});  

这种方式有一个隐藏的巨大隐藏 “骚操作”:可以在任何地方创建作业调度服务,多次调用可以创建多个作业调度器。

推荐使用 Serve.Run()Serve.RunGeneric() 方式替代Furion 框架提供了 Serve.Run() 方式支持跨平台使用,还能支持注册更多服务,如:

Serve.Run(services =>  
{  
    services.AddSchedule(options =>  
    {  
        options.Add<MyJob>(Triggers.Secondly());  
    });  
})  

如无需 Web 功能,可通过 Serve.RunGeneric 替代 Serve.Run

26.1.15 如何部署

如果在项目中使用了定时任务且部署到 IIS 中,那么需要设置 IIS 禁止回收,点击查看 IIS 回收问题解决方案

部署建议建议定时任务采用 Worker Service 独立部署方式,不应依托 Web 项目进程中。查看【 Worker Service】章节

26.1.15.1 Worker Service 代码集成例子

1. 安装 FurionSundial

# 完整的开发框架  
dotnet add package Furion;  

# 只需要定时任务服务功能  
dotnet add package Sundial  

2. 注册 Schedule 服务

using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  
using Furion.Schedule;  
// using Sundial;  

namespace FurionWorkers;  

public class Program  
{  
      public static void Main(string[] args)  
      {  
            CreateHostBuilder(args).Build().Run();  
      }  

      public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .ConfigureServices((hostContext, services) =>  
                {  
                      services.AddSchedule(options =>  
                      {  
                          options.AddJob<MyJob>(Triggers.PeriodSeconds(5));  
                      });  
                });  
}  

小知识如果使用 Serve 模式,那么代码将非常精简,无需上面第二个步骤的代码\~,如:

Serve.RunGeneric(services =>  
{  
      services.AddSchedule(options =>  
      {  
            options.AddJob<MyJob>(Triggers.PeriodSeconds(5));  
      });  
})  

26.1.16 Dashboard 看板功能

版本说明以下内容仅限 Furion 4.8.4 + 版本使用。

Furion 4.8.4+ 版本内置了一个嵌入的定时任务看板 UI,只需要在 Startup.cs 中启用即可,如:

Sundial 中使用如果使用的是 Sundial 独立开源项目,只需要安装 Sundial.Dashboard 包即可,无需安装 Sundial,前者已添加了后者的引用。

app.UseStaticFiles();  
app.UseScheduleUI();  

// 还可以配置生产环境关闭  
app.UseScheduleUI(options =>  
{  
    options.RequestPath = "/custom-job";  // 配置看板入口地址,Furion 4.8.5.6+ 版本支持,必须以 / 开头且不以 / 结尾(Furion 4.9.5+不再有此限制)  
    options.DisableOnProduction = true;  
    options.SyncRate = 300;   // 控制看板刷新频率,默认 300ms,Furion 4.8.7.43+ 支持,`Furion 4.8.8.29+` 已移除  
    // options.VisualPath = "/job";  // 二级虚拟目录路径,Furion 4.8.8.20+ 版本支持,必须以 / 开头且不以 / 结尾  
    options.DisplayEmptyTriggerJobs = true;     // 是否显示空作业触发器的作业,Furion 4.9.2.39+ 支持  
    options.DisplayHead = true;     // 是否显示页头,Furion 4.9.2.39+ 支持  
    options.DefaultExpandAllJobs = false;     // 是否默认展开所有作业,Furion 4.9.2.40+ 支持  
});  

中间件说明app.UseScheduleUI() 必须在 app.UseStaticFiles() 之后注册。

26.1.16.1 在 Worker Service 中注册

默认情况下,Worker Service 不提供 Web 功能,那么自然而然不能提供 Web 看板功能,如果想使其支持,可通过以下步骤:

  1. 添加日志配置(appsettings.jsonappsettings.Development.json
{  
  "Logging": {  
    "LogLevel": {  
      "Default": "Information",  
      "Microsoft.Hosting.Lifetime": "Information",  
      "Microsoft.AspNetCore": "Warning",  
      "System.Net.Http.HttpClient": "Warning"  
    }  
  }  
}  

  1. 注册中间件服务
using Microsoft.AspNetCore.Builder;  
using Microsoft.AspNetCore.Hosting;  
using WorkerService1;  

IHost host = Host.CreateDefaultBuilder(args)  
    .ConfigureServices(services =>  
    {  
        services.AddSchedule();  
        services.AddHostedService<Worker>();  
    })  
    .ConfigureWebHostDefaults(builder => builder.Configure(app =>  
    {  
        app.UseStaticFiles();  
        app.UseScheduleUI();  
    }))  
    .Build();  

host.Run();  

26.1.16.2 看板配置选项

app.UseScheduleUI 提供了可选的 ScheduleUIOptions 配置选项,提供以下配置:

  • RequestPath:配置看板入口地址,string 类型,默认 /schedule,需以 / 开头,结尾不包含 /
  • DisableOnProduction:是否在生产环境关闭,bool 类型,默认 false
  • ~~SyncRate:控制看板刷新频率,int 类型,默认 300,单位 毫秒Furion 4.8.7.43+ 支持,Furion 4.8.8.29+ 已移除~~
  • VisualPath:二级虚拟目录路径,string 类型,需以 / 开头,结尾不包含 /Furion 4.8.8.20+ 支持
  • DisplayEmptyTriggerJobs:是否显示空作业触发器的作业,bool 类型,默认 trueFurion 4.9.2.39+ 支持
  • DisplayHead:是否显示页头,bool 类型,默认 trueFurion 4.9.2.39+ 支持
  • DefaultExpandAllJobs:是否默认展开所有作业,bool 类型,默认 falseFurion 4.9.2.40+ 支持

接着打开浏览器并访问 /schedule 地址即可:

前端源码地址:https://gitee.com/dotnetchina/Furion/tree/v4/clients/schedule-dashboard

26.1.17 常见问题

26.1.17.1 作业处理程序中获取当前时间存在偏差

通常我们会在 IJob 实现类型中获取当前时间,但是这个时间可能存在着极小的误差,如:

public class MyJob : IJob  
{  
    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)  
    {  
        var nowTime = DateTime.Now; // 此时的时间未必是真实触发时间,因为还包含创建线程,初始化等等时间  

        // 正确的做法是  
        var nowTime = context.OccurrenceTime;  
    }  
}  

26.1.17.2 作业触发器参数序列化/反序列化

框架提供了 Schedular 静态类方法:Serialize/Deserialize 可对作业触发器参数 object[] 类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如:

// 序列化,方便组合 `UI` 不同输入框作业触发器参数  
var args = new object[] { "* * * * * *", CronStringFormat.WithSeconds };  
var stringArgs = Schedular.Serialize(args);  

// 反序列化,方便拆开作业触发器参数在 `UI` 不同列展示  
var stringArgs = "[\"* * * * *\",0]";  
var args = Schedular.Deserialize<object[]>(stringArgs);  

26.1.17.3 作业信息额外数据序列化/反序列化

框架提供了 Schedular 静态类方法:Serialize/Deserialize 可对作业信息额外是数据 Dictionary<string, object> 类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如:

// 序列化,方便组合 `UI` 不同输入框作业信息额外数据  
var jobData = new Dictionary<string, object> { { "name", "Furion" } };  
var stringJobData = Schedular.Serialize(jobData);  

// 反序列化,方便拆开作业作业信息额外数据在 `UI` 不同列展示  
var stringJobData = "{\"name\":\"Furion\"}";  
var args = Schedular.Deserialize<Dictionary<string, object>>(stringJobData);  

26.1.17.4 作业处理程序延迟处理

在作业处理程序中如需使用到延迟线程操作,推荐使用 Task.Delay 而不是 Thread.Sleep,原因是后者是同步延迟会阻塞线程,而且不能取消。

26.1.17.5 定时任务部署说明

部署定时任务请确保服务器(操作系统)不会进入休眠且长时间运行。

如果在项目中使用了定时任务且部署到 IIS 中,那么需要设置 IIS 禁止回收,避免事件总线服务进入休眠,点击查看 IIS 回收问题解决方案

26.1.18 反馈与建议

与我们交流给 Furion 提 Issue