v13.39.0 的官方源码,将任务调度的基本操作应用于包开发。关于服务提供者的加载,请参阅包自动发现的内部结构;关于配置的发布,请参阅包配置的合并与缓存。
在 Schedule 解析后注册
在常规的 Laravel 应用中,FoundationServiceProvider 会将 Schedule::class 注册为 singleton。其创建过程会调用 Console Kernel 的 resolveConsoleSchedule(),并应用应用程序的时区和调度器所用的缓存存储。
包应扩展这个共享的 Schedule,而不是创建自己的 new Schedule() 了事。下面的服务提供者假定 Acme\Courier\ 通过 Composer 的 PSR-4 对应到 src/,并且包中有 config/courier.php。
callAfterResolving() 在尚未解析时会等待之后的解析;如果已经解析,则会获取实例并立即调用回调。因此,即使其他服务提供者先使用了 Schedule,也能完成注册。详情请参阅服务解析后钩子。
runningInConsole() 并不用于判断特定的 Artisan 命令。本例将通过 HTTP 使用 Schedule 的场景排除在外。另外,注册钩子的服务提供者本身不要设为延迟服务提供者。因为如果在需要之前 boot() 没有运行,钩子也不会被注册。
事件的注册与任务的执行是两回事。加载此服务提供者并不会立即执行 Heartbeat。仅注册也不会启动 cron 或
schedule:work。由使用者显式启用
包中的config/courier.php 默认设为禁用。
schedule.enabled 改为 true。本例在注册时读取这些值。时区默认为 UTC;如果选择其他时区,还需要考虑夏令时导致的重复执行或漏执行。
由于配置是浅合并的,注册器中也要为每个键指定默认值。这样即使部分覆盖导致 schedule 数组中的其他键缺失,也能以基本配置完成注册。
防止向同一个 Schedule 重复注册
下面的src/CourierSchedule.php 注册一个记录一条日志的最小 Heartbeat。借助 WeakMap,该注册器只会向同一个 Schedule 对象添加一次。
Schedule::call()、exec()、job() 会将事件添加到数组中。它们并不是查找同名事件并进行替换或去重的 API。
事件名称也关系到锁的兼容性
name() 是 description() 的别名,但它对锁名称的影响因事件类型而异。以下是默认的 mutex 名称。
对于 CallbackEvent,要在
withoutOverlapping() 或 onOneServer() 之前设置非空名称。如果未设置名称,这些方法会抛出异常。
job() 通常以任务类名等作为初始名称。但是,当以不同的参数或对象注册同一个任务时,应为每个对象显式指定名称。仅改变参数并不会让初始名称自动变成不同的名称。
SyncCatalogJob 是包所提供的任务类示例。任务的实现方法请参阅队列与任务。
更改名称、命令或 cron 表达式时,根据事件类型,锁的标识也会随之改变。在新旧版本进程并存的部署中,即使是相同的处理也可能获取不同的锁。不要把它当作单纯的显示文字变更,而应将任务名称作为稳定的公开规范来管理。
区分 onOneServer 与 withoutOverlapping
onOneServer()用于选出在该执行时刻负责执行的服务器。经确认,CacheSchedulingMutex会在事件的 mutex 名称后加上时分,并以 1 小时的有效期保存。它与随任务结束而释放的防重叠锁是不同的。withoutOverlapping()用于防止在前一次执行仍在进行时发生重叠。指定的有效期单位为分钟,默认为 1,440 分钟。有效期过短时,锁会在处理结束前失效。- 当
job()将任务投递到队列时,调度事件的完成并不等于任务处理的完成。worker 端的重复控制需要另行设计,例如任务端的唯一任务或WithoutOverlapping中间件等。
onOneServer() 的使用者,应告知其必须满足官方文档指定的 database、memcached、dynamodb、redis 共享缓存配置。锁并不保证业务处理的 exactly-once,处理本身也需要能够承受重复执行。
繁重的依赖在执行时解析
上述 Heartbeat 中的LoggerInterface,会在事件执行时由 CallbackEvent 使用容器的 call() 进行解析。这种结构在注册钩子时既不会输出日志,也不会创建客户端。
不在构造函数中放置外部连接或业务处理的设计,在 Artisan 命令的依赖解析中同样重要。
此外,
call() 和 job() 属于 CallbackEvent,因此不能使用 runInBackground()。耗时的处理应委托给队列,或根据需要选择 command() 的后台执行。
不要篡改整个应用的配置
Schedule::useCache() 会更改 Schedule 所持有的 mutex 对象的存储。它并不是只针对包事件的配置,也会影响共享同一 mutex 的其他事件。不要在包的 boot() 中无条件调用它,而应将其视为由使用方应用选择的配置。
同样,包也不应擅自更改 Schedule::alwaysOnOneServer()、调度器的暂停以及维护模式期间的执行策略。
配置变更后要重新创建 Schedule
示例中的enabled、cron 表达式和时区都是注册时的配置。对于已创建的 Schedule,仅更改 config() 的值并不会添加或删除事件,也不会重新读取配置。WeakMap 的判定也不是用于重新配置的。
在使用配置缓存的部署中,应重建缓存,并在加载新配置的进程中确认 schedule:list。对于长时间运行的调度器,以及正在执行不足一分钟任务的 schedule:run,也要确认运维上不会残留旧代码和旧配置。关于子分钟任务在部署时的中断,请参阅任务调度。
将注册与执行分开测试
下面是使用包的 Composer 自动加载和 PHPUnit 的tests/CourierScheduleTest.php 示例。将 mutex 替换为测试替身,在不使用外部缓存或真实 cron 的情况下确认注册结果。
不要为了通过注册测试而用
schedule:run 执行使用方应用的全部任务。将主体测试与在受控环境中进行的调度器集成测试分开,可以避免无关定时任务带来的副作用。
相关页面
任务调度
确认执行频率、cron 的配置、委托给队列以及部署时的运维。
包的服务解析后钩子
确认对未解析和已解析服务的应用,以及重新注册时的注意事项。
参考的一手资料
- Laravel 官方文档:任务调度
- Laravel 官方文档:包开发
- FoundationServiceProvider:Schedule 的 singleton 注册
- Console Kernel:Schedule 的创建与默认配置
- ServiceProvider:callAfterResolving()
- Schedule:事件的添加、任务的延迟创建、mutex 的共享
- Event:命令事件的 mutex 名称
- CallbackEvent:名称的必要条件与执行时的依赖解析
- CacheEventMutex:防重叠锁与有效期的转换
- CacheSchedulingMutex:按执行时刻划分的锁
- 官方测试:事件的注册与时区
- 官方测试:任务类名的延迟创建