Quartz.net - 调整和加速 SystemTime 导致失火的问题

Quartz.net - Issues with Adjusting and Speeding up SystemTime causing Misfires

出于测试原因,我希望能够调整 Quartz.Net 当前认为的时间,这样我就不必等待数小时、数天或数周来检查我的代码是否正常工作。

为此,我创建了以下简单函数(它在 F# 中,但可以用 C# 或其他语言轻松完成):

let SimulateTime = fun () ->
    currentTime <- DateTimeOffset.UtcNow
    timeDifferenceInSeconds <- (currentTime - lastCheckedTime).TotalSeconds
    simulatedTime <- simulatedTime.AddSeconds((timeDifferenceInSeconds *scaleTimeBy))
    lastCheckedTime <- currentTime
    simulatedTime

其中 currentTime、lastCheckedTime 和 simulatedTime 都是 DateTimeOffset 类型,timeDifferenceInSeconds 和 scaleTimeBy 都是 float 类型。

然后我更改 SystemTime.Now 和 SystemTime.UtcNow 以使用上述功能,如下所示:

SystemTime.Now <- 
    Func<DateTimeOffset>(
        fun () -> SimulateTime())
SystemTime.UtcNow <- 
    Func<DateTimeOffset>(
        fun () -> SimulateTime())

Mark Seemann 在我之前的一个问题中展示的可以找到

现在这基本上可以工作了,除了看起来较长的函数导致它被相当大的幅度关闭。我的意思是我所有的触发器都会失灵。例如,如果我将触发器设置为每小时发生一次并将 scaleTimeBy 设置为 60.0,以便每一秒都计为一分钟,它实际上永远不会按时触发。如果我有一个 misfire 策​​略,触发器可以关闭,但它列出的激活时间将晚到半小时标记(因此比本示例中应该慢了整整 30 秒)。

不过我可以这样做:

Console.WriteLine(SimulateTime())
Thread.Sleep(TimeSpan.FromSeconds(60.0))
Console.WriteLine(SimulateTime())

在此示例中,两次输出到屏幕的时间差恰好是一个小时,因此调用似乎不应该添加比实际增加的时间差多的时间差。

有人对如何解决此问题或处理此问题的更好方法有任何建议吗?

编辑: 所以 SimulateTime 函数的 C# 版本应该是这样的:

public DateTimeOffset SimulateTime() {
    currentTime = DateTimeOffset.UtcNow;
    double timeDifference = (currentTime - lastCheckedTime).TotalSeconds;
    simulatedTime = simulatedTime.AddSeconds(timeDifference * scaleTimeBy);
    lastCheckedTime = currentTime
    return simulatedTime;}

如果这可以帮助任何人解决这个问题。

所以这个问题是由于 Quartz.net 认为它不会很快发生任何触发器以避免发出太多调用而空闲并等待的事实引起的。默认情况下,如果在时间跨度内没有任何触发发生,它会等待大约 30 秒。 idleWaitTime 变量是在 QuartzSchedulerThread 中设置的时间跨度。现在,在检查可能很快发生的触发器时,它还会使用 QuartzSchedulerResources 中的 BatchTimeWIndow。

idleWaitTime 和 BatchTimeWindow 都可以在 configuration/properties 文件中设置,它们被称为 "org.quartz.scheduler.idleWaitTime" 和 "org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow."

基于它在 BatchTimeWindow 中的调用,我认为这只是获取变量的前瞻性(因为如果我加快速度,我想要一个小的 idleWaitTime 但我会希望它更进一步地寻找触发器,因为你等待的几秒钟实际上是几分钟,所以会比它想象的更早触发),但是在配置属性的页面上 "org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow" 的描述暗示它可能导致事情提前触发并且不太准确。所以从这里开始就是修改 idleWaitTime

的代码
let threadpool = Quartz.Simpl.SimpleThreadPool()
let jobstore = Quartz.Simpl.RAMJobStore()
let idleWaitTime = TimeSpan.FromSeconds(30.0/scaleTimeBy)
let dbfailureretryinverval = TimeSpan(int64 15000)
Quartz.Impl.DirectSchedulerFactory.Instance.CreateScheduler("TestScheduler","TestInstance",threadpool,jobstore,idleWaitTime,dbfailureretryinverval)
let scheduler = Quartz.Impl.DirectSchedulerFactory.Instance.GetScheduler("TestScheduler")

您可以使用 DirectSchedulerFactory 创建一个具有您想要的 idleWaitTime 的调度程序,它可能会使用更好的文档。它还需要一堆你可能想要或可能不想修改的东西,具体取决于你正在做什么。对于线程池,我只使用 Quartz.net 的默认 SimpleThreadPool,因为此时我不关心弄乱线程,也不想解释你是如何这样做的,除非那是问题的全部要点。提供有关工作商店的信息 here。我在这里使用 RAMJobStore,因为它比 AdoJobStore 更简单,但对于这个例子来说应该无关紧要。 dbfailureretryinterval 是这个示例不关心的另一个值,所以我只是查看它的默认设置。它的值对于这个例子来说应该是最不重要的,因为没有连接到数据库。对于 idleWaitTime 可能想要做更多的测试来找出什么是一个好的价值,但我选择只通过 scaleTimeBy 缩放它的默认值 30 秒,因为这是我用来衡量事情进展速度的东西经过。所以如果我让程序模拟时间以更快的速度流逝,这应该可以做到这一点,那么它应该只在较短的时间内保持空闲状态。需要注意的一件重要事情是,当以这种方式创建调度程序时,它也不会返回,因此需要单独调用以获取我刚刚创建的调度程序。我不知道为什么会这样,我猜如果你正在创建多个调度程序而不一定要使用它们,那么这样更好。

毕竟,您仍然可能会出现一些失火率。虽然它现在空闲的时间单位要小得多(只有几秒钟,因此根据您的用例可能是一个可接受的余量),但它仍然存在问题,它只是在检查它是否有即将到来的触发器接下来的几分之一秒。

让我们看看向 BatchTimeWindow 添加时间是否有帮助?

let threadpool = Quartz.Simpl.SimpleThreadPool()
let threadexecutor = Quartz.Impl.DefaultThreadExecutor()
let jobstore = Quartz.Simpl.RAMJobStore()
let schedulepluginmap = System.Collections.Generic.Dictionary<String,Quartz.Spi.ISchedulerPlugin>()
let idleWaitTime = TimeSpan.FromSeconds(30.0/timeScale)
let maxBatchSize = 1
let batchTimeWindow = TimeSpan.FromSeconds(timeScale)
let scheduleexporter = Quartz.Simpl.RemotingSchedulerExporter()
Quartz.Impl.DirectSchedulerFactory.Instance.CreateScheduler("TestScheduler","TestInstance",threadpool,threadexecutor,jobstore,schedulepluginmap,idleWaitTime,maxBatchSize,batchTimeWindow,scheduleexporter)
let scheduler = Quartz.Impl.DirectSchedulerFactory.Instance.GetScheduler("TestScheduler")

现在这有更多的变量,这些变量对于本示例的目的而言并不真正关心,甚至不会费心去检查,因为调整 batchTimeWindow 实际上会使情况变得更糟。就像让你在 30 分钟前回到失火状态。所以不, batchTimeWindow 虽然看起来可能有用但没有用。只修改idleWaitTime.

这种用途的理想情况是需要较短的等待时间和较长的前瞻时间,但似乎没有可用的选项。