hyperf全局异常处理器对协程任务无效,因其仅作用于http请求生命周期,而异步队列等协程任务运行在consumerprocess中,不经过http异常链路;async-queue组件自行捕获并静默处理job异常,除非job类handle()中显式抛出未捕获异常,否则重试机制不触发。

Hyperf全局异常处理器为什么对协程任务无效
因为Hyperf的http异常处理器只作用于HTTP请求生命周期,而异步队列、定时任务、自定义进程等协程任务运行在ConsumerProcess或TimerProcess中,不经过HTTP Server的异常处理链路。你在config/autoload/exceptions.php里配的http handler,对handle()里抛出的RuntimeException根本不起作用。
常见现象包括:
- 异步任务失败后Redis中没进
async-queue:failed列表 - 日志里查不到任何错误堆栈,只有“任务已执行完毕”假象
-
retry_seconds和max_attempts完全不触发
根本原因不是配置错了,而是异常压根没走到你写的handler里——它被async-queue组件自己捕获并静默吞掉了(除非你显式抛出)。
async-queue任务异常必须由Job类自身抛出
Hyperf异步队列的重试机制只响应未被捕获的异常。只要handle()方法里有try-catch且没throw,框架就认为任务成功。
正确做法是:
- 删掉空
catch (\Exception $e) { },或者改成catch (\Exception $e) { throw $e; } - 如需记录日志,必须在
throw前调用logger()->error(),例如:catch (\Exception $e) { logger()->error('SendEmailJob failed', ['user_id' => $this->userId, 'exception' => $e]); throw $e; } - 确保Job类继承
Hyperf\AsyncQueue\Job,且构造函数里不传PDO、Redis等不可序列化对象(否则投递阶段就失败,根本到不了handle())
协程内DB/Redis操作必须用协程安全驱动
在handle()中直接调用PDO::query()、file_put_contents()或非协程版Redis客户端,会导致协程挂起甚至静默退出——这不是异常,是协程调度器主动放弃该协程,也不会触发重试。
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
必须确认:
- 数据库连接使用
hyperf/database封装的协程PDO(不是原生PDO) - Redis客户端用
hyperf/redis提供的RedisFactory实例,而非new \Redis() - 所有IO操作(HTTP请求、文件读写)都走协程适配版本,例如
Co\curl_exec()或Hyperf\HttpClient\HttpClient
否则即使throw new RuntimeException(),也大概率卡在IO上,超时后被handle_timeout硬杀,重试逻辑照样失效。
想统一捕获协程任务异常?别碰全局ExceptionHandler
试图在shouldReport()里拦截JobException再转给自定义handler,只会让问题更隐蔽。async-queue有自己的异常传播路径:JobException → RetryHandler → FailedHandler,中间插一个Web层的ExceptionHandler,反而会切断重试链路。
真正可控的统一处理点只有两个:
- 在每个Job的
handle()末尾加finally块做通用清理(如释放临时资源) - 通过
Hyperf\AsyncQueue\Event\AfterHandle事件监听器做事后审计,但不能干预重试逻辑
最易被忽略的一点:协程上下文在异步任务中默认不继承主请求的coroutine_id和request_id,所以logger()->info()打出来的日志可能无法关联原始请求——要用ApplicationContext::getContainer()->get(CoroutineContextInterface::class)手动注入上下文,否则排查时连“谁触发的任务”都找不到。










