时间重新安排逻辑

Time Rescheduling Logic

我正在编写类似调度程序的代码(如果重要的话,在 PHP 中)并且遇到了一件有趣的事情:重新安排重复性任务很容易,但是如果由于某种原因它是 运行 明显晚于预期?
例如,假设一项工作需要每小时 运行,而它的下一个计划 运行 是 13.05.2021 18:00,但它 运行 会在 13.05.2021 20:00。现在正常的重新安排逻辑将采用原始安排的时间并添加重复频率(在本例中为 1 小时),但这会使新时间 13.05.2021 19:00,这可能会导致此作业 运行 两次。从理论上讲,我们可以使用“last 运行”的时间,但它可以是 13.05.2021 20:03 之类的东西,这将使新时间成为 13.05.2021 21:03.
现在我的问题是:我们可以使用什么逻辑,以便在这种情况下下一次是 13.05.2021 21:00?我试过谷歌搜索这样的东西,但找不到任何东西。我确实看到,例如 Windows 中的 Event Scheduler 会以某种方式重新安排作业,我想这样做。

在这种情况下没有对错之分,这实际上取决于您的业务逻辑以及您希望如何构建它。

WordPress 和 Drupal 这两个最大的 CMS 也面临着这个问题,归结为“穷人的 cron”与“系统 cron”。对于“穷人的 cron”,这些系统依赖于有人点击网站来“唤醒”调度程序,如果一个月内没有人访问您的网站,您的任务也不会 运行。这两个系统都建议使用系统的 cron 来更加一致,并以特定的时间间隔“唤醒”调度程序。我也鼓励您在您的系统中探索这一点。

下一个问题是,您如何存储重复周期?您是否(有效地)在所有可能的 运行 时间里都有一个 table?那么每小时 运行 有 24 个条目?或者是否只有一个任务具有理想的 运行 date/time?与存储大量重复数据的前者相比,后者通常更容易控制。

然后,任务是否会自行重新安排,调度程序是否会这样做,或者是否存在调度程序询问下一个最佳任务运行的中间地带?弄清楚这一点非常重要,并且存在一些细微差别。

另一件需要考虑的事情是,如果任务 运行 比计划提前了怎么办?例如,如果任务 运行 是 01:00 和 01:15,世界会崩溃吗,或者它只是次优。

通常,当我构建这些类型的系统时,我的任务符合一种模式(OOP 中的接口)并支持“下一个 运行 时间”。调度程序从数据存储中提取所有“下一个 运行 时间”和 运行 已过期的任务。这样做,单个任务不可能同时存在于 01:00 和 02:00,因为它只会在数据存储中存在一次,例如在 01:00。如果调度程序随后在 01:15 醒来,它会发现 01:00 任务已过期并 运行 发送它,然后它会向该任务请求下一个 运行。该任务查看时钟(如果您在分布式环境中 运行 则由调度程序提供的时间)并且该任务执行自己的逻辑来确定它。如果逻辑是每小时,您可以从“现在”开始添加 60 分钟,然后删除分钟部分,因此 01:15 变为 02:00。

将一些异常处理和可能的数据库事务加入到这个组合中,以保证任务不会失败但仍会重新安排。

我实际上找到了一种非常简单的方法来完成我需要的事情,所以将其作为答案发布。
如果我们有一个以秒为单位的 frequency 的值(至少在我的例子中是这样),并且我们有原始的 nextrun,这是任务最初应该是 运行 的时候,那么逻辑如下:

  1. 我们需要获取当前时间(time()UTC_TIMESTAMP() 或其他)。
  2. 我们需要将当前时间与 nextrun 进行比较,并以秒为单位计算出它们之间的差异。
  3. 然后我们通过将时间差除以 frequency 来计算在这些秒数内可以完成多少次任务迭代。
  4. 我们将结果值四舍五入 (ceil())。如果我们的值低于 1,我们可能需要对其进行清理。
  5. 我们将这个向上舍入的值乘以 frequency,这将得到与步骤 2 不同的结果,这是该方法的盐。
  6. 我们将得到的秒数添加到 nextrun

就是这样。这并不能保证你永远不会有一个任务 运行 两次,如果它在第 6 步的时间值之前几秒结束,但据我所知,MS Event Scheduler 有同样的“缺陷”。
由于我在 SQL 中进行此计算,因此在 SQL 中会这样计算(至少对于 MySQL/MariaDB):

UPDATE `cron__schedule` SET `nextrun`=TIMESTAMPADD(SECOND, IF(CEIL(TIMESTAMPDIFF(SECOND, `nextrun`, UTC_TIMESTAMP())/`frequency`) > 0, CEIL(TIMESTAMPDIFF(SECOND, `nextrun`, UTC_TIMESTAMP())/`frequency`), 1)*`frequency`, `nextrun`)

参考上面的逻辑来解释:

  1. UTC_TIMESTAMP()
  2. TIMESTAMPDIFF(SECOND, `nextrun`, UTC_TIMESTAMP()) - 以秒为单位的时间比较。
  3. TIMESTAMPDIFF(...)/`frequency`
  4. CEIL(...) 将值四舍五入。 IF(...) 用于清理,因为我们可以获得 0 秒,这将导致我们根本不更改时间。
  5. CEIL(...)*`frequency`
  6. TIMESTAMPADD(...)

我不喜欢因为 IF(...) 而不得不使用 TIMESTAMPDIFF(...) 两次,但我不知道如何在不移动到存储过程的情况下避免这种情况,这感觉有点矫枉过正。此外,据我所知,无论如何,MySQL 应该只计算一次这个值。但是,如果有人可以就更简洁的方法向我提出建议,我会更新答案。