Sundial(一)
Sundial 源码梳理 - v2.5.6
代码目录一览
通过入口点说明
实现IServiceCollection,并返回IServiceCollection(Extensions/ScheduleServiceCollectionExtensions.css 53行)
初始化作业调度构建器,并将构建器创建为服务
// 实例化ScheduleOptionsBuilder
scheduleOptionsBuilder ??= new ScheduleOptionsBuilder();
// 注册内部服务
services.AddInternalService(scheduleOptionsBuilder);
// AddInternalService方法中(ScheduleServiceCollectionExtensions.css 89行)
scheduleOptionsBuilder.Build(services);
/**
Build方法中添加了作业监视器,作业执行其和作业调度持久化和作业集群的依赖注入
通过AddSingleton(S, T)将接口和类注入到程序中,后期可通过services.BuildServiceProvider().GetService<T>()来获得服务对象
**/
// 注册作业调度器日志服务
services.AddSingleton<IScheduleLogger>(serviceProvider =>
{
//ActivatorUtilities.CreateInstance 主要实现带参的IOC注入,默认启用日志
var scheduleLogger = ActivatorUtilities.CreateInstance<ScheduleLogger>(serviceProvider
, scheduleOptionsBuilder.LogEnabled);
return scheduleLogger;
});
// 注册作业计划工厂服务
services.AddSingleton<ISchedulerFactory>(serviceProvider =>
{
//默认不使用 UTC 时间
var schedulerFactory = ActivatorUtilities.CreateInstance<SchedulerFactory>(serviceProvider
, schedulerBuilders
, scheduleOptionsBuilder.UseUtcTimestamp);
return schedulerFactory;
});
// 注册作业调度器后台主机服务
// 1.使用AddHostedService,服务会自动调用Worker.StartAsync方法
// 2.返回的方法需集成IHostedService或者BackgroundService
// 3.核心代码,主要用于周期的调度任务
// 4.核心方法
// ExecuteAsync:开始执行
// BackgroundProcessing: ExecuteAsync中的死循环
// CheckIsBlocked : 串行还是并行的判断
// Dispose方法 销毁
// 具体源代码分析参见下一章节(注册作业调度器后台主机服务 ScheduleHostedService.cs分析)
services.AddHostedService(serviceProvider =>
{
// 创建作业调度器后台主机对象
var scheduleHostedService = ActivatorUtilities.CreateInstance<ScheduleHostedService>(
serviceProvider
, scheduleOptionsBuilder.UseUtcTimestamp
, scheduleOptionsBuilder.ClusterId); // 订阅未察觉任务异常事件
var unobservedTaskExceptionHandler = scheduleOptionsBuilder.UnobservedTaskExceptionHandler;
if (unobservedTaskExceptionHandler != default)
{
scheduleHostedService.UnobservedTaskException += unobservedTaskExceptionHandler;
}
return scheduleHostedService;
});
注册作业调度器后台主机服务 ScheduleHostedService.cs分析
重载了StartAsync(服务启动),ExecuteAsync(执行任务),Dispose(销毁)
StartAsync
- 作业集群启动通知
// 实现IJobClusterServer接口
// 出处:https://furion.baiqian.ltd/docs/job/#26113-%E4%BD%9C%E4%B8%9A%E9%9B%86%E7%BE%A4%E6%8E%A7%E5%88%B6
public void Start(JobClusterContext context){
// 根据clusterId 判断,不存在新增,并将状态设置为ClusterStatus.Waiting
}
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);
}
Dispose
- 调用作业集群宕机通知(Crash方法)
ExecuteAsync
调用作业集群的WaitingForAsync方法参见IJobClusterServer中的WaitingForAsync
作业调度工厂进行初始化(SchedulerFactory.Preload)
/// 作业计划工厂默认实现类(内部服务)
/// 参见Factories/SchedulerFactory.Internal.cs 文件
internal sealed partial class SchedulerFactory : ISchedulerFactory{
// 作业调度初始化
public void Preload(){
// 输出作业调度度初始化日志
_logger.LogInformation("Schedule hosted service is preloading..."); // 标记是否初始化成功
var preloadSucceed = true; try
{
// 装载初始作业计划
var initialSchedulerBuilders = _schedulerBuilders.Concat(Persistence?.Preload() ?? Array.Empty<SchedulerBuilder>()); // 如果作业调度器中包含作业计划构建器
if (initialSchedulerBuilders.Any())
{
// 逐条遍历并新增到内存中
foreach (var schedulerBuilder in initialSchedulerBuilders)
{
_ = TrySaveJob(Persistence?.OnLoading(schedulerBuilder) ?? schedulerBuilder
, out _
, false);
}
}
}
catch (Exception ex)
{
preloadSucceed = false;
_logger.LogError(ex, "Schedule hosted service preload failed, and a total of <0> schedulers are appended.");
} // 标记当前方法初始化完成
PreloadCompleted = true; // 释放引用内存并立即回收GC
_schedulerBuilders.Clear();
GC.Collect(); // 输出作业调度器初始化日志
if (preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count);
}
}
开始监听(调用BackgroundProcessing方法)
参见BackgroundProcessing
释放作业计划工厂
- _schedulerFactory.Dispose();
BackgroundProcessing
该方法为异步任务方法:async Task BackgroundProcessing(CancellationToken stoppingToken)
方法中获取的检查时间为UTC时间或默认显示时间
查找所有该时间要触发的作业
创建一个任务工厂(TaskFactory)
Parallel.ForEach的方式并行触发每个符和条件的作业任务
遍历触发器 Triggers
判断是否为串行执行:CheckIsBlocked,如果是且还未完成则跳出
设置作业触发器为运行态,检查记录信息并设置下一个触发的时间
将作业和触发器放到作业计划工厂的BlockingCollection中
使用Parallel.For来提高并发
创建线程taskFactory.StartNew
创建上下文JobExecutingContext
通过await Monitor.OnExecutingAsync()方法进行作业执行前的任务监听(自然也有结束后的监听)
开始执行IJobExecutor执行器(默认执行自带的,也可以自己实现)
// 默认策略
// 调用作业处理程序并配置出错执行重试
await Retry.InvokeAsync(async () =>
{
await jobHandler.ExecuteAsync(jobExecutingContext, stoppingToken);
}
, trigger.NumRetries
, trigger.RetryTimeout
, retryAction: (total, times) =>
{
// 输出重试日志
_logger.LogWarning("Retrying {times}/{total} times for {jobExecutingContext}", times, total, jobExecutingContext);
});
// 执行器的重试策略
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);
});
}
}
在注册 Schedule 服务中注册 YourJobExecutor:
services.AddSchedule(options =>
{
// 添加作业执行器
options.AddExecutor<YourJobExecutor>();
});
检查作业的信息,如果都处于运行状态,则将触发器变为就绪状态
完成后做后续处理:作业完成,将作业信息的运行数据写入持久化中,写入执行日志
//运行数据写入持久化中
// 放入BlockingCollection<PersistenceContext>中
// 该队列使用了TaskCreationOptions.LongRunning 方法让线程长时间运行该任务
Task.Factory.StartNew(state => ((SchedulerFactory)state).ProcessQueue()
, this, TaskCreationOptions.LongRunning)
private void ProcessQueue(){
// GetConsumingEnumerable 遍历
foreach (var context in _persistenceMessageQueue.GetConsumingEnumerable())
{
try
{
// 作业触发器更改通知
if (context is PersistenceTriggerContext triggerContext)
{
Persistence.OnTriggerChanged(triggerContext);
}
// 作业信息更改通知
else Persistence.OnChanged(context);
}
catch (Exception ex)
{
if (context is PersistenceTriggerContext triggerContext) _logger.LogError(ex, "Persistence of <{TriggerId}> trigger of <{JobId}> job failed.", triggerContext.TriggerId, triggerContext.JobId);
else _logger.LogError(ex, "The JobDetail of <{JobId}> persist failed.", context.JobId);
}
}
}
// 作业持久化器 IJobPersistence
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 中注册: services.AddSchedule(options =>
{
options.AddPersistence<DbJobPersistence>();
});
异常处理
相关日志输出
// 记录错误信息,包含错误次数和运行状态
trigger.IncrementErrors(jobDetail, startAt); // 将作业触发器运行数据写入持久化
_schedulerFactory.Shorthand(jobDetail, trigger); // 输出异常日志
_logger.LogError(ex, "Error occurred executing {jobExecutingContext}.", jobExecutingContext); // 标记异常
executionException = new InvalidOperationException(string.Format("Error occurred executing {0}.", jobExecutingContext.ToString()), ex); // 捕获 Task 任务异常信息并统计所有异常
if (UnobservedTaskException != default)
{
var args = new UnobservedTaskExceptionEventArgs(
ex as AggregateException ?? new AggregateException(ex)); UnobservedTaskException.Invoke(this, args);
}
CheckIsBlocked
- 并行则直接返回false,执行下面操作
- 触发器没有就绪,则设置为就绪状态,然后返回false,执行下面操作
- 触发器已经就绪,则继续就绪状态,计算下次触发时间,返回true,跳出
未完待续
Sundial(一)的更多相关文章
- little alchemy攻略
一个造物游戏: acidrain=rain+smoke airlplain=metal+bird alcohol=fruit+time algae=plant+water allergy=dust+h ...
- January 11th, 2018 Week 02nd Thursday
Live, travel, adventure, bless, and don't be sorry. 精彩地活着,不停地前行,大胆冒险,心怀感激,不留遗憾. Everything we do is ...
- Xapian简明教程(未完成)
第一章 简介 1.1 简介 Xapian是一个开源的搜索引擎库,它可以让开发者自定义的开发一些高级的的索引和查找因素应用在他们的应用中. 通过阅读这篇文档,希望可以帮助你创建第一个你的索引数据库和了解 ...
- 【雅思】【绿宝书错词本】List13~24
List 13 ❤audacious a.大胆的:有冒险精神的:鲁莽的:厚颜无耻的 ❤tramp v.跋涉:踩踏 n.长途跋涉 ❤lexicographer n.词典编纂者 ❤manipulate v ...
- 深度学习之加载VGG19模型分类识别
主要参考博客: https://blog.csdn.net/u011046017/article/details/80672597#%E8%AE%AD%E7%BB%83%E4%BB%A3%E7%A0% ...
- List of Mozilla-Based Applications
List of Mozilla-Based Applications The following is a list of all known active applications that are ...
- notes 摘自陶哲轩演讲
摘自陶哲轩演讲http://www.youku.com/playlist_show/id_5267259.htmlA frog in a well 井底之蛙 Aristotle 亚里士多 ...
- ImageNet2017文件下载
ImageNet2017文件下载 文件说明 imagenet_object_localization.tar.gz包含训练集和验证集的图像数据和地面实况,以及测试集的图像数据. 图像注释以PASCAL ...
- ImageNet2017文件介绍及使用
ImageNet2017文件介绍及使用 文件说明 imagenet_object_localization.tar.gz包含训练集和验证集的图像数据和地面实况,以及测试集的图像数据. 图像注释以PAS ...
- Java 框架、库和软件的精选列表(awesome java)
原创翻译,原始链接 本文为awesome系列中的awesome java Awesome Java Java 框架.库和软件的精选列表 项目 Bean映射 简化 bean 映射的框架 dOOv - 为 ...
随机推荐
- 详解AQS中的condition源码原理
摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...
- Python基础之模块:3、os模块 sys模块 json模块 json实战
目录 一.os模块 1.创建目录 2.删除目录 3.查看指定路径下目录内容 4.删除/重命名文件 5.获取/切换当前目录 6.动态获取项目根路径 7.拼接/切割路径 8.判断文件.目录是否存在 9.判 ...
- pinpoint:初始化hbase数据库
安装完成hbase之后,需要初始化hbase的pinpoint库(创建表). 1. 登录数据库 [root@monitor default]# cd /home/pinpoint/hbase-1.7. ...
- NC-UClient下载安装应用详解
aliases: [] tags : " #NC " summary: [UClient下载安装NCC应用详解] product: [NCX] author : [yaenli] ...
- Python的几种lambda排序方法
1.对单个变量进行排序 #lst = [[5,8],[5,3],[3,1]] lst.sort(key = lambda x : x[1]) #lst = [[3,1],[5,8],[5,3]] 以元 ...
- Day10:for循环结构的使用详解
for循环 将0~100内的奇.偶数分别求和 思路 第一步先将0~100以内的奇.偶数分成两队,第二步使奇数累加.ou'shu public class ForCirculate{ public st ...
- 2022春每日一题:Day 32
题目:[USACO12DEC]First! G 不太记得当时怎么想的了,但是显然,当一个字符串的前缀存在则他一定不是first,然后做法:对于每个字符串,把每个字符结尾跟他有相同前缀的单词的同元素建边 ...
- Vue2基本组件间通信
Vue2组件通信的基础方式 自己的理解:组件化通信,无非就是数据你传我,我传你,两个组件的相互交流,方法很多,下方有图示(此篇建议小白阅读,大神的话也不会看,哈哈哈哈!仅供参考,有不同的意见可以一起交 ...
- javaweb string
今天遇到一个跨域请求jsonp格式报错,其原因是其中一个参数过从我方数据库取出就带有换行格式的,类似于: 这条数据竟然自带格式换行. 而我们现常用的trim()只能去掉字符串的头部和尾部的空格, 而要 ...
- 【Java SE】Day07 API、Scanner类、Random类、ArrayList类
一.API 1.概述: API(Application Programming Interface),应用程序编程接口 Java API:程序员的字典,是类的说明文档 2.使用步骤 帮助文档:JDK_ ...