hyperf异步队列任务失败时日志空白、重试失效、任务消失,主因是handle()中静默吞异常、全局异常处理器拦截队列异常、协程不安全操作或handle_timeout硬杀导致重试机制未触发。

Hyperf异步队列任务执行失败时,日志空白、重试不生效、任务直接消失——这不是配置漏了,而是异常没被正确捕获或重试机制被意外绕过。很多开发者把任务类里加个try-catch就以为万事大吉,结果发现失败任务既没进failed队列,也没触发retry_seconds重试,甚至根本没留下任何痕迹。
任务类中未抛出异常,导致重试机制失效
Hyperf异步队列的重试逻辑只对未捕获的异常生效。如果你在handle()方法里用try-catch吞掉所有错误却不主动throw,框架就认为“任务成功执行”,直接归档,retry_seconds和max_attempts形同虚设。
方法一:禁止静默吞异常
在Job类的handle()方法中,删除或注释掉空的catch块,例如把catch (\Exception $e) { }改成catch (\Exception $e) { throw $e; }。
方法二:显式重抛带上下文的异常
保留日志记录,但必须重抛——否则重试不会触发:catch (\Exception $e) { logger()->error('SendEmailJob failed for user '.$this->userId, ['exception' => $e]); throw $e; }。
【关键前提】Job类必须继承Hyperf\AsyncQueue\Job,且不能在构造函数中传入PDO、Redis连接等不可序列化对象,否则任务投递阶段就失败,根本到不了handle()。
全局异常处理器拦截了队列异常,导致重试失效
很多人在config/autoload/exceptions.php中配置了全局异常处理器,本意是统一返回JSON格式错误,却意外拦截了ConsumerProcess进程中的队列异常——而这个进程不属于HTTP请求生命周期,不该走Web层异常处理流程。
第一步:确认异常处理器作用域
打开app/Exception/Handler/ExceptionHandler.php,检查shouldReport()方法是否对Hyperf\AsyncQueue\Exception\JobException或Throwable做了无差别捕获。
第二步:排除队列异常类型
在shouldReport()中添加白名单过滤:if ($throwable instanceof \Hyperf\AsyncQueue\Exception\JobException) { return false; },让队列异常穿透出去交由async-queue组件自身处理。
第三步:验证重试行为
手动触发一次失败任务(如故意在handle()中写throw new RuntimeException('test')),观察Redis中async-queue:failed列表是否新增记录,并检查retry_seconds后任务是否重新入队。
协程内未使用协程安全的日志与DB操作,引发静默失败
在handle()中调用传统PDO::query()或file_put_contents(),可能因协程切换导致资源句柄错乱,任务中途退出却不抛异常——这种失败不会触发重试,因为PHP没报错,只是协程无声终止。
改用协程适配的组件:
数据库操作必须用Hyperf\Database封装的QueryBuilder,而不是原生PDO。
日志写入必须用Hyperf\Logger\LoggerFactory获取的实例,它已自动注入协程上下文ID,避免多任务日志混杂。
文件IO严禁使用file_put_contents,应改用Co\Stdio::write()或投递到另一个专用于文件操作的异步Job中。
【致命陷阱】在handle()中启动新协程(如Coroutine::create())却不await,会导致父协程结束而子协程被强制销毁,任务状态变为“成功”,实际逻辑从未执行。
超时设置不合理,任务被强制kill却不触发重试
当任务执行时间超过handle_timeout(默认10秒),Hyperf会向协程发送SIGKILL信号强制终止。此时PHP无法触发任何catch块,也不会进入failed队列——这是硬杀,不是异常。
方法一:调高handle_timeout值
根据任务实际耗时调整,例如视频转码类任务设为'handle_timeout' => 300(5分钟)。
方法二:在任务内部主动分片+心跳检测
对长耗时任务(如处理10万行Excel),每处理1000行就调用if (Co::stats()['coroutine'] > 0) { Co::sleep(0.001); }让出CPU,并检查当前协程是否已被取消(Co::getCid() === 0则说明被kill)。
方法三:启用进程级超时兜底
在config/autoload/async_queue.php中设置'processes' => 2,配合'concurrent' => ['limit' => 5],避免单个慢任务拖垮整个消费者进程。











