monolyth/croney

基于PHP的CRON计划任务调度器

0.6.3 2023-02-01 14:59 UTC

README

基于PHP的CRON计划任务调度器

你是否讨厌为每个应用程序管理无数个计划任务?我们当然讨厌!我们也讨厌为了绕过这个问题而必须遵守特定库的代码格式(例如Symfony、Laravel...你应该知道我是指谁)。

你知道我们想做什么吗?我们只想注册一堆可调用函数,让一个中心脚本去处理。你好,Crony!

安装

Composer(推荐)

composer require monomelodies/croney

手动

  1. 下载或克隆存储库;
  2. 将命名空间 Croney 添加到路径 path/to/croney/src 的PSR-4自动加载器中。

设置可执行文件

Crony需要定期运行,因此创建一个简单的可执行文件,我们将将其添加为cron作业

#!/usr/bin/php
<?php

// Let's assume this is in bin/cron.
// It's empty for now.
$ chmod a+x bin/cron
$ crontab -e

Crony运行频率由你决定。默认假设每分钟运行一次,因为这是类Unix系统中最小的时间间隔。稍后我们将看到如何优化为每五分钟运行一次。现在,使用* * * * *(即每分钟)注册cron作业。

Scheduler

Crony的核心是一个Scheduler类的实例。这是用于运行任务的方式,它负责(正如其名称所暗示的)安排任务。

在你的bin/cron文件中

#!/usr/bin/php
<?php

use Croney\Scheduler;

$schedule = new Scheduler;

添加任务

Scheduler扩展了ArrayObject,因此要添加任务,只需设置它即可。最简单的方法是添加一个可调用函数

#!/usr/bin/php
<?php

// ...
$schedule['some-task'] = function () {
    // ...perform the task...
};

此任务每分钟运行一次(或根据你设置的cron作业的时间间隔)。任务可以是任何可调用函数,包括类方法(甚至是静态方法)。

你也可以传递一个类名,然后它将被实例化并调用__invoke。如果你需要传递构造函数参数,自己进行实例化。

设置完所有任务后,在Scheduler上调用process以实际运行它们

<?php

// ...
$schedule->process();

在特定的时间间隔或时间运行任务

为了更精确地控制任务运行的时间,向你的可调用函数添加Monolyth\Croney\RunAt属性

<?php

$scheduler['some-task'] =
    #[Monolyth\Croney\RunAt("Y-m-d H:m")]
    function () {
        // ...
    };

at的参数是一个PHP日期字符串,当使用运行时的当前时间解析时,应该通过preg_match匹配它。上面的示例每分钟运行一次任务(如果你的cron作业每分钟运行一次,这是默认设置)。要每五分钟运行一次任务,可以写如下内容

<?php

$scheduler['some-task'] =
    #[Monolyth\Croney\RunAt("Y-m-d H:[0-5][05]")]
    function () {
        // ...
    };

请注意,date函数使用占位符,因此如果你需要在例如小数(\d)上进行正则表达式匹配,你需要双重转义它。有关所有有效占位符的列表,请参阅date的PHP手册页。

