-
Notifications
You must be signed in to change notification settings - Fork 329
Description
问题描述
当前测试框架使用的端口分配机制存在经典的 TOCTOU (Time of Check, Time of Use) 竞态条件,这是 Issue #395 中报告的服务器启动失败的根本原因。
虽然 #395 通过添加超时机制实现了快速失败(治标),但本 Issue 旨在从根本上解决端口分配的竞态问题(治本)。
根本原因分析
当前实现的问题
文件位置:framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/util/TestUtils.java:26-32
```java
public static int getLocalAvailablePort() {
try (ServerSocket serverSocket = new ServerSocket(0)) {
return serverSocket.getLocalPort(); // OS 分配端口 8080
} // ← ServerSocket 关闭,端口 8080 被释放
//
}
```
TOCTOU 竞态条件
- T1: `ServerSocket(0)` → OS 分配端口 8080
- T2: `ServerSocket.close()` → 端口 8080 被释放
- [时间窗口]
⚠️ 通常 1-100ms,高负载下可达数秒 - T3: 其他进程/测试可能占用端口 8080
- T4: `NettyHttpClassicServer.bind(8080)` → 绑定失败!
为什么在并发构建中更容易发生
- Maven 并行执行多个模块测试(Surefire forkCount > 1)
- 多个 JVM 进程同时请求端口
- 连续 145+ 个测试服务器,端口池耗尽
- 系统负载高时,端口回收延迟
影响范围
- 所有使用
@MvcTest或@EnableMockMvc注解的测试类(至少 6 个) - 偶发性端口冲突导致测试失败
- 在 CI/CD 并发构建环境中概率更高
建议的修复方案
方案 A:让 Netty 自动分配端口(推荐)
核心思路:不预先获取端口,而是让 Netty 在 bind 时自动分配,然后从 Channel 获取实际端口。
优点:
- 彻底消除 TOCTOU 竞态条件
- 简化端口管理逻辑
- 符合 Netty 最佳实践
缺点:
- 需要修改多个组件
- 工作量较大(预估 8-16 小时)
实施步骤:
- 修改 `TestUtils.getLocalAvailablePort()` 或移除此方法
- 修改 `DefaultFitTestService`:传递端口 0 给 TestFitRuntime
- 修改 `NettyHttpClassicServer`:bind(0) 后从 Channel 获取实际端口
- 修改 `MockMvc`:在服务器启动后获取真实端口
- 确保 `MockController` 健康检查仍然可访问
关键代码示例:
```java
// NettyHttpClassicServer.java
private void startServer() {
// ...
if (this.httpPort == 0) {
// 让 Netty 自动分配端口
Channel channel = serverBootstrap.bind(0).sync().channel();
this.httpPort = ((InetSocketAddress) channel.localAddress()).getPort();
log.info("HTTP server bound to auto-assigned port: {}", this.httpPort);
} else {
// 使用指定端口
Channel channel = serverBootstrap.bind(this.httpPort).sync().channel();
}
// ...
}
// 需要提供获取实际端口的方法
public int getActualPort() {
return this.httpPort;
}
```
方案 B:保持 ServerSocket 打开直到成功绑定
核心思路:不关闭 ServerSocket,将其传递给 Netty,由 Netty 接管。
优点:
- 消除时间窗口
- 修改范围相对较小
缺点:
- 实现复杂,需要传递 ServerSocket
- Netty 可能不直接支持这种方式
方案 C:添加端口冲突重试机制
核心思路:如果端口绑定失败,自动重试 2-3 次。
优点:
- 实现相对简单
- 降低冲突概率
缺点:
- 不能彻底解决问题,只是降低概率
- 增加启动延迟
工作量评估
- 复杂度:中高(Medium-High)
- 工作量:8-16 小时
- 风险等级:中(Medium)- 需要充分测试
- 优先级:中高(Medium-High)
分解:
- 代码修改:4-6 小时
- 测试验证(包括并发场景):3-6 小时
- 代码审查和文档更新:1-4 小时
测试策略
- 单元测试:验证端口分配逻辑
- 并发测试:并发启动 10 个测试服务器
- 压力测试:模拟 160 个模块并发构建
- 回归测试:运行所有 @mvctest 测试
相关文件
- `framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/util/TestUtils.java`
- `framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpClassicServer.java`
- `framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/service/DefaultFitTestService.java`
- `framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java`
依赖关系
- 前置:Issue 🐛 [Test] MockMvcListener 无限等待导致测试卡死 30+ 分钟 #395(添加超时兜底机制)应先完成
- 后续:完成后可以移除 🐛 [Test] MockMvcListener 无限等待导致测试卡死 30+ 分钟 #395 中的超时机制(或保留作为双重保障)
参考资料
确认事项
- 已分析根本原因(TOCTOU 竞态条件)
- 已识别影响范围
- 已评估工作量和风险
- 已设计多个可行方案
关联 Issue:#395(短期兜底机制修复)
标签建议:type: enhancement, in: fit, priority: medium-high