From 830b42eca6a86f931683fe8c3753e5470088055f Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 9 Mar 2026 01:20:18 -0700 Subject: [PATCH] feat: auto-configure max volume count when max=0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When -max=0, dynamically calculate max volume count based on free disk space, existing volumes, and EC shard count — matching Go's MaybeAdjustVolumeMax(). Recalculate on each heartbeat tick and when volume_size_limit changes from master. --- seaweed-volume/src/server/heartbeat.rs | 24 ++++++++-- seaweed-volume/src/storage/disk_location.rs | 18 +++++++ seaweed-volume/src/storage/store.rs | 52 +++++++++++++++++++++ 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/seaweed-volume/src/server/heartbeat.rs b/seaweed-volume/src/server/heartbeat.rs index 48d1a9355..587b01ea5 100644 --- a/seaweed-volume/src/server/heartbeat.rs +++ b/seaweed-volume/src/server/heartbeat.rs @@ -236,11 +236,21 @@ async fn do_heartbeat( match resp { Ok(Some(hb_resp)) => { if hb_resp.volume_size_limit > 0 { - let s = state.store.read().unwrap(); - s.volume_size_limit.store( - hb_resp.volume_size_limit, - std::sync::atomic::Ordering::Relaxed, - ); + let changed = { + let s = state.store.read().unwrap(); + s.volume_size_limit.store( + hb_resp.volume_size_limit, + std::sync::atomic::Ordering::Relaxed, + ); + s.maybe_adjust_volume_max() + }; + if changed { + let adjusted_hb = collect_heartbeat(config, state); + last_volumes = adjusted_hb.volumes.iter().map(|v| (v.id, v.clone())).collect(); + if tx.send(adjusted_hb).await.is_err() { + return Ok(None); + } + } } let metrics_changed = apply_metrics_push_settings( state, @@ -263,6 +273,10 @@ async fn do_heartbeat( } _ = volume_tick.tick() => { + { + let s = state.store.read().unwrap(); + s.maybe_adjust_volume_max(); + } let current_hb = collect_heartbeat(config, state); last_volumes = current_hb.volumes.iter().map(|v| (v.id, v.clone())).collect(); if tx.send(current_hb).await.is_err() { diff --git a/seaweed-volume/src/storage/disk_location.rs b/seaweed-volume/src/storage/disk_location.rs index cee6b4fdb..4cc3f080c 100644 --- a/seaweed-volume/src/storage/disk_location.rs +++ b/seaweed-volume/src/storage/disk_location.rs @@ -424,6 +424,24 @@ impl DiskLocation { self.volumes.iter_mut() } + /// Sum of unused space in writable volumes (volumeSizeLimit - actual size per volume). + /// Used by auto-max-volume-count to estimate how many more volumes can fit. + pub fn unused_space(&self, volume_size_limit: u64) -> u64 { + let mut unused: u64 = 0; + for vol in self.volumes.values() { + if vol.is_read_only() { + continue; + } + let dat_size = vol.dat_file_size().unwrap_or(0); + let idx_size = vol.idx_file_size(); + let used = dat_size + idx_size; + if volume_size_limit > used { + unused += volume_size_limit - used; + } + } + unused + } + /// Check disk space against min_free_space and update is_disk_space_low. pub fn check_disk_space(&self) { let (total, free) = get_disk_stats(&self.directory); diff --git a/seaweed-volume/src/storage/store.rs b/seaweed-volume/src/storage/store.rs index cad54bc33..1602b7ea9 100644 --- a/seaweed-volume/src/storage/store.rs +++ b/seaweed-volume/src/storage/store.rs @@ -373,6 +373,58 @@ impl Store { .sum() } + /// Total EC shard count across all EC volumes. + pub fn ec_shard_count(&self) -> usize { + self.ec_volumes + .values() + .map(|ecv| ecv.shards.iter().filter(|s| s.is_some()).count()) + .sum() + } + + /// Recalculate max volume counts for locations with original_max_volume_count == 0. + /// Returns true if any max changed (caller should re-send heartbeat). + pub fn maybe_adjust_volume_max(&self) -> bool { + let volume_size_limit = self.volume_size_limit.load(Ordering::Relaxed); + if volume_size_limit == 0 { + return false; + } + + let mut has_changes = false; + let mut new_max_total: i32 = 0; + + let total_ec_shards = self.ec_shard_count(); + + for loc in &self.locations { + if loc.original_max_volume_count == 0 { + let current = loc.max_volume_count.load(Ordering::Relaxed); + let (_, free) = super::disk_location::get_disk_stats(&loc.directory); + + let unused_space = loc.unused_space(volume_size_limit); + let unclaimed = (free as i64) - (unused_space as i64); + + let vol_count = loc.volumes_len() as i32; + let ec_equivalent = ((total_ec_shards + + crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT) + / crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT) + as i32; + let mut max_count = vol_count + ec_equivalent; + + if unclaimed > volume_size_limit as i64 { + max_count += (unclaimed as u64 / volume_size_limit) as i32 - 1; + } + + loc.max_volume_count.store(max_count, Ordering::Relaxed); + new_max_total += max_count; + has_changes = has_changes || current != max_count; + } else { + new_max_total += loc.original_max_volume_count; + } + } + + crate::metrics::MAX_VOLUMES.set(new_max_total as i64); + has_changes + } + /// Free volume slots across all locations. pub fn free_volume_count(&self) -> i32 { self.locations