经过昨晚的调整,结果达到预期。
背景
我桌子这里浇花的A4工控机,里面时间比实际时间要慢5分钟,导致我设定每天9点开始的浇花动作,实际执行时间都变成了9点5分。例如我在9点2分登录A4,查看时间是这样:
从星尘代理节点性能数据也可以看到偏差297秒(A4浇花工控机):
从昨天星尘监控调用链,也可以看到A4在自己的9点执行,实际上服务器是9点5分。
解决方案
星尘AppClient有个GetNow()方法,提供基于服务器时间的本地时间。它决定于登录和每次心跳,都计算服务器的时间差Span。然后GetNow()的时候,本质上就是取本机时间加上时间差Span。
初期的想法,是给TimerX增加一个GetNow委托,配置星尘后,这个委托指向AppClient.GetNow()。TimerX内部根据Cron表达式计算下一次执行时间的时候,就用GetNow()来取代原来的DateTime.Now。实际写代码的时候,发现这个方案有点Low。想起新版dotNet好像有个时间提供者。
经查资料,时间提供者TimeProvider正合我意。里面的关键是GetUtcNow,默认返回DateTime.UtcNow。给TimerX加上TimeProvider并默认指向TimeProvider.System,也就是SystemTimeProvider。
在星尘里,初始化星尘工厂后,直接修改定时器调度器的时间提供者为新的StarTimeProvider,代码如下:
[MemberNotNullWhen(true, nameof(_client))]
private Boolean Valid()
{
//if (Server.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Server));
//if (AppId.IsNullOrEmpty()) throw new ArgumentNullException(nameof(AppId));
if (Server.IsNullOrEmpty() || AppId.IsNullOrEmpty()) return false;
if (_client == null)
{
//if (!AppId.IsNullOrEmpty()) _tokenFilter = new TokenHttpFilter
//{
// UserName = AppId,
// Password = Secret,
// ClientId = ClientId,
//};
var client = new AppClient(Server)
{
Factory = this,
AppId = AppId,
AppName = AppName,
Secret = Secret,
ClientId = ClientId,
NodeCode = Local?.Info?.Code,
//Filter = _tokenFilter,
//UseWebSocket = true,
Log = Log,
};
#if !NET40
// 设置全局定时调度器的时间提供者,借助服务器时间差,以获得更准确的时间。避免本地时间偏差导致定时任务执行时间不准确
TimerScheduler.GlobalTimeProvider = new StarTimeProvider { Client = client };
#endif
//var set = StarSetting.Current;
//if (set.Debug) client.Log = XTrace.Log;
client.WriteInfoEvent("应用启动", $"pid={Process.GetCurrentProcess().Id}");
_client = client;
InitTracer();
client.Tracer = _tracer;
client.Start();
// 注册StarServer环境变量,子进程共享
Environment.SetEnvironmentVariable("StarServer", Server);
}
return true;
}
星尘时间提供者如下:
internal class StarTimeProvider : TimeProvider
{
public ClientBase Client { get; set; } = null!;
public override DateTimeOffset GetUtcNow() => Client != null ? Client.GetNow().ToUniversalTime() : base.GetUtcNow();
}
发布修改版星尘,修改浇花程序A4Flower(源码),编译后,再通过星尘平台把程序发布到A4工控机里面。今天早上9点正,A4准时开始浇花(调用链),也即是开头的截图。
总结
星尘时间提供者里面的ClientBase,来自于NewLife.Remoting,那么这个时间提供者是否应该由Remoting提供?所以,实际上使用服务器时间做定时任务,并不一定需要依赖星尘,依赖其它基于Remoting框架的项目也是可以的,例如IoT网关,它就有这个需求,不要因本机时间偏差而导致Cron绝对定时器执行时间有偏差。
说改就改:
/// <summary>基于服务器时间差的时间提供者</summary>
public class ServerTimeProvider : TimeProvider
{
/// <summary>客户端</summary>
public ClientBase Client { get; set; } = null!;
/// <summary>获取UTC时间</summary>
/// <returns></returns>
public override DateTimeOffset GetUtcNow() => Client != null ? DateTime.UtcNow.Add(Client.Span) : base.GetUtcNow();
}
由于AppClient继承ClientBase,其GetNow()就来自于后者。因此ClientBase干脆直接提供时间差Span属性,在服务器时间差提供者里面,可以用DateTime.UtcNow.Add(Span),避免时间反复折算。
至此,使用TimeProvider完美解决Cron绝对定时时间不准的问题!