Coolify 中 Laravel Actions 的 Job 入口:用 dispatch 与 asJob 把业务逻辑队列化的完整指南
2026/9/8 22:03:50 网站建设 项目流程

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前移到调用方,语义更直白。

dispatchSyncdispatchNow

同步执行(当前进程立即跑完,不走 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_QUEUESdeployments),否则作业永远不会被处理。选择策略:静态固定走属性,动态决定走configureJob

五、重试、退避、超时与失败处理

对重量级、易抖动的远程操作任务,必须显式声明重试策略。Coolify 的 Action 需要与 SSH/远程 Docker 命令打交道(参考 CleanupDocker 内部大量instant_remote_process调用),重试与退避策略因此至关重要。

最大尝试次数$jobTries

public int $jobTries = 10;

最大异常数$jobMaxExceptions

在超过允许的"未处理异常次数"后才判定失败(避免偶发异常过快耗尽重试):

public int $jobMaxExceptions = 3;

重试退避$jobBackoffgetJobBackoff

属性方式(固定秒数):

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$jobMaxExceptionsgetJobBackoff()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):

  1. handle(...)直测:验证业务正确性;
  2. 入口测试:以Queue::fake()/Bus::fake()校验入队编排。

十、Checklist:入队前自检清单

根据参考文档(references/job.md),发布队列化 Action 前逐项核对:

  • 异步/同步分发方式是否匹配场景:dispatch(后台异步)、dispatchSync(立即同步)、dispatchAfterResponse(响应后收尾);
  • 需要时显式配置队列:$jobConnection$jobQueueconfigureJob(...)
  • 重试/退避/超时策略是否经过有意设计($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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询