首页
/ Coolify 的 Horizon 队列告警实战:waits 阈值、通知路由与 JobFailed 的边界

Coolify 的 Horizon 队列告警实战:waits 阈值、通知路由与 JobFailed 的边界

2026-09-05 18:31:47作者:郦嵘贵Just

本文基于 Coolify 仓库内的 Horizon 告警参考文档(.agents/skills/configuring-horizon/references/notifications.md)展开,讲清三件实战中最容易被混淆的事:config/horizon.php 中的 waits 数组如何决定 LongWaitDetected 事件何时触发、如何通过 HorizonServiceProvider 把告警路由到邮件/Slack/短信、以及为什么"失败任务告警"不属于 Horizon 官方通知路由的范畴。读完可掌握 Coolify(Laravel + Redis 队列 + Laravel Horizon)场景下队列积压告警的正确配置路径与排错思路。

一、核心概念:LongWaitDetected 事件与 waits 阈值

Laravel Horizon 内置了一个名为 LongWaitDetected 的事件:当某个队列中的任务在等待时间超过设定阈值仍未被消费时,Horizon 会触发该事件,并把它交给自己的通知发送器(notification sender)处理。阈值不在通知路由里配置,而是写在 config/horizon.phpwaits 数组中,每一个 连接:队列 组合可以拥有独立的等待秒数

文档给出的结论很直接:

如果告警触发得太频繁或太晚,应该调整 waits,而不是去改通知路由配置。

在 Coolify 仓库中可以直接验证这一机制的存在方式。config/horizon.php 中对应的配置段:

/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired.
|
*/

'waits' => [
    'redis:default' => 60,
],

要点拆解:

  • 键的格式是 connection:queue,例如 'redis:default' => 60 表示 redis 连接的 default 队列;如果你的业务任务派发到其他队列(Coolify 默认队列由 HORIZON_QUEUES 环境变量控制,见下文),需要为对应组合单独声明阈值,否则该组合不产生 LongWaitDetected 告警。
  • 单位是秒,60 即任务在队列中积压等待超过 60 秒才告警。
  • 这个值是"触发事件"的开关,与"事件发生后通知谁"完全是两回事——前者在 waits,后者在 Service Provider 的路由方法里。

值得结合仓库环境说明的一点:Coolify 锁定的 Horizon 版本是 laravel/horizon: ^5.48.2(见 composer.json 的 require 段)。LongWaitDetected 属于 Laravel 队列事件体系(Illuminate\Queue\Events\LongWaitDetected),Horizon 在其内部将其接入通知发送链路,因此上述机制在当前仓库的实现事实层面成立;而"12.x 文档"之类的版本表述指的是 Laravel Horizon 文档版本线,与仓库实际锁定的包版本无关,配置行为以仓库内 config/horizon.php 为准。

二、通知路由:在 HorizonServiceProvider 中接入邮件 / Slack / SMS

文档的第二个要点:不要把 LongWaitDetected 当普通 Laravel 事件手动注册 listener。Horizon 已经把该事件接入了自己的通知发送器,正确做法是在 App\Providers\HorizonServiceProviderboot() 方法里使用内置路由方法:

  • Horizon::routeMailNotificationsTo($email) —— 邮件告警
  • Horizon::routeSlackNotificationsTo($webhookUrl) —— Slack webhook
  • Horizon::routeSmsNotificationsTo($phone) —— 短信告警

也就是说,"配置告警" = 配 waits(何时触发)+ 配路由(触发后发给谁),两者缺一不可;如果你发现告警"没人收到",先检查路由是否配置,而不是怀疑事件没触发。

Coolify 仓库中的 HorizonServiceProvider 实际结构

app/Providers/HorizonServiceProvider.php 继承自 Laravel\Horizon\HorizonApplicationServiceProvider,这是理解整套机制的关键:父类在 boot() 中完成了 LongWaitDetected 到通知发送器的接线与路由注册,子类 boot() 首行调用 parent::boot()第 34 行)之后,才能在其后安全叠加自定义逻辑。

Coolify 的 boot() 中实际叠加了两段与队列事件相关的自定义监听,可作为"在 Horizon 官方路由之外追加自定义处理"的参照实现:

  1. JobReserved 监听第 35–53 行):当 App\Jobs\ApplicationDeploymentJob 被 worker 领取时,从任务 tags 中解析出 App\Models\ApplicationDeploymentQueue 的 ID,把 horizon_job_id 回写到部署队列记录上——这是 Coolify 部署流程与 Horizon 任务 ID 的关联机制,与告警无关,但展示了同一 Service Provider 内事件监听的典型写法。
  2. JobFailed 监听第 55–73 行):仅在云端环境(isCloud())下,当失败异常为 DeploymentExceptionTimeoutExceededException 时,尽力从 Horizon 的失败任务存储中删除该任务(app(JobRepository::class)->deleteFailed($uuid)),并用 try/catch 包裹保证"清理失败不影响原始失败语义"。

