From ba7418570039c911bfb06e952fca5bea8e94275d Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 14 Jan 2026 14:12:49 -0800 Subject: [PATCH] fix: CompactMap race condition causing runtime panic (#8029) Fixed critical race condition in CompactMap where Set(), Delete(), and Get() methods had issues with concurrent map access. Root cause: segmentForKey() can create new map segments, which modifies the cm.segments map. Calling this under a read lock caused concurrent map write panics when multiple goroutines accessed the map simultaneously (e.g., during VolumeNeedleStatus gRPC calls). Changes: - Set() method: Changed RLock/RUnlock to Lock/Unlock - Delete() method: Changed RLock/RUnlock to Lock/Unlock, optimized to avoid creating empty segments when key doesn't exist - Get() method: Removed segmentForKey() call to avoid race condition, now checks segment existence directly and returns early if segment doesn't exist (optimization: avoids unnecessary segment creation) This fix resolves the runtime/maps.fatal panic that occurred under concurrent load. Tested with race detector: go test -v -race ./weed/storage/needle_map/... --- weed/storage/needle_map/compact_map.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/weed/storage/needle_map/compact_map.go b/weed/storage/needle_map/compact_map.go index c3365c788..d28b6c95c 100644 --- a/weed/storage/needle_map/compact_map.go +++ b/weed/storage/needle_map/compact_map.go @@ -236,8 +236,8 @@ func (cm *CompactMap) segmentForKey(key types.NeedleId) *CompactMapSegment { // Set inserts/updates a NeedleValue. // If the operation is an update, returns the overwritten value's previous offset and size. func (cm *CompactMap) Set(key types.NeedleId, offset types.Offset, size types.Size) (oldOffset types.Offset, oldSize types.Size) { - cm.RLock() - defer cm.RUnlock() + cm.Lock() + defer cm.Unlock() cs := cm.segmentForKey(key) return cs.set(key, offset, size) @@ -248,7 +248,11 @@ func (cm *CompactMap) Get(key types.NeedleId) (*NeedleValue, bool) { cm.RLock() defer cm.RUnlock() - cs := cm.segmentForKey(key) + chunk := Chunk(key / SegmentChunkSize) + cs, ok := cm.segments[chunk] + if !ok { + return nil, false + } if cnv, found := cs.get(key); found { nv := cnv.NeedleValue(cs.chunk) return &nv, true @@ -258,10 +262,14 @@ func (cm *CompactMap) Get(key types.NeedleId) (*NeedleValue, bool) { // Delete deletes a map entry by key. Returns the entries' previous Size, if available. func (cm *CompactMap) Delete(key types.NeedleId) types.Size { - cm.RLock() - defer cm.RUnlock() + cm.Lock() + defer cm.Unlock() - cs := cm.segmentForKey(key) + chunk := Chunk(key / SegmentChunkSize) + cs, ok := cm.segments[chunk] + if !ok { + return types.Size(0) + } return cs.delete(key) }