2-6 边界异常处理:定时重试请求健康的服务
问题场景
上一节实现了拦截器和动态 Client 切换,但在实际运行中发现了几个边界条件的 Bug:
| Bug | 表现 | 原因 |
|---|---|---|
| service 为空时仍创建 Client | initClient(undefined) 报错 | updateService 失败后没有阻止 initClient 调用 |
| 订阅空值导致崩溃 | Cannot read property 'getService' | BehaviorSubject 初始值为 null,订阅者未做空值判断 |
| 生命周期钩子未防护 | onModuleInit 执行失败导致整个模块无法启动 | 未捕获初始化异常 |
Bug 修复一:service 为空时跳过 initClient
问题代码:
// updateService 的 catch 块中
catch (error) {
this.updateService(); // 递归重试
}
this.initClient(service); // service 可能为空,但仍然执行
typescript
修复方案:只在 service 存在时才初始化 Client:
async updateService() {
try {
const service = await this.getHealthyService();
if (service) {
this.initClient(service);
}
} catch (error) {
// service 不存在,5秒后重试
this.clearTimer();
this.timeoutControl = setTimeout(
() => this.updateService(),
5000
);
}
}
typescript
Bug 修复二:订阅者空值判断
问题代码:
// AuthService 中直接使用 client
this.consulService.getClient().subscribe((client) => {
this.client.findOne(...); // client 可能为 null
});
typescript
修复方案:添加空值判断:
this.consulService.getClient().subscribe((client) => {
if (client) {
this.client = client;
}
});
typescript
Bug 修复三:生命周期钩子防护
问题:onModuleInit 执行失败会阻止 NestJS 应用启动。
修复方案:在生命周期钩子中添加异常捕获:
async onModuleInit() {
try {
await this.updateService();
} catch (error) {
// 初始化失败不阻塞启动,依赖定时重试恢复
console.log('[ConsulService] 初始化失败,等待定时重试');
return;
}
}
typescript
重试流程验证
完整的重试链路
Gateway 启动
└── onModuleInit()
└── updateService()
├── service 存在 → initClient → 发布 Client → 完成
└── service 不存在(后端未启动)
├── 不执行 initClient(修复后)
├── 清除旧定时器
└── setTimeout(5秒) → updateService()
├── 仍然失败 → 继续重试
└── 成功 → initClient → 发布 Client
└── 订阅者收到新 Client → 服务恢复
text
调试验证步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 启动 Gateway(不启动 user 微服务) | 控制台输出 "retrying to get service" |
| 2 | 等待 5 秒 | 再次输出重试日志 |
| 3 | 启动 user 微服务 | 下一次重试获取到 service |
| 4 | 观察日志 | Client 创建成功,服务恢复正常 |
| 5 | 发起 HTTP 请求 | 正常返回 access_token |
断点调试要点
- 46 行(updateService 入口):观察 service 是否为空
- 85 行(catch 块):确认重试定时器设置
- 110 行(initClient 调用):确认 service 存在时才执行
边界条件总结
| 边界条件 | 处理方式 |
|---|---|
| Consul 连接失败 | 5 秒后重试,不阻塞启动 |
| 服务列表为空 | 跳过 initClient,等待重试 |
| 所有实例不健康 | 持续重试直到有健康实例 |
| Client 更新失败 | 不发布 null,保留上一个有效 Client |
| 订阅者收到 null | 空值判断,跳过赋值 |
参考资源
- NestJS Lifecycle Events - 生命周期钩子
- Consul Health Service API - 健康服务查询
↑