很多人问起,为什么 skynet 的时钟不随系统时钟变更。我的答案是,skynet 系统保证内部的计时机构是单调递增的,有了这个约束,用起来可以回避很多问题。
那么怎么做一个定时任务,好像 crontab 那样,而且可以随系统时间调整而变化。我的答案是,这是个纯业务层的需求,你爱怎么做怎么做,根据你的需求(业务量多少,定时是否频繁等)可以有不同的实现方式。但是总而言之,你需要在 skynet 里定制一个服务,可以按时驱动某种消息。而这个服务的定时机构,不应该完全依赖于 skynet 自己的内部时钟。
如果你的任务并不多,我可以描述一个最简单的实现方法:
在这个服务中,可以维系一张无序表,里面放着未来什么时候将触发什么任务。这些任务是其它位置注册过来的,任务只需要是一个独立的字符串即可,也可以是一个任务名带一个时间,比如 “9 点领奖” 这样。到点可以把这个串发送给订阅者。
串中带上时间的好处是,万一哪个环节出了问题,订阅方可以自己去重,丢弃那些已经做过的任务。
这样使用的流程就是,任务管理器向此服务注册一系列任务,约定每个任务的串。任务执行者去这个服务订阅对应的串,一旦到时间,就会获得通知。
这个服务可以使用一个 skynet 内置的 timeout 回调来触发任务。每个注册的新任务都比较一下历史所有任务,如果时间更进则注册一个新的 skynet.timeout 回调。而每次收到 skynet.timeout 提醒时,都检查一下最近的那个任务是否可以通知出去,如果发送完通知,则在剩下的任务中查找最近的一个。
这个算法有 O(n) 的复杂度,但实现会常简单。鉴于 n 通常都不大,我们用简单的方法就够了。
该服务还可以支持和系统时间同步。skynet 目前的机制是 skynet.now() 返回的是进程启动的时长,而 skynet.starttime() 返回的是进程启动的 UTC 时间。如果支持和系统时间同步,或是直接设置一个虚拟的时间,我们就只是修正一下内部记录的时间基点而已。服务的通知机制则和内部时间基点加起来做任务时间就可以了。
另外,还可以支持从外部(比如监听一个 socket 端口,或是 http 端口)直接发布一个任务,而不需要自己等待时钟。这样,你完全可以在外部系统的 crontab 中写好定时触发的任务,用命令行向 skynet 进程发布。
有了外部驱动的能力,你就不需要自己再重新发明轮子,再造一个 crontab 了。要知道实现一整套 crontab 兼容的 DSL 还是有一些工作量的。
最后,请警惕的是,如果你的 skynet 进程需要开上几周甚至几个月。skynet.now() 长期看是不可靠的。因为硬件时钟本身就不会特别准,系统为了保持一个长期稳定的时间,也是靠定期去网络对时实现的。而 skynet 进程一旦启动,就没有机会对时了。所以,如果你的进程需要工作很长时间的话,不要直接依赖 skynet.now() 来做长周期的定时任务。尤其在多台机器组成集群的架构下,还有可能多台机器长期工作时钟快慢不一致。