由于cron的粒度,preg_match调用不会检查字符串位置,即如果你只传递'H'作为匹配的日期,它将在每小时运行,但也会在每分钟(因为00-24都是有效的分钟,并且它也会匹配'i'以及每天、每月和(由于我在2399年之前不期望这个库仍然存在,所以对实际意义而言)每年运行。所以请尽可能具体!

请注意,由于cron的粒度,秒部分是不相关的,应该省略,否则任务可能永远不会运行(因为与之比较的日期也不包含秒)。

请注意,如果你的任务是__invoke可调用的对象,RunAt属性应该在__invoke方法上,而不是对象本身上。

减少脚本运行的频率

我们之前提到过,您也可以选择每分钟运行一次cronjob,例如每五分钟运行一次。如果您只有每五分钟(或其倍数)运行的作业,那就没问题,不需要进一步配置。但假设您希望每五分钟运行一次cronjob,同时还能够根据分钟来安排任务呢?

这种情况的一个例子就是每五分钟运行一次的cronjob,定义了五个任务,每个任务都在上一个任务后一分钟运行。

构建Scheduler对象时,第一个参数实际上是它的持续时间(以分钟为单位)

<?php

$scheduler = new Scheduler(5); // Runs for five minutes

(如您所猜,这里的默认值是1。)

当您调用process时,任务实际上会运行5次(每分钟一次)并在适当的时间执行。例如。

<?php

$scheduler = new Scheduler(5);

// First task, runs only on the first loop
$scheduler['first-task'] =
    #[RunAt("H:[0-5]0")]
    function () {
        // ...
    };
// Second task, runs only on the second loop
$scheduler['second-task']
    #[RunAt("")]
    = function () {
        $this->at('H:[0-5]1');
    };
// etc.

Croney在循环之间调用PHP的sleep函数。

Croney试图计算实际需要睡眠的秒数,所以如果第一个循环中的任务总共耗时3秒,那么它会在下一次循环之前睡眠57秒。但是请注意,这并不精确,也不保证您的任务会准时运行。如果您的任务涉及基于时间的操作,请确保将时间“向下取整”到预期的值。

从理论上讲,您可以让脚本在1月1日凌晨运行,并从那里计算一切。但在现实世界中,这显然不切实际,因为任何错误都意味着您必须等待整整一年才能看到您的修复是否解决了问题!

典型值是每5或10分钟,在非常繁忙的服务器上可能是30或60。

长时间运行的作业

通常任务以(微)秒运行,但有时您的某个任务会“长时间运行”。如果这是故意的(例如,定期清理用户上传的文件),那么您显然会在安全的间隔内运行它,并且您应该注意在任务本身中限制操作(例如,“每次运行最多100个文件”)。尽管如此,您偶尔还需要编写一个应该经常运行的,但在极端情况下可能比预期耗时更长的任务。

一个虚构的例子:一个读取邮箱的任务(例如,将它们推送到票务系统)。如果邮箱由于某种原因爆炸(让我们保持乐观,想象您的应用程序一夜之间变得非常受欢迎;)这将导致问题:前一个运行可能仍在读取邮件,而下一个运行已经开始,导致邮件被处理两次。显然这不是所希望的。

Croney在运行任务之前会“锁定”每个任务,并且在任务被锁定的情况下不会尝试重新运行。如果运行失败是由于锁定,则记录警告,并定期重试任务,直到cronjob运行结束(假设其RunAt配置允许这样做)。

锁定是基于任务名称的MD5散列。这包括当前工作目录,所以为不同项目创建的多个Croney实例不会冲突。

错误处理

您可以将Psr\Log\LoggerInterface的实例作为参数传递给Scheduler构造函数。然后它将用于记录任务触发的任何消息,方式由您指定。

如果没有定义记录器,所有消息都会发送到STDOUTSTDERR(见包含的ErrorLogger)。

如果您的个别任务也需要记录,您需要为它们提供自己的记录器实例。

开发

在开发过程中,您可能希望在测试时运行任务(而不仅仅是特定时间),也可能只想运行特定任务。从版本0.3开始,Croney提供了两个命令行标志来支持这一点

--all|-a 使用此标志来运行所有任务,无论指定了什么调度。不要在生产环境中这样做!

--job=jobname|-jjobname 只运行指定的jobname。如果作业计划在特定时间运行,您可能需要与--all(或-a)标志一起使用此标志。

您可能还希望接收更多详细的反馈来了解正在发生的事情。为了实现这一点,请使用--verbose(或-v)标志调用您的可执行文件。

如果您需要在一个长时间运行的计划中测试任务,在任务完成前等待几分钟会很烦人。在这种情况下,只需覆盖调度器的内部sleeper属性。以下是一个例子(实际上是用作Croney本身单元测试的例子)

<?php

$scheduler = new class (2) extends Scheduler {
    public function __construct(int $duration)
    {
        parent::__construct($duration);
        $this->sleeper = new class () extends Sleeper {
            public function snooze(int $seconds) : void
            {
                // parent implementation: simply `sleep($seconds)`
                // next call is needed to realign the internal timer
                // so RunAt attributes keep working.
                $this->advanceInternalClock();
            }
        };
    }
};

Croney还包括一个TestLogger,该日志简单地回显日志消息,因此您可以在测试中使用输出缓冲区来检查它们。