mirror of
https://github.com/milvus-io/milvus.git
synced 2026-10-12 02:19:10 +00:00
`clearRedundantNodes` passes `lo.MapToSlice(c.mixcoord)`, `lo.MapToSlice(c.querynode)` and the three others as arguments to `go clearNodes(...)`. Arguments are evaluated in the calling goroutine, and `clearNodes` takes `c.mu` only inside its own body, so those five map reads happen with no lock held. A Milvus process that exits on its own runs the stop callback installed in `getOptions`, which calls `clearProcess` and deletes from the same maps while holding `c.mu`. A node stopping while `MiniClusterSuite.TearDownTest` runs `Reset` is therefore an unsynchronized map read against a concurrent delete: ``` cluster.go:391 clearRedundantNodes -> lo.MapToSlice(c.querynode) read, no lock cluster.go:739 clearProcess -> delete(c.querynode, nodeID) write, holds c.mu ``` ### Fix Take the snapshot of all five maps under `c.mu` before starting the goroutines. `clearNodes` still takes the lock itself, so the snapshot is released before any of them run and the locking discipline of the rest of the function is unchanged. Reading a node that `clearProcess` removes between the snapshot and the loop is harmless: `clearNodes` only calls `IsWorking`, `GetNodeID` and `Stop` on the pointer, which is what the code already did. ### Verification Reproduced with the race detector on an integration suite that adds a QueryNode and lets it stop before teardown. The race fires 2 out of 2 runs before this change and 0 out of 1 after, with the case passing. Twelve integration test files call `AddQueryNode`, so any of them can hit the same window; whether a given suite hits it depends on whether a node happens to be stopping when `Reset` runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: zhenshan.cao <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>