diff --git a/weed/topology/volume_layout.go b/weed/topology/volume_layout.go index 71b2d4a52..6cc58f1b6 100644 --- a/weed/topology/volume_layout.go +++ b/weed/topology/volume_layout.go @@ -783,7 +783,17 @@ func ceilDiv(a, b uint32) uint32 { func (vl *VolumeLayout) GetWritableVolumeCount() (active, crowded int) { vl.accessLock.RLock() defer vl.accessLock.RUnlock() - return len(vl.writables), len(vl.crowded) + // The crowded map retains volumes that later became unwritable (full, + // read-only), so their state survives transient writability flips. Count + // only the writable ones: growth decisions compare crowded against + // writables, and a raw len(vl.crowded) can exceed len(vl.writables) + // permanently, demanding growth forever. + for _, vid := range vl.writables { + if _, ok := vl.crowded[vid]; ok { + crowded++ + } + } + return len(vl.writables), crowded } func (vl *VolumeLayout) CloneWritableVolumes() (writables []needle.VolumeId) { diff --git a/weed/topology/volume_layout_grow_test.go b/weed/topology/volume_layout_grow_test.go index 7e00375c2..83fb2d6f2 100644 --- a/weed/topology/volume_layout_grow_test.go +++ b/weed/topology/volume_layout_grow_test.go @@ -240,6 +240,35 @@ func TestPlanRackAwareGrowth_EvenDistributionAcrossUnevenDCs(t *testing.T) { } } +// Volumes packed to capacity (e.g. by fs.mergeVolumes) go crowded and then +// unwritable, but stay in the crowded map. ShouldGrowVolumes must count only +// writable crowded volumes, or those leftovers keep writable <= crowded true +// forever and every assign-path grow request passes the gate. +func TestShouldGrowVolumes_UnwritableCrowdedVolumes(t *testing.T) { + rp, _ := super_block.NewReplicaPlacementFromString("000") + vl := NewVolumeLayout(rp, needle.EMPTY_TTL, types.HardDriveType, 30000, false) + + vl.accessLock.Lock() + vl.setVolumeWritable(1) + vl.setVolumeWritable(2) + vl.accessLock.Unlock() + + vl.SetVolumeCrowded(1) + vl.SetVolumeCapacityFull(1) + + if _, crowded := vl.GetWritableVolumeCount(); crowded != 0 { + t.Fatalf("expected 0 writable crowded volumes, got %d", crowded) + } + if vl.ShouldGrowVolumes() { + t.Fatal("volume 2 still has room, growth is not needed") + } + + vl.SetVolumeCrowded(2) + if !vl.ShouldGrowVolumes() { + t.Fatal("every writable volume is crowded, growth is needed") + } +} + func restoreCopyCounts(copy1, copy2 uint32) { VolumeGrowStrategy.Copy1Count = copy1 VolumeGrowStrategy.Copy2Count = copy2