matrixorigin / matrixorigin/matrixone

[Bug]: shardservice Close 漏关 remote RPC server,返回后监听端口仍可连接

Open
#25,884 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] 已检索 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

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.