注意第二段监听不是告警,而是数据清理。它恰好佐证了文档第三节的观点——Coolify 需要额外行为时,走的是"自定义队列事件处理"路线,而不是指望 Horizon 官方通知路由覆盖失败场景。

此外,该 Provider 的 gate() 方法(第 76–89 行)通过 viewHorizon Gate 限制面板访问:仅用户 ID 为 0 或邮箱在 horizon.allowed_emails 配置(对应 config/horizon.phpHORIZON_ALLOWED_EMAILS 环境变量,逗号分隔)中的用户可访问。这与通知路由同属"接入层"配置,排障时可一并核对。

路由配置的落地位置

在你自己的应用(或扩展 Coolify 的派生项目)中,推荐写法:

namespace App\Providers;

use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;

class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
    public function boot(): void
    {
        parent::boot();

        // 告警触发后,路由到邮件
        Horizon::routeMailNotificationsTo('ops@example.com');

        // 可选:Slack webhook / 短信
        // Horizon::routeSlackNotificationsTo('https://hooks.slack.com/services/...');
        // Horizon::routeSmsNotificationsTo('+8613800138000');
    }
}

两个实践提示(与文档口径一致):

  • 路由调用放在 boot() 且必须在 parent::boot() 之后,确保父类完成事件接线;
  • 如果告警"时灵时不灵",优先用 waits 中目标队列组合是否存在、阈值是否合理来解释,路由配置本身不控制触发时机。

三、失败任务(JobFailed)告警:不属于 Horizon 官方通知路由

文档第三节划清了边界:Horizon 官方文档覆盖的是长等待(LongWaitDetected)告警,不要假设文档里存在 JobFailed 的 listener 示例。任务失败(Illuminate\Queue\Events\JobFailed)属于 Laravel 队列事件体系,若需要"任务失败即通知",应当:

  1. 作为自定义队列事件处理实现——监听 JobFailed(如 HorizonServiceProvider.php 第 55 行 的做法),在其内部自行拼装通知逻辑(发邮件、推 webhook 等);
  2. 参考的是 Laravel 队列(Queue)文档,而非 Horizon 的通知路由 API。

Coolify 仓库本身即为这一路线提供了现成证据:它的 JobFailed 监听只处理"删除特定失败任务"这一业务需求,并未接入任何通知渠道;失败任务对用户的可见性由 Coolify 自身的应用层通知体系承担(如 app/Jobs/ApplicationDeploymentJob.php 所在的部署流程与 bootstrap/helpers/notifications.php 对应的通知工具),这正是"Horizon 官方路由 + 应用自定义事件处理"分工的典型形态。

排错对照表:

现象 应排查的位置 依据
告警太频繁 / 太迟 config/horizon.phpwaits 阈值 config/horizon.php
告警从未送达 HorizonServiceProvider::boot() 中的 Horizon::route*NotificationsTo() 是否配置 HorizonServiceProvider.php
想要"任务失败"告警 自定义监听 JobFailed,走队列事件文档 HorizonServiceProvider.php(清理型监听的参照)

四、结合 Coolify 队列现状的配置建议

config/horizon.php 的 supervisor 定义可以看到,Coolify 使用单个 supervisor(键名 s6),队列为 HORIZON_QUEUES 环境变量指定的 high,default(默认值),tries 为 1、maxJobs 为 400,并带有按 ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600 抬高的动态 timeout 下限。由此可给出针对本仓库的告警落地建议:

  1. 为真实消费队列设置 waits。仓库当前 waits 只有 redis:default,而 supervisor 实际消费 high,default 两个队列——如果 high 队列上的任务也需积压告警,应在 waits 中补充 'redis:high' => <秒数>,而不是调整路由。
  2. 阈值与任务耗时匹配。Coolify 队列中存在长任务(部署、备份),timeout 被刻意抬高;若把 waits 设得远小于长任务的实际执行前置等待,可能出现"任务还在正常排程中"的误报,建议以队列 P95 排队时长为基线上浮设置。
  3. 不要为失败告警寻找 Horizon 开关。按第三节路线,在 boot() 内叠加 JobFailed 监听(注意保持 try/catch 兜底风格),通知渠道复用应用层已有的通知设施。

五、小结

围绕 .agents/skills/configuring-horizon/references/notifications.md 的三个要点,可以归纳为一条清晰的排障决策链:

  • 触发层waitsconfig/horizon.php)按 connection:queue 设定 LongWaitDetected 秒数阈值,告警时机只在这里调整;
  • 路由层HorizonServiceProvider::boot()Horizon::routeMailNotificationsTo() / routeSlackNotificationsTo() / routeSmsNotificationsTo(),Horizon 已内置 LongWaitDetected 与通知发送器的接线,无需手动注册 listener;
  • 边界层:JobFailed 告警不在 Horizon 官方通知路由范围内,属于自定义队列事件处理,应参照 Laravel 队列文档实现(Coolify 的 JobFailed 清理监听 是该模式的仓库内实例)。

配套参考文档位于同一 skill 目录下:supervisor 与扩容策略见 references/supervisors.md,指标快照与保留策略见 references/metrics.md

登录后查看全文
热门项目推荐
相关项目推荐