Skip to content

🐛 [Test] MockMvcListener 无限等待导致测试卡死 30+ 分钟 #395

@CodeCasterX

Description

@CodeCasterX

问题摘要 / Issue Summary

MockMvcListener.beforeTestClass() 中的服务器启动等待逻辑缺少超时机制,当 MockMvc 服务器启动失败时会导致测试无限等待,卡死 30+ 分钟。

版本信息 / Version Information

v3.6.2-SNAPSHOT (分支: fit-enhancement-fastjson-upgrade, Commit: e08cb48)

操作系统 / Operating System

macOS (Apple Silicon)

发生了什么?/ What happened?

在执行 `mvn clean install` 进行完整构建时,测试在 `fit-validation-hibernate-jakarta` 模块的 `ValidationDataControllerTest` 处卡死,已持续运行 30+ 分钟仍未结束。

通过 `jstack` 分析发现主线程卡在无限循环中:

```
"main" #1 prio=5 os_prio=31 cpu=23035.57ms elapsed=1814.34s tid=0x00007fed3e009a00 nid=0x1503 waiting on condition
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep([email protected]/Native Method)
at modelengine.fitframework.util.ThreadUtils.sleep(ThreadUtils.java:55)
at modelengine.fitframework.test.domain.listener.MockMvcListener.beforeTestClass(MockMvcListener.java:76)
```

已等待 1814.34 秒(约 30 分钟),正常的 Maven 构建应在 8-15 分钟内完成。

期望的行为 / Expected Behavior

  1. MockMvc 服务器启动应该有超时机制(建议 30-60 秒)
  2. 超时后应抛出清晰的异常信息,说明服务器启动失败的原因
  3. 测试应该快速失败(fail-fast),而不是无限等待

根本原因分析 / Root Cause Analysis

问题代码位置
`framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java:75-78`

```java
boolean started = this.isStarted(mockMvc);
while (!started) {
ThreadUtils.sleep(100);
started = this.isStarted(mockMvc);
}
```

问题

  • ❌ 没有超时限制的无限循环
  • ❌ 如果 `isStarted()` 持续返回 `false`,循环将永远执行
  • ❌ 没有错误信息输出,难以诊断问题

可能导致启动失败的原因

  1. 端口被占用(默认端口可能与其他服务冲突)
  2. 系统资源不足
  3. 防火墙阻止本地连接
  4. MockController 未正确注册或启动

重现步骤 / Steps to Reproduce

稳定重现(推测)

  1. 占用 MockMvc 使用的默认端口
  2. 运行任何使用 `@MvcTest` 注解的测试
  3. 测试将卡在 `beforeTestClass` 阶段

偶发重现(本次遇到)

  1. 执行 `mvn clean install`
  2. 等待构建进行到 `fit-validation-hibernate-jakarta` 模块
  3. 测试在 `ValidationDataControllerTest` 处卡死

重现频率:偶发(本次在第 145/160 个模块时发生)

相关日志 / Relevant Logs

Maven 构建日志

```
[INFO] Building FIT Hibernate Validation Jakarta 3.6.2-SNAPSHOT [145/160]
[INFO] --- surefire:3.5.3:test (default-test) @ fit-validation-hibernate-jakarta ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running modelengine.fitframework.validation.ValidationDataControllerTest
12月 31, 2025 10:10:40 上午 org.hibernate.validator.internal.util.Version
INFO: HV000001: Hibernate Validator 9.0.1.Final

之后无任何输出,卡死在此处

```

进程信息

```bash

Surefire 进程已运行 30+ 分钟

$ ps -p 10934 -o etime=
34:40

线程堆栈显示在无限循环中

$ jstack 10934
"main" #1 ... elapsed=1814.34s ... TIMED_WAITING (sleeping)
at modelengine.fitframework.test.domain.listener.MockMvcListener.beforeTestClass(MockMvcListener.java:76)
```

构建统计

  • 构建开始时间:10:10:40
  • 当前时间:10:40+
  • 卡住时长:30+ 分钟
  • 正常构建时长:8-15 分钟

建议的修复方案 / Suggested Fix

方案 1:添加超时机制(推荐)

```java
@OverRide
public void beforeTestClass(TestContext context) {
Class<?> testClass = context.testClass();
if (AnnotationUtils.getAnnotation(testClass, EnableMockMvc.class).isEmpty()) {
return;
}
MockMvc mockMvc = new MockMvc(this.port);
context.plugin().container().registry().register(mockMvc);

// 添加超时机制
long startTime = System.currentTimeMillis();
long timeout = 30_000; // 30 秒超时
boolean started = this.isStarted(mockMvc);

while (!started) {
    if (System.currentTimeMillis() - startTime > timeout) {
        throw new IllegalStateException(StringUtils.format(
            "MockMvc server failed to start within {0}ms. [port={1}]",
            timeout, this.port));
    }
    ThreadUtils.sleep(100);
    started = this.isStarted(mockMvc);
}

}
```

方案 2:增强错误诊断

```java
private boolean isStarted(MockMvc mockMvc) {
MockRequestBuilder builder = MockMvcRequestBuilders.get(MockController.PATH).responseType(String.class);
try (HttpClassicClientResponse response = mockMvc.perform(builder)) {
String content = response.textEntity()
.map(TextEntity::content)
.orElseThrow(() -> new IllegalStateException(StringUtils.format(
"Failed to start mock http server. [port={0}]",
this.port)));
return Objects.equals(content, MockController.OK);
} catch (IOException | ClientException e) {
// 记录详细的失败原因
logger.debug("MockMvc health check failed: {}", e.getMessage());
return false;
}
}
```

方案 3:配置化端口(避免冲突)

允许通过配置或环境变量指定 MockMvc 端口,避免端口冲突导致启动失败。

影响范围 / Impact

  • 严重性:中高(Medium-High)
  • 影响模块:所有使用 `@MvcTest` 注解的测试类
  • 影响场景
    • ✅ CI/CD 流水线可能卡死
    • ✅ 本地开发时构建时间异常
    • ✅ 难以诊断和调试

额外信息 / Additional Context

当前环境信息

  • Java 版本:OpenJDK 17.0.17
  • Maven 版本:3.9.12
  • 测试框架:JUnit 5 (JUnit Platform)
  • Surefire 版本:3.5.3

相关测试

  • 测试类:`ValidationDataControllerTest`
  • 测试注解:`@MvcTest(classes = {ValidationDataController.class})`
  • 模块:`fit-validation-hibernate-jakarta` (145/160)

临时解决方案

手动终止卡住的测试进程:
```bash

查找 Surefire 进程

jps -l | grep surefire

终止进程

kill -9
```


确认事项 / Confirmations

  • 我已经搜索了现有的 issues,确认这不是重复问题
  • 我已经阅读了项目文档和 FAQ
  • 我提供的信息准确完整
  • 我愿意协助测试问题修复

Metadata

Metadata

Assignees

Labels

in: fitIssues in FIT modulestype: bugA general bug

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions