Appearance
调度作业
调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。
关于调度作业
使用场景
定时任务的应用场景非常广,几乎是每一个软件系统必备功能:
- 叫你起床的闹钟
- 日历日程提醒
- 生日纪念日提醒
- 定时备份数据库
- 定时清理垃圾数据
- 定时发送营销信息,邮件
- 定时上线产品,比如预售产品,双十一活动
- 定时发送优惠券
- 定时发布,实现 Devops 功能,如 Jenkins
- 定时爬虫抓数据
- 定时导出报表,历史统计,考勤统计
- ...
快速入门
- 定义作业处理程序
MyJob
:
cs
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
- 在
Startup.cs
注册Schedule
服务:
cs
services.AddSchedule(options =>
{
// 注册作业,并配置作业触发器
options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行
});
- 查看作业执行结果
bash
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()
方法并提供以下几种格式:
bash
# 持续运行格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 -> 下一次触发时间
# 触发停止格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 [触发器终止状态]
指定作业 Id
默认情况下,不指定作业 Id
会自动生成 job[编号]
。
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>("myjob", Triggers.Secondly());
});
查看作业执行结果:
bash
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
多个作业触发器
有时候,一个作业支持多种触发时间,比如 每分钟
执行一次,每 5秒
执行一次,每分钟第 3/7/8秒
执行一次。
cs
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 秒
});
查看作业执行结果:
cs
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
串行
执行
默认情况下,作业采用 并行
执行方式,也就是不会等待上一次作业执行完成,只要触发时间到了就自动执行,但一些情况下,我们可能希望等待上一次作业完成再执行,如:
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 串行,每秒执行
});
cs
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秒
}
}
查看作业执行结果:
cs
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
警告日志,如需关闭只需要:
cs
services.AddSchedule(options =>
{
options.LogEnabled = false;
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 每秒执行
});
查看作业执行结果:
bash
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
打印作业完整信息
框架提供了四种方式打印作业完整信息。
- 第一种:输出完整的作业
JSON
信息:context.ConvertToJSON()
cs
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;
}
}
查看作业打印结果:
json
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": "Penkar.Schedule.PeriodSecondsTrigger",
"assemblyName": "Penkar",
"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()
cs
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;
}
}
查看作业打印结果:
json
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": "Penkar.Schedule.PeriodSecondsTrigger",
"assembly_name": "Penkar",
"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()
cs
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;
}
}
查看作业打印结果:
sql
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 = 'Penkar.Schedule.PeriodSecondsTrigger',
assembly_name = 'Penkar',
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()
cs
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;
}
}
查看作业打印结果:
bash
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 ━━━━━━━━━━━
┣ Penkar.Schedule.PeriodSecondsTrigger
┣
┣ triggerId: job1_trigger1
┣ jobId: job1
┣ triggerType: Penkar.Schedule.PeriodSecondsTrigger
┣ assemblyName: Penkar
┣ 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 ━━━━━━━━━━━
运行时(动态)操作作业
有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业:
- 注册
services.AddSchedule()
服务
cs
// 可以完全动态操作,只需要注册服务即可
services.AddSchedule();
// 也可以部分静态,部分动态注册
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));
});
- 注入
ISchedulerFactory
服务
cs
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
public void AddJob()
{
_schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());
}
}
- 查看作业执行结果
bash
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
作业触发器特性
默认情况下,框架不会扫描 IJob
实现类的作业触发器特性,但可以设置作业的 IncludeAnnotations
进行启用。
- 启用
IncludeAnnotations
扫描
cs
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true)
, Triggers.PeriodSeconds(5)); // 这里可传可不传,传了则会自动载入特性和这里配置的作业触发器
// 还可以更简单~~
options.AddJob(typeof(MyJob).ScanToBuilder());
// 还可以批量新增 Penkar 4.8.2.4+
options.AddJob(App.EffectiveTypes.ScanToBuilders());
});
- 在
MyJob
中添加多个作业触发器特性
cs
[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;
}
}
- 查看作业执行结果
bash
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
HTTP
请求作业
HTTP
请求作业通常用于定时请求/访问互联网地址。
cs
services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
System.Net.Http.IHttpClientFactory
错误
如遇 Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate 'Penkar.Schedule.HttpJob'.
错误,请先注册 servces.AddHttpClient()
服务。
作业执行日志如下:
bash
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>
委托方式作业
有时我们需要快速开启新的定时作业但不考虑后续持久化存储(如数据库存储),这时可以使用委托作业方式,如:
cs
services.AddSchedule(options =>
{
// 和 IJob 的 ExecuteAsync 方法签名一致
options.AddJob((context, stoppingToken) =>
{
// 可通过 context.ServiceProvider 解析服务;框架提供了 .GetLogger() 拓展方法输出日志
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
});
作业执行日志如下:
bash
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
非 IOC/DI
项目中使用
在一些不支持依赖注入的项目类型如 Console、WinForm、WPF
中,可以通过以下方式使用:
- 方式一:无需获取其他服务对象
cs
_ = new ServiceCollection()
.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Period(5000));
})
.GetScheduleHostedService()
.StartAsync(new CancellationTokenSource().Token);
- 方式二:需要后续解析服务
cs
// 注册服务并构建
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
对象用类的静态属性存储起来即可,如:
cs
public class DI
{
public static IServiceProvider Services {get; set;}
}
之后通过 DI.Services = services;
即可,后续便可以通过 DI.Services.GetService<T>()
解析服务。
作业信息 JobDetail
及构建器
关于作业信息
框架提供了 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? | 作业更新时间 |
关于作业信息构建器
作业信息 JobDetail
是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 JobDetail
对象呢?
JobBuilder
是作业调度模块提供可用来生成运行时 JobDetail
的类型,这样做的好处可避免外部直接修改运行时 JobDetail
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 JobBuilder
对象。
- 通过
Create
静态方法创建
cs
// 根据作业 Id 创建
var jobBuilder = JobBuilder.Create("job1");
// 根据 IJob 实现类类型创建
var jobBuilder = JobBuilder.Create<MyJob>();
// 根据程序集名称和类型完全限定名(FullName)创建
var jobBuilder = JobBuilder.Create("YourProject", "YourProject.MyJob");
// 根据 Type 类型创建
var jobBuilder = JobBuilder.Create(typeof(MyJob));
// 通过委托创造动态作业
var jobBuilder = JobBuilder.Create((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});
- 通过
JobDetail
类型创建
这种方式常用于在运行时更新作业信息。
cs
var jobBuilder = JobBuilder.From(jobDetail);
//也可以通过以下方式
var jobBuilder = jobDetail.GetBuilder();
- 通过
JSON
字符串创建
该方式非常灵活,可从配置文件,JSON
字符串,或其他能够返回 JSON
字符串的地方创建。
cs
var jobBuilder = JobBuilder.From(@"{
""jobId"": ""job1"",
""groupName"": null,
""jobType"": ""MyJob"",
""assemblyName"": ""ConsoleApp13"",
""description"": null,
""concurrent"": true,
""includeAnnotations"": false,
""properties"": ""{}"",
""updatedTime"": null
}");
如果使用的是 .NET7
,可使用 """
避免转义,如:
cs
var jobBuilder = JobBuilder.From("""
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:00:59.390"
}
""");
关于属性名匹配规则
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
命名方式。
不支持 UnderScoreCase(下划线命名法)
,如 "include_annotations": true
- 还可以通过
Clone
静态方法从一个JobBuilder
创建
cs
var jobBuilder = JobBuilder.Clone(fromJobBuilder);
克隆说明
克隆操作只会克隆 AssemblyName
,JobType
,GroupName
,Description
,Concurrent
,IncludeAnnotations
,Properties
,DynamicExecuteAsync
(动态作业)。
- 不会克隆
JobId
,UpdatedTime
。
- 还可以通过
LoadFrom
实例方法填充当前的JobBuilder
比如可以传递匿名类型,类类型,字典 Dictionary<string, object>
类型:
cs
// 会覆盖所有相同的值
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(下划线命名法)
命名方式。
设置作业信息构建器
JobBuilder
提供了和 JobDetail
完全匹配的 Set[属性名]
方法来配置作业信息各个属性,如:
cs
services.AddSchedule(options =>
{
var jobBuilder = JobBuilder.Create<MyJob>()
.SetJobId("job1") // 作业 Id
.SetGroupName("group1") // 作业组名称
.SetJobType("Penkar.Application", "Penkar.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", "Penkar" } }) // 作业类型额外数据,支持多个重载,推荐!!!
.SetDynamicExecuteAsync((context, stoppingToken) => {
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}) // 动态委托处理程序,一旦设置了此委托,那么优先级将大于 MyJob 的 ExecuteAsync
;
options.AddJob(jobBuilder, Triggers.PeriodSeconds(5));
});
作业信息/构建器额外数据
有时候我们需要在作业运行的时候添加一些额外数据,或者实现多个触发器共享数据,经常用于 串行
执行中(并行
也同样工作),后面一个触发器需等待前一个触发器完成。
cs
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;
}
}
查看作业运行日志:
bash
info: 2022-12-03 23:16:46.5150228 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-03 23:16:46.5197497 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-03 23:16:46.6987703 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-03 23:16:46.7003295 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-03 23:16:46.7248216 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-03 23:16:51.7013640 +08:00 星期六 L MyJob[0] #8
count: 0 <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-03 23:16:51.663 -> 2022-12-03 23:16:56.656
info: 2022-12-03 23:16:56.6768044 +08:00 星期六 L MyJob[0] #9
count: 1 <job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-03 23:16:56.656 -> 2022-12-03 23:17:01.635
info: 2022-12-03 23:17:01.6454604 +08:00 星期六 L MyJob[0] #8
count: 2 <job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-03 23:17:01.635 -> 2022-12-03 23:17:06.608
info: 2022-12-03 23:17:06.6247917 +08:00 星期六 L MyJob[0] #6
count: 3 <job1> [C] <job1 job1_trigger1> 5s 4ts 2022-12-03 23:17:06.608 -> 2022-12-03 23:17:11.586
作业调度模块为 JobDetail
和 JobBuilder
提供了多个方法操作额外数据:
cs
// 查看所有额外数据
var properties = jobDetail.GetProperties();
// 查看单个额外数据,返回 object
var value = jobBuilder.GetProperty("key");
// 查看单个额外数据泛型
var value = jobDetail.GetProperty<int>("key");
// 添加新的额外数据,支持链式操作,如果键已存在,则跳过
jobDetail.AddProperty("key", "Penkar").AddProperty("key1", 2);
// 添加或更新额外数据,支持链式操作,不存在则新增,存在则替换,推荐
jobDetail.AddOrUpdateProperty("key", "Penkar").AddOrUpdateProperty("key1", 2);
// 还可以通过委托的方式:如果键不存在则插入 count = newValue,否则更新为 value(旧值)+1
jobDetail.AddOrUpdateProperty("count", newValue, value => value + 1);
// 删除某个额外数据,支持链式操作,如果 key 不存在则跳过
jobDetail.RemoveProperty("key").RemoveProperty("key1");
// 清空所有额外数据
jobDetail.ClearProperties();
作业额外数据类型支持
作业额外数据每一项的值只支持 int32
,string
,bool
,null
或它们组成的数组类型。
作业信息特性
作业信息特性 [JobDetail]
是为了方便运行时或启动时快速创建作业计划构建器而提供的,可在启动时或运行时通过以下方式创建,如:
cs
[JobDetail("job1", "这是一段描述")]
[PeriodSeconds(5, TriggerId = "trigger1")]
public class MyJob : IJob
{
}
- 启动
IncludeAnnotations
属性自动填充
cs
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>()
.SetIncludeAnnotations(true)); // 此时 [JobDetail] 配置的非空属性将自动复制给 JobBuilder,[PeriodSeconds] 也会自动创建 TriggerBuilder
});
- 手动扫描并创建作业计划构建器
cs
var schedulerBuilder = typeof(MyJob).ScanToBuilder();
- 通过程序集类型扫描批量创建作业计划构建器
也可以用于作业持久化 Preload
初始化时使用:
cs
public IEnumerable<SchedulerBuilder> Preload()
{
// 扫描所有类型并创建
return App.EffectiveTypes.Where(t => t.IsJobType())
.Select(t => t.ScanToBuilder());
// 还可以更简单~~
return App.EffectiveTypes.ScanToBuilders();
}
作业信息特性还提供了多个属性配置,如:
JobId
:作业信息 Id,string
类型GroupName
:作业组名称,string
类型Description
:描述信息,string
类型Concurrent
:是否采用并行执行,bool
类型,如果设置为false
,那么使用串行
执行
使用如下:
cs
[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
{
// ....
}
多种格式字符串输出
JobDetail
和 JobBuilder
都提供了多种将自身转换成特定格式的字符串。
- 转换成
JSON
字符串
cs
var json = jobDetail.ConvertToJSON();
字符串打印如下:
json
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-04 11:51:00.483"
}
- 转换成
SQL
字符串
cs
// 输出新增 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);
字符串打印如下:
sql
-- 新增语句
INSERT INTO tbName(
jobId,
groupName,
jobType,
assemblyName,
description,
concurrent,
includeAnnotations,
properties,
updatedTime
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp13',
NULL,
1,
0,
'{}',
'2022-12-04 11:53:05.489'
);
-- 删除语句
DELETE FROM tbName
WHERE JobId = 'job1';
-- 更新语句
UPDATE tbName
SET
job_id = 'job1',
group_name = NULL,
job_type = 'MyJob',
assembly_name = 'ConsoleApp13',
description = NULL,
concurrent = 1,
include_annotations = 0,
properties = '{}',
updated_time = '2022-12-04 11:53:05.489'
WHERE job_id = 'job1';
- 转换成
Monitor
字符串
cs
var monitor = jobDetail.ConvertToMonitor();
字符串打印如下:
bash
┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
┣ MyJob
┣
┣ jobId: job1
┣ groupName:
┣ jobType: MyJob
┣ assemblyName: ConsoleApp13
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022-12-04 11:55:11.186
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
- 简要字符串输出
cs
var str = jobDetail.ToString();
字符串打印如下:
bash
<job1> 这是一段描述 [C]
自定义 SQL
输出配置
cs
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(..)
会导致死循环。
启用作业执行日志输出
通常我们需要在 IJob
实现类中输出作业触发日志,如 _logger.LogInformation($"{context}");
cs
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
实现类都可能输出,所以提供了更便捷的配置,无需每一个 IJob
编写 _logger.LogInformation($"{context}");
。
配置启用如下:
cs
services.AddSchedule(options =>
{
options.JobDetail.LogEnabled = true; // 默认 false
});
之后 MyJob
可以更加精简了,日志类别自动设置为 MyJob
类型完整限定名。
cs
public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// 这里写业务逻辑即可,无需调用 _logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
作业执行日志如下:
bash
info: 2022-12-14 11:56:12.3963326 +08:00 星期三 L Penkar.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 Penkar.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 Penkar.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
作业处理程序 IJob
作业处理程序是作业符合触发时间执行的业务逻辑代码,通常由程序员编写,作业处理程序需实现 IJob
接口。
如何定义
cs
public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// your code...
}
}
JobExecutingContext
上下文
JobExecutingContext
上下文作为 ExecuteAsync
方法的第一个参数,包含以下运行时信息:
JobExecutingContext
属性列表JobId
:作业Id
TriggerId
:当前触发器Id
JobDetail
:作业信息Trigger
:作业触发器OccurrenceTime
:作业计划触发时间,最准确的记录时间ExecutingTime
:实际执行时间(可能存在误差)RunId
:本次作业执行唯一Id
,Penkar 4.8.5.1+
提供Result
:设置/读取本次作业执行结果,Penkar 4.8.7.7+
提供ServiceProvider
:服务提供器,Penkar 4.8.7.10+
提供
JobExecutingContext
方法列表.ConvertToJSON(naming)
:将上下文转换成JSON
字符串.ToString()
:输出为字符串
作业处理程序实例
默认情况下,作业处理程序会在作业触发器符合触发条件下通过 ActivatorUtilities.CreateInstance
动态创建,也就是每次触发都会创建新的 IJob
实例,如:
cs
var jobHandler = ActivatorUtilities.CreateInstance(_serviceProvider, jobType);
其中 _serviceProvider
是单例服务提供器,所以 IJob
实现类只能通过构造函数注入 单例服务
。如果没有范围作用域服务的需求,那么可以将 IJob
注册为单例服务,这样就可以避免每次重复创建 IJob
实例,对性能和减少内存占用有不小优化。 如:
cs
services.AddSingleton<YourJob>();
如果希望能够在构造函数注入范围作用域或瞬时作用域,可实现 IJobFactory
接口,如:
cs
using Penkar.Schedule;
using Microsoft.Extensions.DependencyInjection;
namespace Penkar.Application;
public class JobFactory : IJobFactory
{
public IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context)
{
return ActivatorUtilities.CreateInstance(serviceProvider, context.JobType) as IJob;
// 如果通过 services.AddSingleton<YourJob>(); 或 serivces.AddScoped<YourJob>(); 或 services.AddTransient<YourJob> 可通过下列方式
// return serviceProvider.GetRequiredService(context.JobType) as IJob;
}
}
之后注册 JobFactory
即可,如:
cs
services.AddSchedule(options =>
{
// 添加作业处理程序工厂
options.AddJobFactory<JobFactory>();
});
这样作业就可以注入范围和瞬时服务了。
依赖注入
实现 IJob
的作业处理程序类型默认注册为 单例
,那么只要是单例的服务,皆可以通过构造函数注入,如:ILogger<>
,IConfiguration
。
cs
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
- 如果是非
单例
的接口,如瞬时
或范围
服务,可通过IServiceScopeFactory
创建
推荐 IJobFactory
方式
通过上一小节 IJobFactory
统一实现。推荐。
cs
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_schedulerFactory = scopeFactory;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
using var serviceScope = _scopeFactory.CreateScope();
var repository = serviceScope.ServiceProvider.GetService<IRepository<User>>();
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
- 针对高频定时任务,比如每秒执行一次,或者更频繁的任务
推荐 IJobFactory
方式
通过上一小节 IJobFactory
统一实现。推荐。
为了避免频繁创建作用域和销毁作用域,可创建长范围的作用域。
cs
public class MyJob : IJob, IDisposable
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScope _serviceScope;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_serviceScope = scopeFactory.CreateScope();
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var repository = _serviceScope.ServiceProvider.GetService<IRepository<User>>();
var user = await repository.GetAsync(1);
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
public void Dispose()
{
_serviceScope?.Dispose();
}
}
动态作业 DynamicJob
框架提供了便捷的动态作业 DynamicJob
类型,可通过 Func<JobExecutingContext, CancellationToken, Task>
委托传入,无需创建 IJob
实现类型。
框架还为 JobExecutionContext
属性 ServiceProvder
提供了 .GetLogger()
拓展方法,方便快速获取 ILogger<System.Logging.DynamicJob>
日志对象实例。
cs
// 通过 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));
动态作业执行结果:
bash
info: 2022-12-04 12:26:18.6562296 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:26:18.6618404 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:26:18.8727764 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:26:18.8745765 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:26:18.9013540 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:26:23.8753926 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:26:23.837 -> 2022-12-04 12:26:28.835
info: 2022-12-04 12:26:28.8686474 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:26:28.835 -> 2022-12-04 12:26:33.823
info: 2022-12-04 12:26:33.8531796 +08:00 星期日 L System.Logging.DynamicJob[0] #13
<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:26:33.823 -> 2022-12-04 12:26:38.820
动态作业和普通作业的区别
- 动态作业处理程序类型是:
DynamicJob
类型 - 动态作业提供的
.GetLogger()
拓展输出日志类别是:System.Logging.DynamicJob
- 如果普通作业同时设置了
SetJobType
和SetDynamicExecuteAsync
,那么优先作为动态作业执行。 - 动态作业无法将
Func<..>
进行序列化持久化存储
使用 Roslyn
动态创建
按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 Roslyn
动态编译 C#
代码。
- 根据字符串创建
IJob
类型
cs
// 调用 Schedular 静态类提供的 CompileCSharpClassCode 方法
var jobAssembly = Schedular.CompileCSharpClassCode(@"
using Penkar.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");
- 注册作业
cs
// 可以在启动的时候添加
services.AddSchedule(options =>
{
options.AddJob(jobType
, Triggers.PeriodSeconds(5));
});
// 也可以完全在运行时添加(常用)
_schedulerFactory.AddJob(jobType
, Triggers.PeriodSeconds(5));
查看作业执行日志:
bash
info: 2022-12-04 12:38:00.6249410 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:38:00.6294089 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:38:00.7496005 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:38:00.7514579 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:38:00.7836777 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:38:05.7389682 +08:00 星期日 L YourProject.MyJob[0] #6
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:38:05.713 -> 2022-12-04 12:38:10.692
info: 2022-12-04 12:38:10.7108416 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:38:10.692 -> 2022-12-04 12:38:15.673
info: 2022-12-04 12:38:15.6925578 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:38:15.673 -> 2022-12-04 12:38:20.656
惊不惊喜,意外意外~。
小知识
通过 Roslyn
的方式支持创建 IJob
,JobDetail
,Trigger
,Scheduler
哦,自行测试。😊
作业执行异常处理
正常情况下,程序员应该保证作业执行程序总是稳定运行,但有时候会出现一些不可避免的意外导致出现异常,如网络异常等。
下面给出模拟出现异常和常见的处理方式例子:
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(3));
});
cs
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;
}
}
输出日志如下:
bash
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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 79
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 79
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
从上面执行日志可以看出,作业执行出现异常只会影响当前触发时间的执行,但不会影响下一次执行。出现这种情况通常配置 重试策略 确保每次作业处理程序可能执行成功,如重试 3
次,如:
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(3)
.SetNumRetries(3)); // 重试三次
});
输出日志如下:
bash
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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 91
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 102
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
全局配置重试策略
推荐使用 【作业执行器 IJobExecutor
】 配置全局异常重试策略。
作业执行异常回退策略
作业处理程序执行异常除了配置 重试次数
或配置 全局异常重试策略
以外,还可以实现 IJob.FallbackAsync
进行回退配置。
cs
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;
}
}
输出日志如下:
bash
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 Penkar.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 Penkar.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 Penkar.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 Penkar.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Penkar\samples\Penkar.Application\TestJob.cs:line 22
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 79
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\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 Penkar.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
发生二次异常,如:
cs
public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken)
{
Console.WriteLine("调用了回退");
throw new Exception("回退了我还是出异常~");
return Task.CompletedTask;
}
输出日志将合并前面所有异常并输出:
bash
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 Penkar.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 Penkar.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 Penkar.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 Penkar.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Penkar\samples\Penkar.Application\TestJob.cs:line 22
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 79
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\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 Penkar.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Penkar\samples\Penkar.Application\TestJob.cs:line 22
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 79
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 231
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---
---> (Inner Exception #1) System.Exception: 回退了我还是出异常~
at Penkar.Application.TestJob.FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Penkar\samples\Penkar.Application\TestJob.cs:line 32
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 309<---
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
warn: 2023-04-25 17:25:02.4212180 +08:00 星期二 L Penkar.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
作业调度器被取消处理
一般情况下,作业调度器意外关闭或手动关闭,但作业处理程序异步操作还未处理完成,这个时候我们可以选择取消还是继续执行,如果选择取消:
cs
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
如果未处理完成也会取消操作。
bash
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.
设置本次执行结果
有时候我们希望能够记录本次作业触发器触发返回结果,可通过 context.Result
进行设置。
也可以通过该值来判断作业是否成功执行,如设置了 Result
值但实际发现 trigger.Result
为 null
,那么也就是本次执行未成功。
cs
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}");
context.Result = "执行成功并返回 OK";
await Task.CompletedTask;
}
}
HTTP
请求作业
HTTP
请求作业通常用于定时请求/访问互联网地址。
cs
services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
作业执行日志如下:
bash
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
作业
默认情况下,Penkar
框架提供有限的 HTTP
配置参数,如果不能满足可自行定义。
- 自定义
Http
参数类:MyHttpJobMessage
cs
namespace YourProject.Core;
/// <summary>
/// HTTP 作业消息
/// </summary>
public class MyHttpJobMessage
{
/// <summary>
/// 请求地址
/// </summary>
public string RequestUri { get; set; }
/// <summary>
/// 请求方法
/// </summary>
public HttpMethod HttpMethod { get; set; } = HttpMethod.Get;
/// <summary>
/// 请求报文体
/// </summary>
public string Body { get; set; }
}
- 自定义
Http
作业处理程序:MyHttpJob
cs
/// <summary>
/// HTTP 请求作业处理程序
/// </summary>
public class MyHttpJob : IJob // 也可以继承内部的 HttpJob 类
{
/// <summary>
/// <see cref="HttpClient"/> 创建工厂
/// </summary>
private readonly IHttpClientFactory _httpClientFactory;
/// <summary>
/// 日志服务
/// </summary>
private readonly ILogger<MyHttpJob> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="httpClientFactory"><see cref="HttpClient"/> 创建工厂</param>
/// <param name="logger">日志服务</param>
public MyHttpJob(IHttpClientFactory httpClientFactory
, ILogger<MyHttpJob> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// 具体处理逻辑
/// </summary>
/// <param name="context">作业执行前上下文</param>
/// <param name="stoppingToken">取消任务 Token</param>
/// <returns><see cref="Task"/></returns>
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;
// 解析 HTTP 请求参数,键名称为类名
var httpJobMessage = Penetrates.Deserialize<MyHttpJobMessage>(jobDetail.GetProperty<string>(nameof(MyHttpJob)));
// 空检查
if (httpJobMessage == null || string.IsNullOrWhiteSpace(httpJobMessage.RequestUri))
{
return;
}
// 创建请求客户端
using var httpClient = _httpClientFactory.CreateClient(); // CreateClient 可以传入一个字符串进行全局配置 Client
// 添加请求报文头 User-Agent
httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47");
// 创建请求对象
var httpRequestMessage = new HttpRequestMessage(httpJobMessage.HttpMethod, httpJobMessage.RequestUri);
// 添加请求报文体,默认只支持发送 application/json 类型
if (httpJobMessage.HttpMethod != HttpMethod.Get
&& httpJobMessage.HttpMethod != HttpMethod.Head
&& !string.IsNullOrWhiteSpace(httpJobMessage.Body))
{
var stringContent = new StringContent(httpJobMessage.Body, Encoding.UTF8);
stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
httpRequestMessage.Content = stringContent;
}
// 更多自定义参数========================
// Your Code ....
// 发送请求并确保成功
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, stoppingToken);
// 解析返回值
var bodyString = await httpResponseMessage.Content.ReadAsStringAsync(stoppingToken);
// 输出日志
_logger.LogInformation($"Received HTTP response body with a length of <{bodyString.Length}> output as follows - {(int)httpResponseMessage.StatusCode}{Environment.NewLine}{bodyString}");
// 设置本次执行结果
context.Result = Penetrates.Serialize(new
{
httpResponseMessage.StatusCode,
Body = bodyString
});
}
}
- 注册自定义
Http
作业
cs
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));
});
设置本次执行结果
在一些特定作业处理程序中,有时候希望输出(记录)本次作业一些返回值,此时可通过 context.Result
实现,如:
cs
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
context.Result = "设置本次执行的值";
return Task.CompletedTask;
}
}
Result
和 Properties
除了通过 context.Result
设置作业本次执行结果以外,还可以通过 jobDetail.AddOrUpdateProperty(key, value)
的方式设置。区别在于前者会将值同步到 Trigger
的 Result
中,后者会将值同步在 JobDetail
的 Properties
中。
作业触发器 Trigger
及构建器
关于作业触发器
框架提供了 Trigger
类型来描述作业具体的触发时间,Trigger
类型提供以下只读属性:
属性名 | 属性类型 | 默认值 | 说明 |
---|---|---|---|
TriggerId | string | 作业触发器 Id | |
JobId | string | 作业 Id | |
TriggerType | string | 作业触发器类型,存储的是类型的 FullName | |
AssemblyName | string | 作业触发器类型所在程序集,存储的是程序集 Name | |
Args | string | 作业触发器参数,运行时将反序列化为 object[] 类型并作为构造函数参数 | |
Description | string | 描述信息 | |
Status | TriggerStatus | Ready | 作业触发器状态 |
StartTime | DateTime? | 起始时间 | |
EndTime | DateTime? | 结束时间 | |
LastRunTime | DateTime? | 最近运行时间 | |
NextRunTime | DateTime? | 下一次运行时间 | |
NumberOfRuns | long | 0 | 触发次数 |
MaxNumberOfRuns | long | 0 | 最大触发次数,0 :不限制,n :N 次 |
NumberOfErrors | long | 0 | 出错次数 |
MaxNumberOfErrors | long | 0 | 最大出错次数,0 :不限制,n :N 次 |
NumRetries | int | 0 | 重试次数 |
RetryTimeout | int | 1000 | 重试间隔时间,毫秒单位 |
StartNow | bool | true | 是否立即启动 |
RunOnStart | bool | false | 是否启动时执行一次 |
ResetOnlyOnce | bool | true | 是否在启动时重置最大触发次数等于一次的作业 |
Result | string | 本次执行返回结果,Penkar 4.8.7.7+ | |
ElapsedTime | long | 0 | 本次执行耗时,单位 ms ,Penkar 4.8.7.7+ |
UpdatedTime | DateTime? | 作业触发器更新时间 |
作业触发器状态
作业触发器状态指示了当前作业触发器的状态,使用 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 |
关于作业触发器构建器
作业触发器 Trigger
是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 Trigger
对象呢?
TriggerBuilder
是作业调度模块提供可用来生成运行时 Trigger
的类型,这样做的好处可避免外部直接修改运行时 Trigger
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 TriggerBuilder
对象。
- 通过
Create
静态方法创建
cs
// 根据作业触发器 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("Penkar", "Penkar.Schedule.PeriodTrigger");
// 根据程序集名称和类型完全限定名(FullName) + 构造函数参数创建
var triggerBuilder = TriggerBuilder.Create("Penkar", "Penkar.Schedule.PeriodTrigger", 1000);
// 根据 Type 类型创建
var triggerBuilder = TriggerBuilder.Create(typeof(PeriodTrigger));
// 根据 Type 类型 + 构造函数参数创建
var triggerBuilder = TriggerBuilder.Create(typeof(CronTrigger), "* * * * *", CronStringFormat.Default);
- 通过
Trigger
类型创建
这种方式常用于在运行时更新作业触发器。
cs
var triggerBuilder = TriggerBuilder.From(trigger);
//也可以通过以下方式
var triggerBuilder = trigger.GetBuilder();
- 通过
JSON
字符串创建
该方式非常灵活,可从配置文件,JSON
字符串,或其他能够返回 JSON
字符串的地方创建。
cs
var triggerBuilder = Triggers.From(@"
{
""triggerId"": ""job1_trigger1"",
""jobId"": ""job1"",
""triggerType"": ""Penkar.Schedule.CronTrigger"",
""assemblyName"": ""Penkar"",
""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
,可使用 """
避免转义,如:
cs
var triggerBuilder = Triggers.From("""
{
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Penkar.Schedule.CronTrigger",
"assemblyName": "Penkar",
"args": "[\"* * * * *\",0]",
"description": null,
"status": 8,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-12-04 16:13:00.000",
"nextRunTime": null,
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"result": null,
"elapsedTime": 100,
"updatedTime": "2022-12-04 16:13:00.045"
}
""");
关于属性名匹配规则
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
命名方式。
不支持 UnderScoreCase(下划线命名法)
,如 "include_annotations": true
- 还可以通过
Clone
静态方法从一个TriggerBuilder
创建
cs
var triggerBuilder = TriggerBuilder.Clone(fromTriggerBuilder);
克隆说明
克隆操作只会克隆 AssemblyName
,TriggerType
,Args
,Description
,StartTime
,EndTime
,MaxNumberOfRuns
,MaxNumberOfErrors
,NumRetries
,RetryTimeout
,StartNow
,RunOnStart
,ResetOnlyOnce
。
不会克隆 TriggerId
,JobId
,Status
,LastRunTime
,NextRunTime
,NumberOfRuns
,NumberOfErrors
,Result
,ElapsedTime
,PersistentConnectionUpdatedTime
。
- 还可以通过
LoadFrom
实例方法填充当前的TriggerBuilder
比如可以传递匿名类型,类类型,字典 Dictionary<string, object>
类型:
cs
// 会覆盖所有相同的值
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(下划线命名法)
命名方式。
内置作业触发器构建器
为了方便快速实现作业触发器,作业调度模块内置了 Period(间隔)
和 Cron(表达式)
作业触发器,可通过 TriggerBuilder
类型或 Triggers
静态类创建。
TriggerBuilder
方式
c
// 创建毫秒周期(间隔)作业触发器构建器
var triggerBuilder = TriggerBuilder.Period(5000);
// 创建 Cron 表达式作业触发器构建器
var triggerBuilder = TriggerBuilder.Cron("* * * * *", CronStringFormat.Default);
Triggers
方式,❤️ 推荐
Triggers
静态类具备 TriggerBuilder
所有的静态方法同时还添加了不少更加便捷的静态方法。
cs
// 间隔 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);
自定义作业触发器
除了使用框架提供的 PeriodTrigger
和 CronTrigger
以外,还可以自定义作业触发器,只需要继承 Trigger
并重写 GetNextOccurrence
方法即可,如实现一个间隔两秒的作业触发器。
cs
public class CustomTrigger : Trigger
{
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(2);
}
}
之后可通过 TriggerBuilder.Create
或 Triggers.Create
创建即可:
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<CustomTrigger>());
});
查看作业执行结果:
bash
info: 2022-12-04 17:19:25.0980531 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:19:25.1027083 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:19:25.2702054 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 17:19:25.2723418 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:19:25.2999295 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 17:19:27.2849015 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> 1ts 2022-12-04 17:19:27.234 -> 2022-12-04 17:19:29.232
info: 2022-12-04 17:19:29.2604639 +08:00 星期日 L MyJob[0] #4
<job1> [C] <job1 job1_trigger1> 2ts 2022-12-04 17:19:29.232 -> 2022-12-04 17:19:31.225
info: 2022-12-04 17:19:31.2422514 +08:00 星期日 L MyJob[0] #10
<job1> [C] <job1 job1_trigger1> 3ts 2022-12-04 17:19:31.225 -> 2022-12-04 17:19:33.207
另外,自定义作业触发器还支持配置构造函数参数
参数特别说明
如果自定义作业触发器包含参数,那么必须满足以下两个条件:
- 参数必须通过唯一的构造函数传入,有且最多只能拥有一个构造函数
- 参数的类型只能是
int
,string
,bool
,null
或由它们组成的数组类型
cs
public class CustomTrigger : Trigger
{
public CustomTrigger(int seconds) // 可支持多个参数
{
Seconds = seconds;
}
private int Seconds { get; set; }
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}
}
之后可通过 TriggerBuilder.Create
或 Triggers.Create
创建并传入参数。
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<CustomTrigger>(3));
});
查看作业执行结果:
bash
info: 2022-12-04 17:23:09.3029251 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:23:09.3205593 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:23:09.7081119 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 17:23:09.7506504 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:23:09.9380816 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 17:23:12.6291716 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1 job1_trigger1> 1ts 2022-12-04 17:23:12.590 -> 2022-12-04 17:23:15.582
info: 2022-12-04 17:23:15.6141563 +08:00 星期日 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> 2ts 2022-12-04 17:23:15.582 -> 2022-12-04 17:23:18.572
info: 2022-12-04 17:23:18.5857464 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> 3ts 2022-12-04 17:23:18.572 -> 2022-12-04 17:23:21.551
自定义作业触发器除了可重写 GetNextOccurrence
方法之后,还提供了 ShouldRun
和 ToString
方法可重写,如:
cs
public class CustomTrigger : Trigger
{
public CustomTrigger(int seconds)
{
Seconds = seconds;
}
private int Seconds { get; set; }
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}
public override bool ShouldRun(JobDetail jobDetail, DateTime startAt)
{
// 在这里进一步控制,如果返回 false,则作业触发器跳过执行
return base.ShouldRun(jobDetail, startAt);
}
public override string ToString()
{
return $"<{TriggerId}> 自定义递增 {Seconds}s 触发器";
}
}
推荐重写 GetNextRunTime
和 ToString
方法即可,如:
cs
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;
}
}
查看作业执行结果:
bash
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
作业触发器特性及自定义
如果 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 号(午夜)开始作业触发器特性
使用如下:
cs
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());
// 还可以批量扫描 Penkar 4.8.2.4+
options.AddJob(App.EffectiveTypes.ScanToBuilders());
});
cs
[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;
}
}
查看作业执行结果:
bash
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
除了使用内置特性,我们还可以自定义作业触发器特性,如:
cs
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class CustomAttribute : TriggerAttribute
{
public CustomAttribute(int seconds)
: base(typeof(CustomTrigger), seconds)
{
}
}
自定义作业触发器必备条件
- 必须继承
TriggerAttribute
特性类 - 至少包含一个构造函数且通过基类构造函数配置
:base(实际触发器类型, 构造函数参数)
- 推荐添加
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
特性
使用如下:
cs
[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;
}
}
查看作业执行结果:
bash
info: 2022-12-04 17:44:12.2702884 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 17:44:12.2872399 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 17:44:12.5730241 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 17:44:12.5751444 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 17:44:12.6174459 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 17:44:15.5850848 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:15.537 -> 2022-12-04 17:44:18.542
info: 2022-12-04 17:44:18.5693881 +08:00 星期日 L MyJob[0] #8
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:18.542 -> 2022-12-04 17:44:21.527
info: 2022-12-04 17:44:21.5396428 +08:00 星期日 L MyJob[0] #6
<job1> [C] <job1_trigger1> 自定义递增 3s 触发器 2022-12-04 17:44:21.527 -> 2022-12-04 17:44:24.504
作业触发器特性还提供了多个属性配置,如:
TriggerId
:作业触发器Id
,string
类型Description
:描述信息,string
类型StartTime
:起始时间,string
类型EndTime
:结束时间,string
类型MaxNumberOfRuns
:最大触发次数,long
类型,0
:不限制;n
:N 次MaxNumberOfErrors
:最大出错次数,long
类型,0
:不限制;n
:N 次NumRetries
:重试次数,int
类型,默认值0
RetryTimeout
:重试间隔时间,int
类型,默认值1000
StartNow
:是否立即启动,bool
类型,默认值true
RunOnStart
:是否启动时执行一次,bool
类型,默认值false
ResetOnlyOnce
:是否在启动时重置最大触发次数等于一次的作业,bool
类型,默认值true
使用如下:
cs
[PeriodSeconds(5, TriggerId = "trigger1", Description = "这是一段描述")]
public class MyJob : IJob
{
// ...
}
设置作业触发器构建器
TriggerBuilder
提供了和 Trigger
完全匹配的 Set[属性名]
方法来配置作业触发器各个属性,如:
cs
services.AddSchedule(options =>
{
var triggerBuilder = Triggers.Period(5000)
.SetTriggerId("trigger1") // 作业触发器 Id
.SetTriggerType("Penkar", "Penkar.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("本次返回结果") // 作业触发器本次执行返回结果,Penkar 4.8.7.7+
.SetElapsedTime(100) // 作业触发器本次执行耗时,Penkar 4.8.7.7+
;
options.AddJob<MyJob>(triggerBuilder);
});
作业触发器持久化方法
作业触发器构建器 TriggerBuilder
提供了三个标记作业持久化行为的方法:
Appended()
:标记作业触发器构建器是新增的,届时生成的SQL
是INSERT
语句Updated()
:标记作业触发器构建器已被更新,届时生成的SQL
是Updated
语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中Removed()
:标记作业触发器构建器已被删除,届时生成的SQL
是Deleted
语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至作业计划中
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(
Triggers.PeriodSeconds(5).SetTriggerId("trigger1").Appended()
, Triggers.PeriodSeconds(5).SetTriggerId("trigger2").Updated()
, Triggers.PeriodSeconds(5).SetTriggerId("trigger3").Removed());
});
查看作业调度器初始化日志:
bash
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
多种格式字符串输出
Trigger
和 TriggerBuilder
都提供了多种将自身转换成特定格式的字符串。
- 转换成
JSON
字符串
cs
var json = trigger.ConvertToJSON();
字符串打印如下:
json
{
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Penkar.Schedule.PeriodTrigger",
"assemblyName": "Penkar",
"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"
}
- 转换成
SQL
字符串
cs
// 输出新增 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);
字符串打印如下:
sql
-- 新增操作
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',
'Penkar.Schedule.PeriodTrigger',
'Penkar',
'[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 = 'Penkar.Schedule.PeriodTrigger',
assembly_name = 'Penkar',
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';
- 转换成
Monitor
字符串
cs
var monitor = trigger.ConvertToMonitor();
字符串打印如下:
bash
┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━
┣ Penkar.Schedule.PeriodTrigger
┣
┣ triggerId: job1_trigger1
┣ jobId: job1
┣ triggerType: Penkar.Schedule.PeriodTrigger
┣ assemblyName: Penkar
┣ 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 ━━━━━━━━━━━
- 简要字符串输出
cs
var str = trigger.ToString();
字符串打印如下:
bash
<job1 job1_trigger1> 5s 这是一段描述 1ts
自定义 SQL
输出配置
cs
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(..)
会导致死循环。
查看最近运行记录
GetTimelines()
方法,可获取内存中作业触发器最近运行的 5
条记录,如:
cs
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
更改作业触发器触发时间
如果需要更改作业触发器触发时间通常需要程序员自行组合 .SetTriggerType<TTrigger>()
和 .SetArgs(args)
,但在实际开发中代码非常不直观,所以提供了一些列的 .AlterTo
方法,如:
cs
// 间隔 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);
作业计划 Scheduler
及构建器
关于作业计划
所谓的作业计划(Scheduler
)是将作业信息(JobDetail
),作业触发器(Trigger
)和作业处理程序(IJob
)关联起来,并添加到作业调度器中等待调度执行。
作业计划(Scheduler
)类型对外是不公开的,但提供了对应的 IScheduler
接口进行操作。
关于作业计划构建器
作业计划 Scheduler
是框架提供运行时的内部只读类型,那么我们该如何创建或变更 Scheduler
对象呢?
SchedulerBuilder
是框架提供可用来生成运行时 Scheduler
的类型,这样做的好处可避免外部直接修改运行时 Scheduler
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 SchedulerBuilder
对象。
- 通过
Create
静态方法创建
cs
// 通过作业 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());
- 通过
IScheduler
接口创建
这种方式常用于在运行时更新作业信息。
cs
var schedulerBuilder = SchedulerBuilder.From(scheduler);
//也可以通过以下方式
var schedulerBuilder = scheduler.GetBuilder();
- 通过
JSON
字符串创建
该方式非常灵活,可从配置文件,JSON
字符串,或其他能够返回 JSON
字符串的地方创建。
cs
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"": ""Penkar.Schedule.PeriodTrigger"",
""assemblyName"": ""Penkar"",
""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
,可使用 """
避免转义,如:
cs
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": "Penkar.Schedule.PeriodTrigger",
"assemblyName": "Penkar",
"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
- 还可以通过
Clone
静态方法从一个SchedulerBuilder
创建
cs
var schedulerBuilder = SchedulerBuilder.Clone(fromSchedulerBuilder);
克隆说明
克隆操作将克隆 JobBuilder
和 TriggerBuilders
,同时持久化行为会被标记为 Appended
。
设置作业计划构建器
SchedulerBuilder
提供了多个方法操作 JobBuilder
和 TriggerBuilder
,如:
cs
// 获取作业信息构建器
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())
{
// ....
}
作业计划构建器持久化方法
作业计划构建器 SchedulerBuilder
提供了三个标记作业持久化行为的方法:
Appended()
:标记作业计划构建器是新增的,届时生成的SQL
是INSERT
语句Updated()
:标记作业计划构建器已被更新,届时生成的SQL
是Updated
语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中Removed()
:标记作业计划构建器已被删除,届时生成的SQL
是Deleted
语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至调度器中
cs
services.AddSchedule(options =>
{
options.AddJob(SchedulerBuilder.Create<MyJob>("job1").Appended()
, SchedulerBuilder.Create<MyJob>("job2").Updated()
, SchedulerBuilder.Create<MyJob>("job3").Removed());
});
查看作业调度器初始化日志:
bash
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.
多种格式字符串输出
Scheduler/IScheduler
和 SchedulerBuilder
都提供了多种将自身转换成特定格式的字符串。
- 转换成
JSON
字符串
cs
var json = schedulerBuilder.ConvertToJSON();
字符串打印如下:
json
{
"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": "Penkar.Schedule.PeriodTrigger",
"assemblyName": "Penkar",
"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"
}
]
}
作业调度器 ScheduleOptionsBuilder
配置选项
关于 ScheduleOptionsBuilder
ScheduleOptionsBuilder
配置选项主要是用来初始化作业调度器及相关服务配置的。只作为 services.AddSchedule
服务注册的配置参数,如:
cs
// 通过委托的方式配置
services.AddSchedule(options =>
{
// options 类型为 ScheduleOptionsBuilder
});
// 自行创建对象实例方式配置
var scheduleOptionsBuilder = new ScheduleOptionsBuilder();
services.AddSchedule(scheduleOptionsBuilder);
ScheduleOptionsBuilder
内置属性和方法
- 内置属性配置
cs
services.AddSchedule(options =>
{
// 是否使用 UTC 时间,该配置主要用来作为作业调度器检查时间格式的依据
options.UseUtcTimestamp = false;
// 是否输出作业调度器日志
options.LogEnabled = true;
// 配置集群 Id,默认值为启动程序集的名称
options.ClusterId = "cluster1";
// 配置输出 SQL 的数据库类型,Penkar 4.8.2.3+
options.BuildSqlType = SqlTypes.SqlServer;
// 配置作业信息 JobDetail 相关配置,如配置自定义 SQL 输出
options.JobDetail.ConvertToSQL((tableName, columnNames, jobDetail, behavior, naming) =>
{
});
// 启用作业执行日志输出
options.JobDetail.LogEnabled = true; // 默认 false
// 配置作业触发器 Trigger 相关配置,如配置自定义 SQL 输出
options.Trigger.ConvertToSQL((tableName, columnNames, trigger, behavior, naming) =>
{
});
// 定义未捕获的异常,通常是 Task 异常
options.UnobservedTaskExceptionHandler = (obj, args) =>
{
};
});
- 内置方法配置
cs
services.AddSchedule(options =>
{
// 添加作业
options.AddJob(schedulerBuilder);
options.AddJob(schedulerBuilder, schedulerBuilder1, ...); // Penkar 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,Penkar 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>();
});
作业监视器 IJobMonitor
调度作业服务提供了 IJobMonitor
监视器接口,实现该接口可以监视所有作业处理程序执行事件,包括 执行之前、执行之后,执行异常
。
通过作业监视器可以实现作业完整生命周期控制,还能实现作业执行异常发送短信或邮件通知管理员或项目维护者。
如添加 YourJobMonitor
:
cs
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
:
cs
services.AddSchedule(options =>
{
// 添加作业执行监视器
options.AddMonitor<YourJobMonitor>();
});
执行结果如下:
bash
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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 220
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 87
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 220
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 87
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 218
--- End of inner exception stack trace ---
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
还可以设置执行失败重试,如:
cs
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(5).SetNumRetries(3)); // 重试 3 次
options.AddMonitor<YourJobMonitor>();
});
执行结果如下:
bash
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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 220
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 99
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 110
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<<BackgroundProcessing>b__3>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 220
--- End of stack trace from previous location ---
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 99
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 110
at Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 218
--- End of inner exception stack trace ---
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
关于参数 JobExecutionContext
IJobMonitor
提供的 OnExecutingAsync
和 OnExecutedAsync
接口方法都包含一个 context
参数,前者是 JobExecutingContext
,后者是 JobExecutedContext
,它们都有一个共同的基类 JobExecutionContext
。
JobExecutionContext
提供了以下公共属性和公共方法:
JobExecutionContext
属性列表JobId
:作业Id
TriggerId
:当前触发器Id
JobDetail
:作业信息Trigger
:作业触发器OccurrenceTime
:作业计划触发时间,最准确的记录时间RunId
:本次作业执行唯一Id
,Penkar 4.8.5.1+
提供Result
:设置/读取本次作业执行结果,Penkar 4.8.7.7+
提供ServiceProvider
:服务提供器,Penkar 4.8.7.10+
提供
JobExecutionContext
方法列表.ConvertToJSON(naming)
:将作业计划转换成JSON
字符串.ToString()
:将作业执行信息输出为简要字符串
JobExecutingContext
在基类基础上拓展了ExecutingTime
属性:ExecutingTime
:执行前时间
JobExecutedContext
则在基类基础上拓展了ExecutedTime
和Exception
属性:ExecutedTime
:执行后时间Exception
:执行异常
作业执行器 IJobExecutor
调度作业服务提供了 IJobExecutor
执行器接口,可以让开发者自定义作业处理函数执行策略,如 超时控制,失败重试等等
。
实现重试策略
如添加 YourJobExecutor
:
cs
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
执行出错:
cs
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
:
cs
services.AddSchedule(options =>
{
// 添加作业执行器
options.AddExecutor<YourJobExecutor>();
});
执行结果如下:
bash
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 Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\FriendlyException\Retry.cs:line 99
at Penkar.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Penkar\framework\Penkar\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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 232
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
实现超时控制
如添加 YourJobExecutor
:
cs
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
执行超时:
cs
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 秒
}
}
执行结果如下:
bash
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 Penkar.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<<BackgroundProcessing>b__2>d.MoveNext() in D:\Workplaces\OpenSources\Penkar\framework\Penkar\Schedule\HostedServices\ScheduleHostedService.cs:line 234
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
关于 WaitAsync
说明
WaitAsync
是 .NET6+
新增的 Task
拓展方法,如需在 .NET5
中支持,可添加以下拓展:
cs
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.")
}
}
更多控制
作业执行器功能远不止于此,通过自定义作业执行器还可以实现分片作业,关联子作业,故障转移,集群等控制。
作业计划工厂 ISchedulerFactory
作业计划工厂提供了程序运行时操作作业调度器,作业计划等诸多方法。
ISchedulerFactory
被注册为 单例
服务,允许在任何可依赖注入的服务获取,如:
cs
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
// 也可以通过 App.GetService<ISchedulerFactory>() 获取
}
public void SomeMethod([FromServices]ISchedulerFactory schedulerFactory)
{
}
// .NET7+ 或 Penkar 4.8.0+
public void SomeMethod(ISchedulerFactory schedulerFactory)
{
}
}
查找所有作业
cs
// 查找所有作业,包括 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);
查找下一批触发的作业
cs
// 查找下一批触发的作业
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");
获取单个作业
cs
// 返回 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
保存作业
保存作业是框架提供强大且简单的方法,支持 新增
,编辑
,删除
作业,也就是三大操作都可以直接通过此方法直接操作。
cs
// 返回 ScheduleResult 类型
var scheduleResult = _schedulerFactory.TrySaveJob(schedulerBuilder, out var scheduler);
// 无返回值,支持多个
_schedulerFactory.SaveJob(schedulerBuilder1, schedulerBuilder2, ...)
关于保存作业的背后行为
默认情况下,保存作业需要传递 SchedulerBuilder
对象,这个对象可通过 GetJob(jobId)
获取,如:
cs
var schedulerBuilder = _schedulerFactory.GetJob("jobId")?.GetBuilder();
此时它的内部 Behavior
属性被标记为 PersistenceBehavior.Updated
,也就是更新状态,那么对于这个构建器的任何操作都会标记为 更新
操作。
如果通过 .Appended()
或 .Removed()
方法标记之后,那么它的操作行为就发生变化了。
- 如果被标记为
.Appended()
,那么它将进行新增
操作。如:
cs
schedulerBuilder.Appended();
- 如果被标记为
.Removed()
,那么它将进行删除
操作。如:
cs
schedulerBuilder.Removed();
比如以下的代码实则是 新增
或 删除
或 更新
操作:
cs
// 实际做新增操作
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
也具备相同的行为。
添加作业
框架提供了非常多的重载方法添加作业,如:
cs
// 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 作业,Penkar 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, ...);
更新作业
cs
// 返回 ScheduleResult 方式
var scheduleResult = _schedulerFactory.TryUpdateJob(schedulerBuilder, out var scheduler);
// 无返回值方式
_schedulerFactory.UpdateJob(schedulerBuilder1, schedulerBuilder2, ...);
删除作业
cs
// 返回 ScheduleResult 方式
var scheduleResult = _schedulerFactory.TryRemoveJob("job1", out var scheduler);
var scheduleResult = _schedulerFactory.TryRemoveJob(scheduler);
// 无返回值方式
_schedulerFactory.RemoveJob("job1", "job2", ...);
_schedulerFactory.RemoveJob(scheduler1, scheduler2, ...);
检查作业是否存在
cs
var isExist = _schedulerFactory.ContainsJob("job1");
// 还可以通过 group 查找
var isExist = _schedulerFactory.ContainsJob("job1", "group1");
启动所有作业
cs
_schedulerFactory.StartAll();
// 还可以通过 group 启动
_schedulerFactory.StartAll("group1");
暂停所有作业
cs
_schedulerFactory.PauseAll();
// 还可以通过 group 操作
_schedulerFactory.PauseAll("group1");
删除所有作业
cs
_schedulerFactory.RemoveAll();
// 还可以通过 group 操作
_schedulerFactory.RemoveAll("group1");
强制触发所有作业持久化操作
cs
_schedulerFactory.PersistAll();
// 还可以通过 group 操作
_schedulerFactory.PersistAll("group1");
校对所有作业
cs
_schedulerFactory.CollateAll();
// 还可以通过 group 操作
_schedulerFactory.CollateAll("group1");
强制唤醒作业调度器
正常情况下,作业调度器会自动管理 CPU
休眠和唤醒,但一些特殊情况下需要强制唤醒作业调度器(比如调度器假死了,被回收了...),可通过以下方式:
cs
_schedulerFactory.CancelSleep();
立即执行作业
cs
// 带返回值
var scheduleResult = _schedulerFactory.TryRunJob("job1");
// 不带返回值
_schedulerFactory.RunJob("job1");
注意事项
如果作业本身处于 (Pause)暂停
、(NotStart)初始化时未启动
、(Unoccupied)无触发时间
状态,那么点击 立即执行 后将自动转至 就绪
状态。
作业计划 IScheduler
作业计划 Scheduler
的默认实现接口是 IScheduler
,该接口主要用来操作当前(单个)作业。
获取 SchedulerModel
实例
获取 SchedulerModel
之后可直接访问 JobDetail
和 Trigger
对象。
cs
var schedulerModel = scheduler.GetModel();
获取 SchedulerBuilder
cs
var schedulerBuilder = scheduler.GetBuilder();
获取 JobBuilder
cs
var jobBuilder = scheduler.GetJobBuilder();
获取 TriggerBuilder
集合
cs
var triggerBuilders = scheduler.GetTriggerBuilders();
获取单个 TriggerBuilder
cs
var triggerBuilder = scheduler.GetTriggerBuilder("trigger1");
获取作业信息
cs
var jobDetail = scheduler.GetJobDetail();
获取作业触发器集合
cs
var triggers = scheduler.GetTriggers();
获取单个作业触发器
cs
// 返回 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
保存作业触发器
保存作业触发器是框架提供强大且简单的方法,支持 新增
,编辑
,删除
作业触发器,也就是三大操作都可以直接通过此方法直接操作。
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TrySaveTrigger(triggerBuilder, out var trigger);
// 无返回值,支持多个
scheduler.SaveTrigger(triggerBuilder1, triggerBuilder2, ...)
关于保存作业触发器的背后行为
默认情况下,保存作业触发器需要传递 TriggerBuilder
对象,这个对象可通过 GetTriggerBuilder(triggerId)
获取,如:
cs
var triggerBuilder = scheduler.GetTriggerBuilder("trigger1");
此时它的内部 Behavior
属性被标记为 PersistenceBehavior.Updated
,也就是更新状态,那么对于这个构建器的任何操作都会标记为 更新
操作。
如果通过 .Appended()
或 .Removed()
方法标记之后,那么它的操作行为就发生变化了。
- 如果被标记为
.Appended()
,那么它将进行新增
操作。如:
cs
triggerBuilder.Appended();
- 如果被标记为
.Removed()
,那么它将进行删除
操作。如:
cs
triggerBuilder.Removed();
比如以下的代码实则是 新增
或 删除
或 更新
操作:
cs
// 实际做新增操作
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() 方法
更新作业信息
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryUpdateDetail(jobBuilder, out var jobDetail);
// 无返回值
scheduler.UpdateDetail(jobBuilder);
// Penkar 4.8.6+ 支持
// 返回 ScheduleResult 类型
var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
}, out var jobDetail);
// Penkar 4.8.6+ 支持
// 无返回值
scheduler.UpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
});
添加作业触发器
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryAddTrigger(triggerBuilder, out var trigger);
// 无返回值,支持多个
scheduler.AddTrigger(triggerBuilder1, triggerBuilder2, ...);
更新作业触发器
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryUpdateTrigger(triggerBuilder, out var trigger);
// 无返回值,支持多个
scheduler.UpdateTrigger(triggerBuilder1, triggerBuilder2, ...);
// Penkar 4.8.6+ 支持
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
}, out var trigger);
// Penkar 4.8.6+ 支持
// 无返回值
scheduler.UpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
});
删除作业触发器
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryRemoveTrigger("trigger1", out var trigger);
// 无返回值,支持多个
scheduler.RemoveTrigger("trigger1", "trigger2", ...);
删除当前作业
cs
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryRemove();
// 无返回值
scheduler.Remove();
判断作业触发器是否存在
cs
bool isExist = scheduler.ContainsTrigger("trigger1");
启动作业触发器
cs
bool succeed = scheduler.StartTrigger("trigger1");
暂停作业触发器
cs
bool succeed = scheduler.PauseTrigger("trigger1");
强制触发作业持久化操作
cs
scheduler.Persist();
启动当前作业
cs
scheduler.Start();
暂停当前作业
cs
scheduler.Pause();
校对当前作业
cs
scheduler.Collate();
强制刷新当前作业
通常情况下我们通过 _schedulerFactory.GetJob("jobId")
获取到作业之后,然后对这个作业进行操作,但操作之后这个对象并不能同步更改,需要反复调用 GetJob
方法。。
IScheduler
任何操作都将自动调用 Reload()
方法刷新变量。
cs
// 也可以自己手动强制刷新(通常不需要调用下面代码~~)
scheduler.Reload();
转换成 JSON
格式
cs
var json = scheduler.ConvertToJSON();
var json = scheduler.ConvertToJSON(NamingConventions.CamelCase);
转换成可枚举字典
通常我们在开发应用时,需要将作业计划信息进行拆解,比如一个作业计划包含两个作业触发器,那么可以通过 scheduler.GetEnumerable()
方法生成可枚举字典对象,字典中的项数量等于作业触发器数量。
cs
foreach (var (jobDetail, trigger) in scheduler.GetEnumerable())
{
// ....
}
立即执行作业
cs
scheduler.Run();
注意事项
如果作业本身处于 (Pause)暂停
、(NotStart)初始化时未启动
、(Unoccupied)无触发时间
状态,那么点击 立即执行 后将自动转至 就绪
状态。
作业持久化器 IJobPersistence
关于作业持久化器
作业持久化器指的是可以通过存储介质如数据库中加载作业信息到内存中,又可以将内存中作业调度器的作业信息实时同步回存储介质中。
实现作业持久化器
调度作业服务提供了非常简单的 IJobPersistence
接口,只需实现该接口即可实现持久化,如实现数据库持久化:
cs
public class DbJobPersistence : IJobPersistence
{
public IEnumerable<SchedulerBuilder> Preload()
{
// 作业调度服务启动时运行时初始化,可通过数据库加载,或者其他方式
return Array.Empty<SchedulerBuilder>();
}
public SchedulerBuilder OnLoading(SchedulerBuilder builder)
{
// 如果是更新操作,则 return builder.Updated(); 将生成 UPDATE 语句
// 如果是新增操作,则 return builder.Appended(); 将生成 INSERT 语句
// 如果是删除操作,则 return builder.Removed(); 将生成 DELETE 语句
// 如果无需标记操作,返回 builder 默认值即可
return builder;
}
public void OnChanged(PersistenceContext context)
{
var sql = context.ConvertToSQL("job_detail");
// 这里执行 sql 即可 💖
}
public void OnTriggerChanged(PersistenceTriggerContext context)
{
var sql = context.ConvertToSQL("job_trigger");
// 这里执行 sql 即可 💖
}
}
之后在 Startup.cs
中注册:
cs
services.AddSchedule(options =>
{
options.AddPersistence<DbJobPersistence>();
});
可能有些开发者看到这里一脸不解,持久化不应该这么简单啊!其实就是这么简单....
IJobPersistence
详细说明
IJobPersistence
接口提供了以下四个方法:
Preload
:作业调度服务启动时调用,可在这里动态创建作业计划构建器并返回。
cs
public IEnumerable<SchedulerBuilder> Preload()
{
// 可以这里查询数据库返回
// 这里可以扫描程序集动态创建返回
return App.EffectiveTypes.Where(t => t.IsJobType())
.Select(t => SchedulerBuilder.Create(JobBuilder.Create(t), t.ScanTriggers()));
// 如果类型贴有 [JobDetail] 特性,还可以一键扫描返回
return App.EffectiveTypes.Where(t => t.IsJobType())
.Select(t => t.ScanToBuilder());
// 还可以更简单~~
return App.EffectiveTypes.ScanToBuilders();
// 也可以手动返回
return new[]
{
SchedulerBuilder.Create(JobBuilder.Create<MyJob>(), Triggers.Minutely())
}
}
OnLoading
:作业计划初始化通知,通常在这里进一步修改初始化作业计划构建器。
在作业调度器服务启动时会遍历程序中所有作业计划构建器,然后逐条调用该方法,开发者可以在这里进一步修改作业计划构建器数据,之后选择性返回 SchedulerBuilder
的持久化行为。
SchedulerBuilder
提供 Updated()
,Appended()
和 Removed()
来作为持久化行为标记。此标记将决定最终生成的 SQL
是 UPDATE
还是 INSERT
还是 UPDATE
。
cs
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 builder;
}
如果存储介质(如数据库)已经删除该作业,开发者可以标记为 Removed()
,这样该作业会从内存中移除。
cs
public SchedulerBuilder OnLoading(SchedulerBuilder builder)
{
// 比如这里根据 jobId 查询数据库已经确认数据不存在了
// 标记从其他地方移除
return builder.Removed();
}
如果存储介质(如数据库)新增了新作业但内存中不存在,开发者可以标记为 Append()
,这样该作业会添加到内存中,但原有的 builder
就会被丢弃。
cs
public SchedulerBuilder OnLoading(SchedulerBuilder builder)
{
// 比如在这里动态创建作业计划构建器
var newBuilder = SchedulerBuilder.Create<MyJob>(Triggers.Minutely());
// 还可以克隆一个
var newBuilder = SchedulerBuilder.Clone(builder);
// 还可以读取配置文件/JSON
var newBuilder = SchedulerBuilder.From(json);
// 返回新的作业计划构建器并标记为新增
return newBuilder.Appended();
}
OnChanged
:作业计划Scheduler
的JobDetail
变化时调用。
只要作业计划有任何变化都将触发该方法,该方法有一个 PersistenceContext
类型的参数 context
,PersistenceContext
包含以下成员:
PersistenceContext
属性列表JobId
:作业Id
,string
类型JobDetail
:作业信息,JobDetail
类型Behavior
:持久化行为,PersistenceBehavior
枚举类型,包含Appended
,Updated
和Removed
三个枚举成员
PersistenceContext
方法列表ConvertToSQL
:将PersistenceContext
转换成SQL
字符串,Behavior
属性值不同,生成的SQL
不同ConvertToJSON
:将PersistenceContext
转换成JSON
字符串ConvertToMonitor
:将PersistenceContext
转换成Monitor
字符串ToString
:将PersistenceContext
转换成简要
字符串GetNaming
:提供将特定字符串输出不同的命名规则字符串
cs
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 了!!! 😎
}
OnTriggerChanged
:作业计划Scheduler
的触发器Trigger
变化时调用。
只要作业计划触发器有任何变化都将触发该方法,该方法有一个 PersistenceTriggerContext
类型的参数 context
,PersistenceTriggerContext
继承自 PersistenceContext
:
PersistenceTriggerContext
属性列表JobId
:作业Id
,string
类型JobDetail
:作业信息,JobDetail
类型TriggerId
:作业触发器Id
,string
类型Trigger
:作业触发器,Trigger
类型Behavior
:持久化行为,PersistenceBehavior
枚举类型,包含Appended
,Updated
和Removed
三个枚举成员
PersistenceTriggerContext
方法列表ConvertToSQL
:将PersistenceTriggerContext
转换成SQL
字符串,Behavior
属性值不同,生成的SQL
不同ConvertToJSON
:将PersistenceTriggerContext
转换成JSON
字符串,只包含Trigger
ConvertAllToJSON
:将PersistenceTriggerContext
转换成JSON
字符串,包含JobDetail
和Trigger
ConvertToMonitor
:将PersistenceTriggerContext
转换成Monitor
字符串ToString
:将PersistenceTriggerContext
转换成简要
字符串GetNaming
:提供将特定字符串输出不同的命名规则字符串
cs
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
来生成特定数据库的语句,如:
cs
services.AddSchedule(options =>
{
// 配置输出 SQL 的数据库类型,Penkar 4.8.2.3+
options.BuildSqlType = SqlTypes.SqlServer;
});
作业集群控制
框架提供简单的集群功能,但并不能达到负载均衡的效果,而仅仅提供了故障转移的功能,当一个服务的作业调度器宕机时,另一个服务的作业调度器会启动。
实现集群故障转移
- 创建
JobClusterServer
类并实现IJobClusterServer
cs
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);
}
}
- 注册集群服务
cs
services.AddSchedule(options =>
{
options.ClusterId = "cluster1";
options.AddClusterServer<JobClusterServer>();
});
- 作业集群输出日志
bash
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
作业集群数据库表设计
只需包含 Id
,ClusterId
,Description
,Status
,UpdatedTime
字段即可,其中 Status
是 ClusterStatus
枚举类型。
ClusterStatus
包含以下枚举成员Crashed
:宕机Working
:正常工作Waiting
:等待被唤醒,默认值
如何实现负载均衡
框架只提供了简单的故障转移的集群功能,如需实现负载均衡,可通过 TCP/IP
套接字实现。
ScheduleServe
静态类
该功能 建议 仅限不能通过 services.AddXXX
方式使用,比如控制台,Winfrom/WPF
等。
cs
IDisposable dispose = ScheduleServe.Run(options =>
{
options.AddJob<MyJob>(Triggers.Secondly());
});
这种方式有一个隐藏的巨大隐藏 “骚操作”:可以在任何地方创建作业调度服务,多次调用可以创建多个作业调度器。
推荐使用 Serve.Run()
或 Serve.RunGeneric()
方式替代
Penkar
框架提供了 Serve.Run()
方式支持跨平台使用,还能支持注册更多服务,如:
cs
Serve.Run(services =>
{
services.AddSchedule(options =>
{
options.Add<MyJob>(Triggers.Secondly());
});
})
如无需 Web
功能,可通过 Serve.RunGeneric
替代 Serve.Run
。
如何部署
部署建议
Worker Service
代码集成例子
1. 安装 Penkar
或 Sundial
包
bash
# 完整的开发框架
dotnet add package Penkar;
# 只需要定时任务服务功能
dotnet add package Sundial
2. 注册 Schedule
服务
cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Penkar.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
模式,那么代码将非常精简,无需上面第二个步骤的代码~,如:
cs
Serve.RunGeneric(services =>
{
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(5));
});
})
Dashboard
看板功能
内置了一个嵌入的定时任务看板 UI
,只需要在 Startup.cs
中启用即可,如:
在 Sundial
中使用
如果使用的是 Sundial
独立开源项目,只需要安装 Sundial.Dashboard
包即可,无需安装 Sundial
,前者已添加了后者的引用。
cs
app.UseStaticFiles();
app.UseScheduleUI();
// 还可以配置生产环境关闭
app.UseScheduleUI(options =>
{
options.RequestPath = "/custom-job"; // 必须以 / 开头且不以 / 结尾
options.DisableOnProduction = true;
options.SyncRate = 300; // 控制看板刷新频率,默认 300ms,Penkar 4.8.7.43+ 支持
});
中间件说明
app.UseScheduleUI()
必须在 app.UseStaticFiles()
之后注册。
在 Worker Service
中注册
默认情况下,Worker Service
不提供 Web
功能,那么自然而然不能提供 Web
看板功能,如果想使其支持,可通过以下步骤:
- 添加日志配置(
appsettings.json
和appsettings.Development.json
)
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
}
- 注册中间件服务
cs
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();
看板配置选项
app.UseScheduleUI
提供了可选的 ScheduleUIOptions
配置选项,提供以下配置:
RequestPath
:配置看板入口地址,string
类型,默认/schedule
DisableOnProduction
:是否在生产环境关闭,bool
类型,默认false
SyncRate
:控制看板刷新频率,int
类型,默认300
,单位毫秒
,Penkar 4.8.7.43+
支持
接着打开浏览器并访问 /schedule
地址即可:
常见问题
作业处理程序中获取当前时间存在偏差
通常我们会在 IJob
实现类型中获取当前时间,但是这个时间可能存在着极小的误差,如:
cs
public class MyJob : IJob
{
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var nowTime = DateTime.Now; // 此时的时间未必是真实触发时间,因为还包含创建线程,初始化等等时间
// 正确的做法是
var nowTime = context.OccurrenceTime;
}
}
作业触发器参数序列化/反序列化
框架提供了 Schedular
静态类方法:Serialize/Deserialize
可对作业触发器参数 object[]
类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如:
cs
// 序列化,方便组合 `UI` 不同输入框作业触发器参数
var args = new object[] { "* * * * * *", CronStringFormat.WithSeconds };
var stringArgs = Schedular.Serialize(args);
// 反序列化,方便拆开作业触发器参数在 `UI` 不同列展示
var stringArgs = "[\"* * * * *\",0]";
var args = Schedular.Deserialize<object[]>(stringArgs);
作业信息额外数据序列化/反序列化
框架提供了 Schedular
静态类方法:Serialize/Deserialize
可对作业信息额外是数据 Dictionary<string, object>
类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如:
cs
// 序列化,方便组合 `UI` 不同输入框作业信息额外数据
var jobData = new Dictionary<string, object> { { "name", "Penkar" } };
var stringJobData = Schedular.Serialize(jobData);
// 反序列化,方便拆开作业作业信息额外数据在 `UI` 不同列展示
var stringJobData = "{\"name\":\"Penkar\"}";
var args = Schedular.Deserialize<Dictionary<string, object>>(stringJobData);
作业处理程序延迟处理
在作业处理程序中如需使用到延迟线程操作,推荐使用 Task.Delay
而不是 Thread.Sleep
,原因是后者是同步延迟会阻塞线程,而且不能取消。