一个发券任务不声不响卡在 processing
问题
抽奖中奖之后,发优惠券是丢到一个后台队列任务里去做的。任务跑完要么标记成功,要么标记失败,状态要落库,方便后面排查。
现象
我 review 代码的时候注意到,这个任务基类处理异常的地方,只接了 Exception 这一支:
try {
$this->process();
} catch (\Exception $e) {
// 标记失败 + 重试
}
按 PHP 的规矩,Exception 和 Error 都是 Throwable 的两个分支。也就是说,如果 process() 里抛出来的是 Error(比如类型错了 TypeError、除零 DivisionByZeroError),它就不在 catch (\Exception $e) 的捕获范围里,会直接冲出这个处理方法,没人接住。
冲出去的后果是:标记失败和重试那段逻辑根本不会执行,任务状态就一直卡在 “processing”,而框架自己的失败表另算一套,两边对不上。这种事不报错、不报警,就那么不声不响地挂着。
怎么解决
我把整个项目里所有走这个基类的队列任务都查了一遍——一共有六个子类,全都没有自己重写那个处理方法,等于全继承了这个缺口。
修的时候要改三处,而且必须同步改,少一处都不行:
- 基类处理方法的 catch,从
catch (\Exception $e)放宽到catch (\Throwable $e); - 下面标记失败那个方法的参数类型,也得从
Exception放宽到Throwable; - 再往下,真正写库标记失败的那个函数,参数同样是
Exception,也得一起放宽。
这里有个坑:如果只把最外层的 catch 放宽是没用的,因为放宽的下一步要把错误对象传给标记失败的函数,那个函数如果还是 Exception 形参,传一个 Error 进去当场就又抛一个新的类型错。
改完我特意做了一次”失败形态”现场复现:先把三处改回 Exception 跑测试,确实有一个用例失败(Error 没被抓到、直接冲出去);再改回 Throwable 重跑,全绿。这样才确认是真修好了,不是自以为修好了。
另外发现,原来的测试都是直接调 process() 绕过了最外层方法,所以这个 catch 行为从来没被测过,正是 bug 能藏那么久的原因。新测试改成直接调最外层方法,专门抛一个 Error 进去,断言状态变成 failed。
结论
Exception 和 Error 在 PHP 里是平级的两兄弟,都源自 Throwable。写 catch 的时候,如果意图是”抓住一切意外”,就该接 Throwable,而不是只接 Exception。后者会悄悄放跑一整类错误。
这种”差一个父类型”的坑,光看代码很容易觉得没问题。最稳的做法是写个测试,故意抛一个 Error 进去验证 catch 真的接得住,而不是靠脑补。