matrixorigin / matrixorigin/matrixone

[Bug]: shardservice 等待 CN 上报时不响应取消导致 Close 永久阻塞

Open
#25,876 0 comments 0 reactions 1 assignee Claimed by @XuPeng-SH View on GitHub
kind/bug needs-triage
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.