matrixorigin / matrixorigin/matrixone
[Bug]: shardservice 等待 CN 上报时不响应取消导致 Close 永久阻塞
- Dominant language
- Go
- Stars
- 1.9k
- Forks
- 311
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 768
Description
### 是否存在相同问题
- [x] 已检索现有 issue。
- 使用 `waitCNReported`、`shardservice close hang`、`CN reported shard service close`、`shard service stopper cancel` 等关键词检索,未发现相同问题。
### 分支与提交
- 分支:最新 `main`
- Commit:`826a3e3d294ba62c7c4da217d1445f3c568ce4d9`
- Commit 标题:`fix(colexec): short-circuit flow-control expressions (#25742)`
- 验证日期:2026-07-20
### 问题描述
生产 CN 初始化 shard service 时无条件传入 `shardservice.WithWaitCNReported()`:
```go
s.shardService = shardservice.NewService(
cfg,
store,
shardservice.WithWaitCNReported(),
)
```
该选项使 `service.doTask` 在开始 heartbeat 之前循环等待当前 CN 出现在 cluster service 中:
```go
if s.options.waitCNReported {
cs := clusterservice.GetMOCluster(s.cfg.ServiceID)
for {
reported := false
cs.GetCNServiceWithoutWorkingState(...)
if reported {
break
}
time.Sleep(time.Second)
}
}
```
循环没有检查 stopper 传入的 `ctx.Done()`,固定 `Sleep` 也不可取消。
`service.Close()` 首先执行 `s.stopper.Stop()`;Stopper 会取消 context,然后一直等待所有任务退出。因此,如果 CN 尚未完成上报就触发关闭,`doTask` 永远等待 CN 上报,`Close()` 也永远阻塞。
### 白盒测试过程与用例
测试不涉及事务或 2PC,直接覆盖 shard service 的启动等待和关闭所有权:
1. 初始化 cluster service,其中只有 `cn-already-reported`;
2. 创建待启动服务 `cn-waiting-for-report`,确保它不在 cluster 快照中;
3. 设置 `waitCNReported = true`,由真实 Stopper 启动 `service.doTask`;
4. 另起 goroutine 调用 `stopper.Stop()`;
5. 要求 Stop 在 200ms 内响应取消并返回;
6. 当前实现超时后,测试主动向 cluster 加入缺失 CN,仅用于解除错误实现的永久等待,避免测试泄漏 goroutine;
7. 观察到 Stop 只有在 CN 被强行加入后才返回。
核心断言:
```go
go func() {
s.stopper.Stop()
close(stopped)
}()
select {
case <-stopped:
// expected
case <-time.After(200 * time.Millisecond):
cluster.AddCN(metadata.CNService{ServiceID: missingServiceID})
<-stopped
t.Fatal("shard service stop did not cancel waitCNReported")
}
```
### 执行命令
原始代码重复复现:
```bash
go test ./pkg/shardservice \
-run '^TestAuditShardServiceStopCancelsWaitCNReported$' \
-count=3 -v
```
原始代码 race 复现:
```bash
go test -race ./pkg/shardservice \
-run '^TestAuditShardServiceStopCancelsWaitCNReported$' \
-count=1 -v
```
临时应用 context-aware 最小修正后的稳定性验证:
```bash
go test ./pkg/shardservice \
-run '^TestAuditShardServiceStopCancelsWaitCNReported$' \
-count=100
go test -race ./pkg/shardservice \
-run '^TestAuditShardServiceStopCancelsWaitCNReported$' \
-count=50
```
### 实际结果
原始代码每次都无法通过 stopper cancel 退出,只有测试补充 CN 后才解除阻塞:
```text
=== RUN TestAuditShardServiceStopCancelsWaitCNReported
wait_cn_reported_close_audit_test.go:65:
shard service stop did not cancel waitCNReported
--- FAIL: TestAuditShardServiceStopCancelsWaitCNReported (1.01s)
FAIL
```
- 原始代码普通测试:`3/3 FAIL`
- 原始代码 race 测试:`1/1 FAIL`,未发现数据竞争
临时在轮询前检查 `ctx.Done()`,并将固定 `Sleep` 改为可被 context 取消的等待后:
- 普通测试:`100/100 PASS`
- race 测试:`50/50 PASS`
验证完成后已撤销生产代码修正,仅保留本地未提交的审计测试;未提交、未推送任何代码。
### 期望结果
`waitCNReported` 的整个等待过程必须响应 stopper context。服务关闭时,即使当前 CN 从未出现在 cluster 快照中,后台任务也应立即退出,`service.Close()` 不应依赖未来的 CN 上报才能返回。
### 影响评估
- CN 在启动未完成、上报延迟或上报失败期间收到关闭信号时,shutdown 可能永久卡住;
- 初始化失败后的清理路径也可能无法完成;
- Stopper 只会周期打印“task still running”,不会强制结束任务,因此阻塞没有自动超时兜底;
- `WithWaitCNReported` 是生产 `cnservice.initShardService` 的默认启用路径。
Contributor guide
Assessment
This issue has not been assessed yet.