master: count only writable volumes as crowded when deciding growth (#10522)

The crowded map keeps volumes that later became unwritable, so their
state survives transient writability flips without flapping. But volumes
packed to capacity (fs.mergeVolumes) or turned read-only stay above the
crowded threshold and get re-marked on every heartbeat, so the raw map
size can permanently exceed the writable count. ShouldGrowVolumes then
returns true forever, every assign-path grow request passes the gate,
and the periodic grow loop fires too, creating volumes without bound --
worse with -volumePreallocate.

Count crowded as the intersection with writables instead: growth checks
and the layout gauges only see crowded volumes that can still take
writes.
This commit is contained in:
Chris Lu
2026-07-31 19:56:45 -07:00
committed by GitHub
parent 1ce106e69d
commit ef6a706c0e
2 changed files with 40 additions and 1 deletions
+11 -1
View File
@@ -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) {
+29
View File
@@ -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