Coolify 中 Laravel Actions 的 Job 入口:用 dispatch 与 asJob 把业务逻辑队列化的完整指南
【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify
本篇技术指南围绕lorisleiva/laravel-actions包的Job 入口(Job Entrypoint)展开,系统讲解如何把一个 Action 通过dispatch系列方法异步/同步入队、用makeJob/withChain做作业编排、借助JobDecorator及一组$job*属性精细控制队列、重试、退避、超时、唯一性与失败处理,并给出配套的队列测试断言。Coolify 在 composer.json 中声明依赖"lorisleiva/laravel-actions": "^2.10.2",其 app/Actions 目录下大量面向服务器的动作(如 Docker 清理、数据库启动)正是以 Action 形态入队执行的,读完本篇你将掌握 Coolify 这类 Laravel 项目中 Action 队列化的标准姿势与源码级依据。
一、为什么要用 Action 当 Job:一个用例、多个入口
Laravel Actions 的核心思想是"一个可复用的用例(use-case)对应一个类",业务逻辑沉淀在handle(...)中,而同一逻辑可以根据场景暴露出不同入口:作为对象直接调用(::run)、作为控制器(asController)、作为队列任务(asJob+dispatch)、作为事件监听器(asListener)、作为命令行(asCommand)。本参考(job.md)专门覆盖其中的Job 入口。
在 Coolify 中,动作类统一使用use AsAction;引入能力,例如 CleanupDocker.php:
<?php namespace App\Actions\Server; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; class CleanupDocker { use AsAction; public string $jobQueue = 'high'; public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false) { // ...真正的 Docker 清理业务逻辑 } }推荐模式
- 用
Action::dispatch(...)做异步执行; - 把队列特有的编排逻辑放在
asJob(...); - 把可复用的业务逻辑放在
handle(...)。
asJob被调用时若类中未定义该方法,会自动回退到handle,因此最简单的"异步化"甚至不需要写asJob——Coolify 中大量动作(如 CleanupDocker)仅声明handle与$jobQueue,靠回退机制完成入队。asJob只在需要针对队列场景做分支时才定义(例如附加上下文参数、读取JobDecorator元数据)。
二、分发方法全解:异步、条件分发与同步
dispatch
把 Action异步分发到队列,由 worker 消费:
SendTeamReportEmail::dispatch($team);Coolify 中的真实调用,例如 StopApplication.php 在停止容器后触发 Docker 清理:
if ($dockerCleanup) { CleanupDocker::dispatch($server, false, false); }同样的模式还出现在 StopDatabase.php 与 StopService.php 中——CleanupDocker::dispatch($server, false, false)的三个实参即handle(Server $server, bool $deleteUnusedVolumes, bool $deleteUnusedNetworks)的入参。
dispatchIf/dispatchUnless
按条件决定是否异步分发:
// 仅当条件是 premium 时才入队 SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team); // 除非满足条件,否则入队 SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);典型用途:把"是否要发邮件/是否要做重量级处理"的守卫判断从handle前移到调用方,语义更直白。
dispatchSync与dispatchNow
同步执行(当前进程立即跑完,不走 worker):
SendTeamReportEmail::dispatchSync($team); SendTeamReportEmail::dispatchNow($team); // dispatchSync 的别名dispatchAfterResponse
在 HTTP 响应发送完成后再同步执行,适合"响应已返回、但不想让用户等待收尾工作"的场景:
SendTeamReportEmail::dispatchAfterResponse($team);需要注意:该机制依赖 Laravel 的 after-response 处理管线,使用前应确认运行环境与队列驱动支持这一语义,避免把重型任务误当成"异步保险"。
各方法一览:
| 方法 | 语义 | 典型场景 |
|---|---|---|
dispatch(...) | 异步入队 | 后台发送邮件、远程 Docker 操作 |
dispatchIf($cond, ...) | 条件满足才异步入队 | 按套餐/开关决定是否执行 |
dispatchUnless($cond, ...) | 条件不满足才异步入队 | 排除式守卫 |
dispatchSync(...)/dispatchNow(...) | 同步执行 | 需要立即得到副作用与结果 |
dispatchAfterResponse(...) | 响应返回后同步执行 | 响应优先、收尾随后 |
三、作业编排:makeJob、makeUniqueJob 与 withChain
makeJob
把 Action 包成一个JobDecorator装饰器对象,便于交给dispatch(...)全局辅助函数或塞进链:
dispatch(SendTeamReportEmail::makeJob($team));makeUniqueJob
创建UniqueJobDecorator。通常配合ShouldBeUnique会自动生效,但也可以强制使用:
dispatch(SendTeamReportEmail::makeUniqueJob($team));withChain
挂接一串"前序任务处理成功后依次执行"的作业:
$chain = [ OptimizeTeamReport::makeJob($team), SendTeamReportEmail::makeJob($team), ]; CreateNewTeamReport::withChain($chain)->dispatch($team);等价写法是借助Bus::chain:
use Illuminate\Support\Facades\Bus; Bus::chain([ CreateNewTeamReport::makeJob($team), OptimizeTeamReport::makeJob($team), SendTeamReportEmail::makeJob($team), ])->dispatch();对应断言(链式作业验证):
use Illuminate\Support\Facades\Bus; Bus::fake(); Bus::assertChained([ CreateNewTeamReport::makeJob($team), OptimizeTeamReport::makeJob($team), SendTeamReportEmail::makeJob($team), ]);链路典型场景:Coolify 中"先构建、再部署、再清理"这类多阶段流水线非常适合抽象为makeJob组成的链;而像数据库"先启动、后按需启动代理"(见 StartDatabase.php 的StartDatabaseProxy::dispatch)这类先后依赖关系,也天然与链式语义吻合。
四、队列归属配置:$jobConnection / $jobQueue / configureJob
属性方式
public string $jobConnection = 'my_connection'; // 队列连接 public string $jobQueue = 'my_queue'; // 队列名Coolify 大量动作直接把高优队列写死在属性上,例如 CleanupDocker.php 的public string $jobQueue = 'high';;同类用法还见于 StopApplication.php、CheckUpdates.php、InstallPrerequisites.php 等,说明 Coolify 约定的基础设施类动作统一走high队列。
方法方式(configureJob)
当队列归属需要动态计算时,用configureJob(JobDecorator $job):
use Lorisleiva\Actions\Decorators\JobDecorator; public function configureJob(JobDecorator $job): void { $job->onConnection('my_connection') ->onQueue('my_queue') ->through(['my_middleware']) ->chain(['my_chain']) ->delay(60); }Coolify 的真实案例:数据库统一启动器 StartDatabase.php 通过configureJob把任务路由到"部署队列":
public function configureJob(JobDecorator $job): void { $job->onQueue(deployment_queue()); }而deployment_queue()是 bootstrap/helpers/shared.php 中定义的辅助函数:
function deployment_queue(): string { return isCloud() ? 'deployments' : 'high'; }它体现了 Coolify 的队列策略:云托管版把部署类任务放到独立deployments队列,由隔离的 Horizon worker 池消费;自托管版则复用共享的high队列。这也意味着使用该辅助函数时,worker 侧必须把对应队列纳入消费(如配置HORIZON_QUEUES含deployments),否则作业永远不会被处理。选择策略:静态固定走属性,动态决定走configureJob。
五、重试、退避、超时与失败处理
对重量级、易抖动的远程操作任务,必须显式声明重试策略。Coolify 的 Action 需要与 SSH/远程 Docker 命令打交道(参考 CleanupDocker 内部大量instant_remote_process调用),重试与退避策略因此至关重要。
最大尝试次数$jobTries
public int $jobTries = 10;最大异常数$jobMaxExceptions
在超过允许的"未处理异常次数"后才判定失败(避免偶发异常过快耗尽重试):
public int $jobMaxExceptions = 3;重试退避$jobBackoff与getJobBackoff
属性方式(固定秒数):
public int $jobBackoff = 60;方法方式支持按尝试次数给出递增数组:
public function getJobBackoff(): array { return [30, 60, 120]; }也可以返回固定int:
public function getJobBackoff(): int { return 60; }超时$jobTimeout
public int $jobTimeout = 60 * 30; // 30 分钟重试截止时间$jobRetryUntil/getJobRetryUntil
属性方式要求时间戳:
public int $jobRetryUntil = 1610191764;方法方式返回DateTime,更可读、可动态计算:
public function getJobRetryUntil(): DateTime { return now()->addMinutes(30); }getJobRetryUntil与$jobTries是两种不同的重试上限:前者按时间兜底、后者按次数兜底。Coolify 开发约定(见 .cursor/skills/laravel-actions/SKILL.md)中推荐组合使用$jobTries、$jobMaxExceptions、getJobBackoff()与getJobRetryUntil(),例如:
public int $jobTries = 3; public int $jobMaxExceptions = 3; public function getJobRetryUntil(): DateTime { return now()->addMinutes(30); } public function getJobBackoff(): array { return [60, 120]; }失败回调jobFailed
作业最终失败时调用,可用于通知、上报、补偿等收尾逻辑;注意它会额外收到本次分发的参数:
public function jobFailed(?Throwable $e, ...$parameters): void { // Notify users, report errors, trigger compensations... }六、唯一性保障:防止重复作业叠加
当同一资源可能被重复触发(例如重复点击、Webhook 风暴、并发心跳)时,给作业加唯一性锁可以避免"同一个动作同时在队列里积压多份"。先让 Action 实现ShouldBeUnique,再用以下成员定义唯一键与锁时长。
唯一键getJobUniqueId/$jobUniqueId
按参数动态取键:
public function getJobUniqueId(Team $team): int { return $team->id; }静态键:
public string $jobUniqueId = 'some_static_key';锁时长getJobUniqueFor/$jobUniqueFor
锁有效期内不会重复入队:
public function getJobUniqueFor(Team $team): int { return $team->role === 'premium' ? 1800 : 3600; }public int $jobUniqueFor = 3600;锁存储getJobUniqueVia
自定义唯一性锁使用的缓存驱动(默认基于 Laravel 缓存):
public function getJobUniqueVia() { return Cache::driver('redis'); }唯一性键在 Coolify 场景中尤其适合"按 Server/Team/资源实例去重":例如按$server->id或$team->id保证同一台服务器上同一类远程维护任务同时只存在一份。
七、队列中间件与可观测性
队列中间件getJobMiddleware
作用于入队后的作业(不是 HTTP 中间件),典型如限流:
public function getJobMiddleware(array $parameters): array { return [new RateLimited('reports')]; }展示名getJobDisplayName
自定义队列面板(如 Horizon/Telescope)中显示的任务名:
public function getJobDisplayName(): string { return 'Send team report email'; }标签getJobTags
给任务打上可检索标签,便于在 Horizon 中按标签监控与排查:
public function getJobTags(Team $team): array { return ['report', 'team:'.$team->id]; }八、模型缺失处理
当任务绑定的 Eloquent 模型已被删除时,选择丢弃任务还是保留并失败:
public bool $jobDeleteWhenMissingModels = true;或方法形式:
public function getJobDeleteWhenMissingModels(): bool { return true; }属性与方法是等效的两种写法,二选一即可。这层语义与 Laravel 内建 Job 的deleteWhenMissingModels一致,能在"资源被删除但队列里还残留旧任务"时优雅降级——对 Coolify 这类资源频繁创建/删除/迁移的系统很有价值。
九、队列测试:用断言锁定入队行为
测试入队行为的标准姿势是Queue::fake()配合 Action 级断言,这样业务逻辑不真跑,只验证"是否被推入队列"。
assertPushed
use Illuminate\Support\Facades\Queue; Queue::fake(); SendTeamReportEmail::assertPushed(); SendTeamReportEmail::assertPushed(3); // 恰好入队 3 次 SendTeamReportEmail::assertPushed($callback); // 回调校验入参 SendTeamReportEmail::assertPushed(3, $callback);$callback会收到四样东西:
- Action 实例;
- 被分发的实参;
JobDecorator实例;- 队列名。
据此可以做细粒度断言,例如:
SendTeamReportEmail::assertPushed(fn ($action, array $args, $job, string $queue) => $args[0]->id === $team->id && $queue === 'reports' );assertNotPushed
验证"在守卫条件下不该入队":
SendTeamReportEmail::assertNotPushed(); SendTeamReportEmail::assertNotPushed($callback);assertPushedOn
验证入队到指定队列:
SendTeamReportEmail::assertPushedOn('reports'); SendTeamReportEmail::assertPushedOn('reports', 3); SendTeamReportEmail::assertPushedOn('reports', $callback); SendTeamReportEmail::assertPushedOn('reports', 3, $callback);链式断言
使用第一节提到的Bus::fake()+Bus::assertChained([...])校验整条链。
Coolify 自身的测试也在大量使用同一套队列断言哲学,只是对象是常规 Job 类。例如 tests/Feature/Api/LifecycleApisTest.php 中Queue::fake()后以Queue::assertPushed(DeleteResourceJob::class)验证资源删除被入队;tests/Feature/Api/DeploymentCancellationApiTest.php 则用回调断言具体作业实例的字段。将同样的模式套用到 Action 上,就是上文的SendTeamReportEmail::assertPushed(...)用法。Coolify 开发规范建议的两层测试策略(见 SKILL.md):
handle(...)直测:验证业务正确性;- 入口测试:以
Queue::fake()/Bus::fake()校验入队编排。
十、Checklist:入队前自检清单
根据参考文档(references/job.md),发布队列化 Action 前逐项核对:
- 异步/同步分发方式是否匹配场景:
dispatch(后台异步)、dispatchSync(立即同步)、dispatchAfterResponse(响应后收尾); - 需要时显式配置队列:
$jobConnection、$jobQueue、configureJob(...); - 重试/退避/超时策略是否经过有意设计(
$jobTries、$jobBackoff/getJobBackoff、$jobTimeout、$jobRetryUntil/getJobRetryUntil); asJob(...)除非确有队列分支需求,否则应委托给handle(...);- 队列测试使用
Queue::fake()与 Action 断言(assertPushed*系列)。
十一、常见陷阱
- 只把领域逻辑写在
asJob(...)中:会破坏"业务逻辑集中在handle、其余入口复用"的原则,导致同步调用与入队行为分叉。正确做法是asJob只做队列编排并委托handle; - 在重型作业上遗漏唯一性/超时/重试控制:远程操作类 Action(如 Coolify 对服务器的批量清理)一旦超时或重复叠加,会造成资源竞争与队列积压;
- 测试中缺少队列专用断言:只测
handle而不断言入队行为,会放过"条件判断写错导致不该入队/入错队列/链式顺序错误"等回归。
结语
在 Coolify 这类以"远程服务器编排"为核心负载的 Laravel 应用中,Action 的 Job 入口是把业务逻辑(handle)与队列机制解耦的关键桥梁:dispatch系方法负责入队形态,makeJob/withChain负责作业编排,JobDecorator与$job*属性负责队列/重试/超时/唯一性等生命周期策略,assertPushed*负责把入队契约固化成测试。参考文档及相关技能位于仓库 .cursor/skills/laravel-actions,可在实现队列化 Action 时随时对照检索。
【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考