matrixorigin / matrixorigin/matrixone
[Bug]: shardservice Close 漏关 remote RPC server,返回后监听端口仍可连接
- Dominant language
- Go
- Stars
- 1.9k
- Forks
- 311
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 768
Description
### 是否存在相同问题
- [x] 已检索 open/closed issue。
- 检索关键词:`shardservice remote server Close`、`shard service listener close`、`remote.server Close shardservice`、`shardservice socket still listening`、`shardservice listener`。
- #25876 是等待 CN 上报时不响应取消导致 `Close` 阻塞;#25739 是 CN 关闭流程遇到早期错误后跳过后续子系统。本问题是 `shardservice.Close()` 正常返回后,其自身启动的 remote RPC server 仍在监听,根因和复现路径不同。
### 分支与提交
- 分支:最新 `main`
- Commit:`862d9d30c2cd7a55995fd0bae0951af0809f5752`
- Commit 时间:`2026-07-20T04:31:19Z`
- Commit 标题:`perf(plan): prune right SINGLE probe scans with runtime filters (#25865)`
- 验证日期:2026-07-20
### 问题描述
`NewService` 通过 `initRemote` 同时创建并启动 remote RPC client 和 server;server 拥有监听端口及服务 goroutine。当前 `service.Close()` 停止 stopper 并关闭内部 channel 后,只调用 `s.remote.client.Close()`:
```go
func (s *service) Close() error {
s.stopper.Stop()
close(s.createC)
close(s.deleteC)
return s.remote.client.Close()
}
```
`s.remote.server.Close()` 从未被调用。因此 `Close()` 返回 nil 后,服务启动的 Unix/TCP listener 仍然存活,新的连接仍可建立。
### 白盒测试过程与用例
该测试不涉及事务或 2PC,直接验证 shard service 对 remote server 的资源所有权:
1. 初始化测试 cluster/runtime;
2. 使用唯一的短 Unix socket 地址创建真实 `shardservice.Service`;
3. 在调用 `Close()` 前通过 `net.DialTimeout` 验证 remote server 已经开始监听;
4. 调用 `svc.Close()` 并确认返回 nil;
5. 再次连接同一个 socket,期望连接失败;
6. 当前实现第二次连接仍成功,证明 `Close()` 返回后 server 仍在接受连接;
7. 测试 cleanup 显式调用 `svc.remote.server.Close()`,仅用于避免失败复现遗留 listener/goroutine。
核心断言:
```go
before, err := net.DialTimeout("unix", socketPath, time.Second)
require.NoError(t, err)
require.NoError(t, before.Close())
require.NoError(t, svc.Close())
after, err := net.DialTimeout("unix", socketPath, 200*time.Millisecond)
if after != nil {
_ = after.Close()
}
require.Error(t, err,
"service.Close returned while the remote server was still accepting connections")
```
### 执行命令
原始代码重复复现:
```bash
go test ./pkg/shardservice \
-run '^TestAuditServiceCloseStopsRemoteServer$' \
-count=3 -v
```
原始代码 race 复现:
```bash
go test -race ./pkg/shardservice \
-run '^TestAuditServiceCloseStopsRemoteServer$' \
-count=1 -v
```
临时在 `Close()` 中同时关闭 remote server/client 后进行稳定性对照:
```bash
go test ./pkg/shardservice \
-run '^TestAuditServiceCloseStopsRemoteServer$' \
-count=100
go test -race ./pkg/shardservice \
-run '^TestAuditServiceCloseStopsRemoteServer$' \
-count=50
```
### 实际结果
原始代码:
```text
=== RUN TestAuditServiceCloseStopsRemoteServer
remote_server_close_audit_test.go:
Error: An error is expected but got nil.
Messages: service.Close returned while the remote server was still accepting connections
--- FAIL: TestAuditServiceCloseStopsRemoteServer
FAIL
```
- 普通测试:`3/3 FAIL`;
- race 测试:`1/1 FAIL`,同一逻辑断言失败,未报告数据竞争。
临时最小修正使用 `errors.Join(s.remote.server.Close(), s.remote.client.Close())`:
- 普通测试:`100/100 PASS`;
- race 测试:`50/50 PASS`。
验证完成后已经撤销临时生产代码修正;白盒测试仅保存在本地审计目录,未提交、未推送。
### 期望结果
`service.Close()` 应关闭由该 service 创建并启动的所有 remote 资源,包括 RPC server 和 client。返回后,原监听地址不应再接受新连接,并应释放 listener、socket 文件和相关 goroutine。
### 影响评估
- CN/shard service 关闭后仍可能接受发往旧实例的连接;
- 同地址重建或重启服务可能因旧 listener 未释放而失败;
- 测试、进程内重建和初始化失败清理路径会泄漏 listener 与 goroutine;
- `Close()` 返回成功会掩盖资源仍存活的真实状态。
Contributor guide
Assessment
This issue has not been assessed yet.