如何不依赖系统信号实现Laravel Horizon的Supervisor暂停
Laravel Horizon自定义管控Supervisor暂停/恢复的问题
问题背景
在Laravel Horizon中部署了多个运行正常的Supervisor进程,需要通过自研Web界面对这些Supervisor做交互管控,核心操作包括暂停(pause)、恢复(continue,即取消暂停)。
需求约束是尽可能不通过系统调用实现功能:官方自带的artisan horizon:pause-supervisor命令底层通过posix_kill($supervisor->pid, 12)向进程发送信号实现暂停,需要规避这种实现方式。
尝试过的方案与异常现象
曾尝试通过直接实例化Supervisor对象的方式实现暂停功能,代码如下:
class HorizonManager { private SupervisorRepository $supervisors; private MasterSupervisorRepository $masters; private WorkloadRepository $workload; private RedisJobRepository $jobRepository; private QueueManager $queueManager; public function __construct(MasterSupervisorRepository $masters, SupervisorRepository $supervisors, WorkloadRepository $workload, RedisJobRepository $jobRepository, QueueManager $manager) { $this->masters = $masters; $this->supervisors = $supervisors; $this->workload = $workload; $this->jobRepository = $jobRepository; $this->queueManager = $manager; } public function pauseSupervisor(string $supervisorName){ $supervisor = $this->supervisors->find($supervisorName); $supervisorOpt = new SupervisorOptions(...$supervisor->options); $sup = new Supervisor($supervisorOpt); $sup->pause(); $sup->persist(); return $this->supervisors->find($supervisorName); } }
测试时出现不符合预期的现象:
- 方法返回结果显示Supervisor状态为已暂停
- 实际对应进程并未真正暂停,即使对实例化的Supervisor对象执行了
persist()持久化操作,Supervisor进程依旧保持运行状态,没有达到暂停效果。
问题根因
代码不生效的核心逻辑错误非常直白:
- 你通过
new Supervisor($supervisorOpt)创建的是一个全新的、存在于当前Web请求内存里的对象实例,和系统里正在运行的Supervisor常驻进程没有任何运行时绑定关系。 - 调用
pause()再persist(),本质只是把你新造的这个对象的"已暂停"状态写到了Redis里,但正在运行的Supervisor是个独立的常驻进程,它自己内存里的运行状态不会因为你在另一个请求里新建个对象改Redis就自动同步。 - 官方用
posix_kill发信号的本质是跨进程给正在运行的Supervisor传递通知,告诉它要切换到暂停状态。你跳过了通知运行中进程的步骤,只改了存在Redis里的状态标记,运行中的进程根本收不到要暂停的指令,自然会继续运行。
无系统调用的实现方法
如果要完全规避posix_kill类系统信号调用,可以直接用Horizon原生的Redis指令通道实现,不需要自己造对象改状态:
Horizon每个运行中的Supervisor进程,会持续监听Redis上对应自己名称的订阅频道,你只要往对应频道里写入暂停/恢复指令,Supervisor轮询到指令后会自动执行状态切换,全程不需要调用系统进程接口。
可直接复用的实现代码:
/** * 暂停指定名称的Supervisor */ public function pauseSupervisor(string $supervisorName): void { $supervisor = $this->supervisors->find($supervisorName); if (!$supervisor) { throw new \InvalidArgumentException("指定Supervisor不存在: {$supervisorName}"); } // 向Horizon专用Redis连接的对应Supervisor频道下发暂停指令 app('redis')->connection('horizon')->publish( "supervisor:{$supervisorName}", json_encode(['command' => 'pause']) ); } /** * 恢复指定名称的Supervisor */ public function continueSupervisor(string $supervisorName): void { $supervisor = $this->supervisors->find($supervisorName); if (!$supervisor) { throw new \InvalidArgumentException("指定Supervisor不存在: {$supervisorName}"); } app('redis')->connection('horizon')->publish( "supervisor:{$supervisorName}", json_encode(['command' => 'continue']) ); }
方案注意点
- 该实现完全不依赖系统进程信号,所有通信走Horizon原生依赖的Redis发布订阅能力,和Horizon内部的进程交互逻辑完全兼容,不会有版本兼容问题
- 不需要手动实例化Supervisor对象、也不需要手动调用
persist()持久化状态:Supervisor收到指令执行完暂停/恢复操作后,会自动把最新的真实运行状态写入Redis,后续你从SupervisorRepository查询到的状态和实际运行状态完全一致 - 如果需要批量操作所有Supervisor,直接遍历
$this->supervisors->all()拿到所有Supervisor名称逐个下发指令即可;如果要做全局暂停/恢复,也可以直接往Master进程的监听频道发对应指令。
内容的提问来源于stack exchange,提问作者Brice Harismendy
相关产品推荐
相关产品推荐

