CI 上间歇失败的启动脚本,追下去十有八九是延迟等待。开发机上跑得好好的,到了更慢的 runner 就开始偶发超时,然后有人把 3 秒改成 8 秒,暂时好了,过一阵机器更忙又不行。
我踩过不止一次。根子在于脚本用固定时长描述了一个本来由事件决定的条件——“前一个进程应该已经就绪了”。
一个 3 秒里藏了三个问题
写 TimerAction(period=3.0) 的时候,隐含的判断是“3 秒后前一个进程已经就绪”。这个判断有三个毛病。
它没说清楚“就绪”的定义,是进程起来、参数读完、还是服务能响应。它把机器的性能写进了配置,慢机器不够快,快机器白等。最要命的是它没有失败路径——如果前一个进程其实崩了,延时到了脚本照样往下走,直到后面某个地方以一种更难解释的方式报错。
用进程事件替掉开头的延时
如果后续节点只要求某个进程已经被拉起,OnProcessStart 就够了:
1 | from launch import LaunchDescription |
但这里有个坑要提醒:进程启动不等于初始化完成。如果 task_server 一起来就调 controller 的服务,而对方还在读参数,这个方案照样失败。依赖的是“可用”而不是“已启动”时,得让被依赖方发出明确的可用信号。
让节点自己喊一声
我的做法是让前一个节点在真正就绪后发一条消息或响应一个服务,launch 侧把它转成事件。这样顺序条件就和业务语义对齐了:不是进程起来了,是我准备好接任务了。
1 | # 先确认就绪条件在运行时能被外部看到 |
如果这个条件在运行时压根观察不到,那 launch 里任何事件处理都只能退回猜测。把就绪定义成可观察的输出,是顺序编排能成立的前提。
超时必须有,而且要能失败
只等事件不设上限,条件永远不满足时启动就一直挂着,CI 只能靠作业级超时砍掉,日志里看不出卡在哪。
1 | from launch.actions import TimerAction, LogInfo |
真实项目里超时路径应该让 launch 以非零状态结束,而不是打一行日志继续跑。不然上层脚本仍然认为启动成功,问题被推迟到测试断言阶段,甚至被当成功能缺陷。
失败路径也要测
只验顺风路径的启动脚本,证明不了它在异常下可解释。至少覆盖三种:被依赖进程启动失败、就绪事件一直不出现、关键进程中途退出。
1 | # 故意让被依赖进程失败,确认 launch 以失败结束 |
退出码是关键证据。被依赖进程崩了 launch 还返回 0,说明顺序编排没把失败传出去。
延时什么时候还能用
不是完全不能用。两类场景下它合适:步骤之间没有可观察的依赖关系;或者作为事件方案的兜底上限。把延时当主要顺序手段才是问题所在。
如果依赖条件观察不到,正确方向是先在节点里补一个可观察的就绪信号,而不是把延时调大。调大只降低了失败频率,把必现变成偶发,排查成本反而更高。
参考资料
证据边界:这里只讲 launch 的顺序表达能力,不含任何项目的启动耗时数据。上面的命令和结构是能跑的最小例子。“事件优于延时”这个判断得结合具体节点的就绪条件才成立——条件本身不可观察时,先补信号,别急着改写成事件。