This is an automated email from the ASF dual-hosted git repository.
sammichen pushed a commit to branch HDDS-5713
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-5713 by this push:
new 4232d2163c8 HDDS-14213. Fix DiskBalancer VolumeChoosingPolicy to
balance when dest volume exists but no src volume exceeds threshold (#9587)
4232d2163c8 is described below
commit 4232d2163c8f21cb59d8cc77426b3cdbf645ca89
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Wed Jan 7 17:52:19 2026 +0530
HDDS-14213. Fix DiskBalancer VolumeChoosingPolicy to balance when dest
volume exists but no src volume exceeds threshold (#9587)
---
.../policy/DefaultVolumeChoosingPolicy.java | 74 ++-
.../TestDefaultVolumeChoosingPolicy.java | 652 +++++++++++++++++++++
.../ozone/scm/node/TestVolumeChoosingPolicy.java | 48 +-
3 files changed, 751 insertions(+), 23 deletions(-)
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/diskbalancer/policy/DefaultVolumeChoosingPolicy.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/diskbalancer/policy/DefaultVolumeChoosingPolicy.java
index c994af442cd..240c5a9f9f0 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/diskbalancer/policy/DefaultVolumeChoosingPolicy.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/diskbalancer/policy/DefaultVolumeChoosingPolicy.java
@@ -26,6 +26,7 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.hdds.fs.SpaceUsageSource;
import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
@@ -67,21 +68,48 @@ public Pair<HddsVolume, HddsVolume>
chooseVolume(MutableVolumeSet volumeSet,
.sorted(Comparator.comparingDouble(VolumeFixedUsage::getUtilization))
.collect(Collectors.toList());
- // Calculate the actual threshold and check src
- final double actualThreshold = getIdealUsage(volumeUsages) +
thresholdPercentage / 100;
- final VolumeFixedUsage src = volumeUsages.get(volumeUsages.size() - 1);
- if (src.getUtilization() < actualThreshold) {
- return null; // all volumes are under the threshold
+ // Calculate ideal usage and threshold range
+ final double idealUsage = getIdealUsage(volumeUsages);
+ final double actualThreshold = thresholdPercentage / 100.0;
+ final double lowerThreshold = idealUsage - actualThreshold;
+ final double upperThreshold = idealUsage + actualThreshold;
+
+ // Log all volume information for investigation
+ if (LOG.isDebugEnabled()) {
+ logVolumeBalancingState(volumeUsages, idealUsage, thresholdPercentage,
+ lowerThreshold, upperThreshold, containerSize, deltaMap);
+ }
+
+ // Get highest and lowest utilization volumes
+ final VolumeFixedUsage highestUsage =
volumeUsages.get(volumeUsages.size() - 1);
+ final VolumeFixedUsage lowestUsage = volumeUsages.get(0);
+
+ // Only return null if highest is below upper threshold AND lowest is
above lower threshold
+ // This means all volumes are strictly within the range (not at
boundaries)
+ if (highestUsage.getUtilization() < upperThreshold &&
+ lowestUsage.getUtilization() > lowerThreshold) {
+ // All volumes are strictly within threshold range, no balancing needed
+ return null;
}
- // Find dst
+ // Determine source volume: highest utilization volume (if above
threshold)
+ final VolumeFixedUsage src = highestUsage;
+
+ // Find destination volume: lowest utilization volume that has enough
space
+ // Prefer volumes below threshold, but accept any volume with lower
utilization than source
for (int i = 0; i < volumeUsages.size() - 1; i++) {
final VolumeFixedUsage dstUsage = volumeUsages.get(i);
final HddsVolume dst = dstUsage.getVolume();
- if (containerSize < dstUsage.computeUsableSpace()) {
+ // Check if destination has enough space and has lower utilization
than source
+ if (dstUsage.getUtilization() < src.getUtilization() &&
+ containerSize < dstUsage.computeUsableSpace()) {
// Found dst, reserve space and return
dst.incCommittedBytes(containerSize);
+ LOG.debug("Chosen volume pair for disk balancing: source={}
(utilization={}), " +
+ "destination={} (utilization={})",
+ src.getVolume().getStorageID(), src.getUtilization(),
+ dst.getStorageID(), dstUsage.getUtilization());
return Pair.of(src.getVolume(), dst);
}
LOG.debug("Destination volume {} does not have enough space, trying
next volume.",
@@ -93,4 +121,36 @@ public Pair<HddsVolume, HddsVolume>
chooseVolume(MutableVolumeSet volumeSet,
lock.unlock();
}
}
+
+ /**
+ * Logs all volume information for disk balancing investigation.
+ *
+ * @param volumeUsages List of volume usages (sorted by utilization
ascending)
+ * @param idealUsage Calculated ideal usage
+ * @param thresholdPercentage Threshold percentage
+ * @param lowerThreshold Lower threshold bound
+ * @param upperThreshold Upper threshold bound
+ * @param containerSize Container size to be moved
+ * @param deltaMap Map of volume deltas
+ */
+ private void logVolumeBalancingState(List<VolumeFixedUsage> volumeUsages,
+ double idealUsage, double thresholdPercentage, double lowerThreshold,
+ double upperThreshold, long containerSize, Map<HddsVolume, Long>
deltaMap) {
+ LOG.debug("Disk balancing state - idealUsage={}, thresholdPercentage={}%,
" +
+ "thresholdRange=({}, {}), containerSize={}",
+ String.format("%.10f", idealUsage), thresholdPercentage,
+ String.format("%.10f", lowerThreshold), String.format("%.10f",
upperThreshold),
+ containerSize);
+ for (int i = 0; i < volumeUsages.size(); i++) {
+ VolumeFixedUsage vfu = volumeUsages.get(i);
+ HddsVolume vol = vfu.getVolume();
+ SpaceUsageSource.Fixed usage = vfu.getUsage();
+ long usableSpace = vfu.computeUsableSpace();
+ LOG.debug("Volume[{}] - disk={}, utilization={}, capacity={}, " +
+ "effectiveUsed={}, available={}, usableSpace={},
committedBytes={}, delta={}",
+ i, vol.getStorageID(), String.format("%.10f", vfu.getUtilization()),
+ usage.getCapacity(), vfu.getEffectiveUsed(), usage.getAvailable(),
+ usableSpace, vol.getCommittedBytes(), deltaMap.getOrDefault(vol,
0L));
+ }
+ }
}
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/diskbalancer/TestDefaultVolumeChoosingPolicy.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/diskbalancer/TestDefaultVolumeChoosingPolicy.java
new file mode 100644
index 00000000000..8a93a1005de
--- /dev/null
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/diskbalancer/TestDefaultVolumeChoosingPolicy.java
@@ -0,0 +1,652 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.container.diskbalancer;
+
+import static
org.apache.hadoop.ozone.container.diskbalancer.DiskBalancerVolumeCalculation.getVolumeUsages;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.fs.MockSpaceUsageCheckFactory;
+import org.apache.hadoop.hdds.fs.MockSpaceUsageSource;
+import org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory;
+import org.apache.hadoop.hdds.fs.SpaceUsagePersistence;
+import org.apache.hadoop.hdds.fs.SpaceUsageSource;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
+import
org.apache.hadoop.ozone.container.diskbalancer.DiskBalancerVolumeCalculation.VolumeFixedUsage;
+import
org.apache.hadoop.ozone.container.diskbalancer.policy.DefaultVolumeChoosingPolicy;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Unit tests for DefaultVolumeChoosingPolicy.
+ */
+public class TestDefaultVolumeChoosingPolicy {
+
+ @TempDir
+ private Path baseDir;
+
+ private static final long MB = 1024L * 1024L;
+ private static final long VOLUME_CAPACITY = 2500L * MB; // 2500MB - same for
all volumes
+ private static final long DEFAULT_CONTAINER_SIZE = 100L * MB; // 100MB
+ private DefaultVolumeChoosingPolicy policy;
+ private MutableVolumeSet volumeSet;
+ private String datanodeUuid;
+ private Map<HddsVolume, Long> deltaMap;
+
+ @BeforeEach
+ public void setup() {
+ datanodeUuid = UUID.randomUUID().toString();
+ policy = new DefaultVolumeChoosingPolicy(new ReentrantLock());
+ deltaMap = new HashMap<>();
+ }
+
+ /**
+ * Test case data structure for volume configuration.
+ */
+ public static class VolumeTestConfig {
+ private final String name;
+ private final double utilization;
+ private final Long customCapacity; // null means use default
VOLUME_CAPACITY
+
+ public VolumeTestConfig(String name, double utilization) {
+ this.name = name;
+ this.utilization = utilization;
+ this.customCapacity = null;
+ }
+
+ public VolumeTestConfig(String name, double utilization, long
customCapacity) {
+ this.name = name;
+ this.utilization = utilization;
+ this.customCapacity = customCapacity;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public double getUtilization() {
+ return utilization;
+ }
+
+ public Long getCustomCapacity() {
+ return customCapacity;
+ }
+ }
+
+ /**
+ * Test scenario configuration.
+ */
+ public static class TestScenario {
+ private final String name;
+ private final List<VolumeTestConfig> volumes;
+ private final double thresholdPercentage;
+ private final long containerSize;
+ private final boolean shouldFindPair;
+ private final String expectedSourceDisk;
+ private final String expectedDestinationDisk;
+ private final Integer expectedSourceIndex;
+ private final Integer expectedDestinationIndex;
+
+ @SuppressWarnings("checkstyle:parameternumber")
+ public TestScenario(String name, List<VolumeTestConfig> volumes, double
thresholdPercentage,
+ long containerSize, boolean shouldFindPair, String expectedSourceDisk,
String expectedDestinationDisk,
+ Integer expectedSourceIndex, Integer expectedDestinationIndex) {
+ this.name = name;
+ this.volumes = volumes;
+ this.thresholdPercentage = thresholdPercentage;
+ this.containerSize = containerSize;
+ this.shouldFindPair = shouldFindPair;
+ this.expectedSourceDisk = expectedSourceDisk;
+ this.expectedDestinationDisk = expectedDestinationDisk;
+ this.expectedSourceIndex = expectedSourceIndex;
+ this.expectedDestinationIndex = expectedDestinationIndex;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<VolumeTestConfig> getVolumes() {
+ return volumes;
+ }
+
+ public double getThresholdPercentage() {
+ return thresholdPercentage;
+ }
+
+ public long getContainerSize() {
+ return containerSize;
+ }
+
+ public boolean shouldFindPair() {
+ return shouldFindPair;
+ }
+
+ public String getExpectedSourceDisk() {
+ return expectedSourceDisk;
+ }
+
+ public String getExpectedDestinationDisk() {
+ return expectedDestinationDisk;
+ }
+
+ public Integer getExpectedSourceIndex() {
+ return expectedSourceIndex;
+ }
+
+ public Integer getExpectedDestinationIndex() {
+ return expectedDestinationIndex;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * Creates a volume with specific utilization and capacity.
+ *
+ * @param name Volume name/identifier
+ * @param utilization Utilization as a double (0.0 to 1.0, representing
percentage)
+ * @param capacity capacity for the volume
+ * @return HddsVolume with specified utilization and capacity
+ */
+ private HddsVolume createVolume(String name, double utilization, long
capacity)
+ throws IOException {
+ long usedSpace = (long) (capacity * utilization);
+ Path volumePath = baseDir.resolve(name);
+
+ // Create a configuration without reserved space to avoid capacity
adjustments
+ OzoneConfiguration volumeConf = new OzoneConfiguration();
+ volumeConf.setFloat("hdds.datanode.dir.du.reserved.percent", 0.0f);
+
+ SpaceUsageSource source = MockSpaceUsageSource.fixed(capacity,
+ capacity - usedSpace);
+ SpaceUsageCheckFactory factory = MockSpaceUsageCheckFactory.of(
+ source, Duration.ZERO, SpaceUsagePersistence.None.INSTANCE);
+
+ HddsVolume volume = new HddsVolume.Builder(volumePath.toString())
+ .conf(volumeConf)
+ .usageCheckFactory(factory)
+ .build();
+ return volume;
+ }
+
+ /**
+ * Creates volumes from test configuration.
+ *
+ * @param configs List of volume configurations
+ * @return List of created HddsVolumes
+ */
+ private List<HddsVolume> createVolumes(List<VolumeTestConfig> configs)
+ throws IOException {
+ List<HddsVolume> volumes = new ArrayList<>();
+ for (VolumeTestConfig config : configs) {
+ long capacity = config.getCustomCapacity() != null ?
+ config.getCustomCapacity() : VOLUME_CAPACITY;
+ HddsVolume volume = createVolume(config.getName(),
config.getUtilization(), capacity);
+ volumes.add(volume);
+ }
+ return volumes;
+ }
+
+ /**
+ * Sets up volume set with given volumes.
+ *
+ * @param volumes List of volumes to add to volume set
+ */
+ private void setupVolumeSet(List<HddsVolume> volumes) throws IOException {
+ // Create volume set without any default volumes from configuration
+ // Use a clean configuration to avoid loading default volumes
+ OzoneConfiguration testConf = new OzoneConfiguration();
+ // Explicitly set HDDS_DATANODE_DIR_KEY to empty to prevent default volumes
+ testConf.set("hdds.datanode.dir.key", "");
+ volumeSet = new MutableVolumeSet(datanodeUuid, testConf, null,
+ StorageVolume.VolumeType.DATA_VOLUME, null);
+
+ // Use setVolumeMapForTesting to set only our test volumes
+ // This replaces the entire volumeMap, removing any default volumes from
configuration
+ Map<String, StorageVolume> volumeMap = new HashMap<>();
+ for (HddsVolume volume : volumes) {
+ volumeMap.put(volume.getStorageDir().getAbsolutePath(), volume);
+ }
+ volumeSet.setVolumeMapForTesting(volumeMap);
+ }
+
+ /**
+ * Generic test method that can be reused for different scenarios.
+ *
+ * @param scenario Test scenario configuration
+ */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("testScenarios")
+ public void testVolumeChoosingPolicy(TestScenario scenario)
+ throws IOException {
+ // Create volumes from configuration
+ List<HddsVolume> volumes = createVolumes(scenario.getVolumes());
+ setupVolumeSet(volumes);
+
+ // Create a map of disk names to volumes for verification
+ Map<String, HddsVolume> diskNameToVolume = new HashMap<>();
+ for (int i = 0; i < scenario.getVolumes().size(); i++) {
+ VolumeTestConfig config = scenario.getVolumes().get(i);
+ diskNameToVolume.put(config.getName(), volumes.get(i));
+ }
+
+ // Get volume usages for verification
+ List<VolumeFixedUsage> volumeUsages = getVolumeUsages(volumeSet, deltaMap);
+
+ // Try to find a valid source-destination pair
+ Pair<HddsVolume, HddsVolume> result = policy.chooseVolume(volumeSet,
+ scenario.getThresholdPercentage(), deltaMap,
scenario.getContainerSize());
+
+ if (scenario.shouldFindPair()) {
+ assertNotNull(result);
+ assertNotNull(result.getLeft());
+ assertNotNull(result.getRight());
+
+ // Verify source is the expected disk
+ if (scenario.getExpectedSourceDisk() != null) {
+ HddsVolume expectedSource =
diskNameToVolume.get(scenario.getExpectedSourceDisk());
+ assertNotNull(expectedSource);
+ assertEquals(expectedSource, result.getLeft());
+ }
+
+ // Verify destination is the expected disk (or one of the valid options)
+ if (scenario.getExpectedDestinationDisk() != null) {
+ HddsVolume expectedDest =
diskNameToVolume.get(scenario.getExpectedDestinationDisk());
+ assertNotNull(expectedDest);
+ assertEquals(expectedDest, result.getRight());
+ }
+
+ // Filter volumeUsages to only include volumes from our test scenario
+ // This excludes any extra volumes that might be added from default
configuration
+ List<VolumeFixedUsage> testVolumeUsages = new ArrayList<>();
+ for (VolumeFixedUsage usage : volumeUsages) {
+ if (diskNameToVolume.containsValue(usage.getVolume())) {
+ testVolumeUsages.add(usage);
+ }
+ }
+ // Sort by utilization to ensure consistent ordering
+
testVolumeUsages.sort(Comparator.comparingDouble(VolumeFixedUsage::getUtilization));
+
+ // Verify source and destination indices match expected values
+ // Since volumes in TestScenario are ordered from lowest to highest
utilization,
+ // and testVolumeUsages are sorted ascending (lowest to highest),
+ // we can verify that the selected volumes are at the expected indices
+ int sourceIndex = -1;
+ int destIndex = -1;
+ for (int i = 0; i < testVolumeUsages.size(); i++) {
+ if (testVolumeUsages.get(i).getVolume().equals(result.getLeft())) {
+ sourceIndex = i;
+ }
+ if (testVolumeUsages.get(i).getVolume().equals(result.getRight())) {
+ destIndex = i;
+ }
+ }
+ assertTrue(sourceIndex >= 0);
+ assertTrue(destIndex >= 0);
+
+ // Verify source is at the expected index (should be the highest
utilization)
+ if (scenario.getExpectedSourceIndex() != null) {
+ assertEquals(scenario.getExpectedSourceIndex(), sourceIndex);
+
+ // Verify that source has the highest utilization
+ double sourceUtilization =
testVolumeUsages.get(sourceIndex).getUtilization();
+ for (int i = 0; i < testVolumeUsages.size(); i++) {
+ if (i != sourceIndex) {
+ double otherUtilization = testVolumeUsages.get(i).getUtilization();
+ assertTrue(sourceUtilization >= otherUtilization);
+ }
+ }
+ }
+
+ // Verify destination is at the expected index (should be the lowest
utilization among valid destinations)
+ if (scenario.getExpectedDestinationIndex() != null) {
+ assertEquals(scenario.getExpectedDestinationIndex(), destIndex);
+
+ // Verify that destination has lower utilization than source
+ double destUtilization =
testVolumeUsages.get(destIndex).getUtilization();
+ double sourceUtilization =
testVolumeUsages.get(sourceIndex).getUtilization();
+ assertTrue(destUtilization < sourceUtilization);
+
+ // Verify that no volume with lower index (lower utilization) has
sufficient space
+ // If a volume at a lower index had sufficient space, it would have
been chosen instead
+ for (int i = 0; i < destIndex; i++) {
+ VolumeFixedUsage lowerUtilUsage = testVolumeUsages.get(i);
+ long usableSpace = lowerUtilUsage.computeUsableSpace();
+ // Policy checks: containerSize < computeUsableSpace() to see if
there's enough space
+ // So if containerSize >= usableSpace, there's NOT enough space
(correct - volume wasn't chosen)
+ assertTrue(scenario.getContainerSize() >= usableSpace);
+ }
+ }
+ } else {
+ assertNull(result);
+ }
+ }
+
+ /**
+ * Provides test scenarios for parameterized testing.
+ */
+ @SuppressWarnings("checkstyle:methodlength")
+ public static Stream<Arguments> testScenarios() {
+ return Stream.of(
+ // Scenario 1: One volume beyond threshold, no volumes under threshold
+ // Disk1: 30%, Disk2: 30%, Disk3: 40%, Threshold: 5%
+ // Ideal: 33.33%, Range: (28.33%, 38.33%), Out of range: Disk3
+ // Expected source: Disk3 (highest) at index 2, Expected destination:
Disk1 or Disk2 (lowest) at index 0 or 1
+ Arguments.arguments(new TestScenario(
+ "OneVolumeBeyondThresholdNoVolumesUnderThreshold",
+ Arrays.asList(
+ new VolumeTestConfig("disk1", 0.30), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk2", 0.30), // Same as disk1 - index
1
+ new VolumeTestConfig("disk3", 0.40) // Highest utilization -
index 2
+ ),
+ 5.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk3", // Expected source (highest)
+ null, // Destination can be disk1 or disk2 (both valid) so
only check source
+ 2, // Expected source index (highest utilization)
+ null // Destination index can be 0 or 1 (both have same
utilization)
+ )),
+
+ // Scenario 2: Volumes both above and below threshold
+ // Disk1: 90%, Disk2: 85%, Disk3: 15%, Threshold: 10%
+ // Ideal: 63.33%, Range: (53.33%, 73.33%), Out of range: Disk1, Disk2,
Disk3
+ // Expected source: Disk1 (highest) at index 2, Expected destination:
Disk3 (lowest) at index 0
+ Arguments.arguments(new TestScenario(
+ "VolumesAboveAndBelowThreshold",
+ Arrays.asList(
+ new VolumeTestConfig("disk3", 0.15), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk2", 0.85), // Middle utilization -
index 1
+ new VolumeTestConfig("disk1", 0.90) // Highest utilization -
index 2
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk1", // Expected source (highest)
+ "disk3", // Expected destination (lowest)
+ 2, // Expected source index (highest utilization)
+ 0 // Expected destination index (lowest utilization)
+ )),
+
+ // Scenario 3: All volumes within threshold
+ // Disk1: 30%, Disk2: 30%, Disk3: 33%, Threshold: 10%
+ // Ideal: 31%, Range: (21%, 41%), Out of range: None
+ // Expected source: None, Expected destination: None
+ Arguments.arguments(new TestScenario(
+ "AllVolumesWithinThreshold",
+ Arrays.asList(
+ new VolumeTestConfig("disk1", 0.30), // Lowest utilization
+ new VolumeTestConfig("disk2", 0.30), // Same as disk1
+ new VolumeTestConfig("disk3", 0.33) // Highest utilization
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ false,
+ null, // No pair expected
+ null,
+ null, // No source index expected
+ null // No destination index expected
+ )),
+
+ // Scenario 4: One volume under threshold, no volumes above threshold
+ // Disk1: 30%, Disk2: 30%, Disk3: 20%
+ // Ideal: 26.67%, Range: (21.67%, 31.67%), Out of range: Disk3
+ // Expected source: Disk1 or Disk2 (highest) at index 1 or 2, Expected
destination: Disk3 (lowest) at index 0
+ Arguments.arguments(new TestScenario(
+ "OneVolumeUnderThresholdNoVolumesAbove",
+ Arrays.asList(
+ new VolumeTestConfig("disk3", 0.20), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk1", 0.30), // Middle utilization -
index 1
+ new VolumeTestConfig("disk2", 0.30) // Highest utilization
(tied with disk1) - index 2
+ ),
+ 5.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ null, // Source can be disk1 or disk2 (both valid, highest)
+ "disk3", // Expected destination (lowest)
+ null, // Source index can be 1 or 2 (both have same
utilization)
+ 0 // Expected destination index (lowest utilization)
+ )),
+
+ // Scenario 5: Extreme imbalance - one very high, others very low
+ // Disk1: 95%, Disk2: 5%, Disk3: 5%, Threshold: 10%
+ // Ideal: 35%, Range: (25%, 45%), Out of range: Disk1, Disk2, Disk3
+ // Expected source: Disk1 (highest) at index 2, Expected destination:
Disk2 or Disk3 (lowest) at index 0 or 1
+ Arguments.arguments(new TestScenario(
+ "ExtremeImbalance",
+ Arrays.asList(
+ new VolumeTestConfig("disk2", 0.05), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk3", 0.05), // Same as disk2 - index
1
+ new VolumeTestConfig("disk1", 0.95) // Highest utilization -
index 2
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk1", // Expected source (highest)
+ null, // Destination can be disk2 or disk3 (lowest)
+ 2, // Expected source index (highest utilization)
+ null // Destination index can be 0 or 1 (both have same
utilization)
+ )),
+
+ // Scenario 6: Multiple volumes above threshold, one below
+ // Disk1: 80%, Disk2: 75%, Disk3: 20%, Threshold: 10%
+ // Ideal: 58.33%, Range: (48.33%, 68.33%), Out of range: Disk1, Disk2,
Disk3
+ // Expected source: Disk1 (highest) at index 2, Expected destination:
Disk3 (lowest) at index 0
+ Arguments.arguments(new TestScenario(
+ "MultipleVolumesAboveOneBelow",
+ Arrays.asList(
+ new VolumeTestConfig("disk3", 0.20), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk2", 0.75), // Middle utilization -
index 1
+ new VolumeTestConfig("disk1", 0.80) // Highest utilization -
index 2
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk1", // Expected source (highest)
+ "disk3", // Expected destination (lowest)
+ 2, // Expected source index (highest utilization)
+ 0 // Expected destination index (lowest utilization)
+ )),
+
+ // Scenario 7: Edge case - volumes at threshold boundaries
+ // Disk1: 50%, Disk2: 40%, Disk3: 60%, Threshold: 10%
+ // Ideal: 50%, Range: (40%, 60%), Out of range: Disk3 (at upper),
Disk2 (at lower)
+ // Expected source: Disk3 (highest) at index 2, Expected destination:
Disk2 (lowest) at index 0
+ Arguments.arguments(new TestScenario(
+ "VolumesAtThresholdBoundaries",
+ Arrays.asList(
+ new VolumeTestConfig("disk2", 0.40), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk1", 0.50), // Middle utilization -
index 1
+ new VolumeTestConfig("disk3", 0.60) // Highest utilization -
index 2
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk3", // Expected source (highest)
+ "disk2", // Expected destination (lowest)
+ 2, // Expected source index (highest utilization)
+ 0 // Expected destination index (lowest utilization)
+ )),
+
+ // Scenario 8: Small threshold with moderate imbalance
+ // Disk1: 35%, Disk2: 30%, Disk3: 30%, Threshold: 2%
+ // Ideal: 31.67%, Range: (29.67%, 33.67%), Out of range: Disk1
+ // Expected source: Disk1 (highest) at index 2, Expected destination:
Disk2 or Disk3 (lowest) at index 0 or 1
+ Arguments.arguments(new TestScenario(
+ "SmallThresholdModerateImbalance",
+ Arrays.asList(
+ new VolumeTestConfig("disk2", 0.30), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk3", 0.30), // Same as disk2 - index
1
+ new VolumeTestConfig("disk1", 0.35) // Highest utilization -
index 2
+ ),
+ 2.0,
+ DEFAULT_CONTAINER_SIZE,
+ true,
+ "disk1", // Expected source (highest)
+ null, // Destination can be disk2 or disk3 (lowest)
+ 2, // Expected source index (highest utilization)
+ null // Destination index can be 0 or 1 (both have same
utilization)
+ )),
+
+ // Scenario 9: Best destination has low utilization but insufficient
space
+ // Disk1: 90% (Source)
+ // Disk2: 10% (Best Util, but small capacity 500MB) -> REJECT
(insufficient space)
+ // Disk3: 20% (Second Best Util, plenty of space) -> ACCEPT
+ // Disk2: capacity=500MB, 10% used=50MB, available=450MB
+ // Container size 500MB > 450MB available, so disk2 is rejected
+ // Disk3: capacity=2500MB, 20% used=500MB, available=2000MB
+ // Container size 500MB < 2000MB available, so disk3 is accepted
+ Arguments.arguments(new TestScenario(
+ "BestDestInsufficientSpace",
+ Arrays.asList(
+ new VolumeTestConfig("disk2", 0.10, 500L * MB), // Lowest
utilization, small capacity - index 0
+ new VolumeTestConfig("disk3", 0.20), // Second lowest - index
1
+ new VolumeTestConfig("disk1", 0.90) // Highest utilization -
index 2
+ ),
+ 10.0,
+ 500L * MB, // Container size larger than disk2's available space
+ true,
+ "disk1", // Expected source (highest)
+ "disk3", // Should skip disk2 and pick disk3
+ 2, // Expected source index (highest utilization)
+ 1 // Expected destination index (disk3 at index 1, since
disk2 at index 0 doesn't have enough space)
+ )),
+
+ // Scenario 10: Volumes just inside threshold boundaries
+ // Disk1: 40.01%, Disk2: 59.99%, Threshold: 10%
+ // Ideal: 50%, Range: (40%, 60%), Out of range: None (both are just
inside)
+ // Expected: No pair should be found as all volumes are within
threshold
+ Arguments.arguments(new TestScenario(
+ "VolumesJustInsideThresholdBoundaries",
+ Arrays.asList(
+ new VolumeTestConfig("disk1", 0.4001), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk2", 0.5999) // Highest utilization
- index 1
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ false, // Should not find pair - both volumes are within
threshold
+ null,
+ null,
+ null, // No source index expected
+ null // No destination index expected
+ )),
+
+ // Scenario 10b: Volumes just outside threshold boundaries
+ // Disk1: 39.99%, Disk2: 60.01%, Threshold: 10%
+ // Ideal: 50%, Range: (40%, 60%), Out of range: Disk1 (just below
lower), Disk2 (just above upper)
+ // Expected: Pair should be found - Disk2 as source at index 1, Disk1
as destination at index 0
+ Arguments.arguments(new TestScenario(
+ "VolumesJustOutsideThresholdBoundaries",
+ Arrays.asList(
+ new VolumeTestConfig("disk1", 0.3999), // Lowest utilization -
index 0
+ new VolumeTestConfig("disk2", 0.6001) // Highest utilization
- index 1
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ true, // Should find pair - volumes are outside threshold
+ "disk2", // Expected source (highest utilization)
+ "disk1", // Expected destination (lowest utilization)
+ 1, // Expected source index (highest utilization)
+ 0 // Expected destination index (lowest utilization)
+ )),
+
+ // Scenario 11: No volumes have enough free space
+ // Disk1: 90% (Source, highest)
+ // Disk2: 10% (Destination candidate, but insufficient space)
+ // Disk3: 20% (Destination candidate, but insufficient space)
+ // Disk2: capacity=500MB, 10% used=50MB, available=450MB
+ // Disk3: capacity=500MB, 20% used=100MB, available=400MB
+ // Container size 500MB > available space on both disk2 and disk3, so
both are rejected
+ // No valid destination found
+ Arguments.arguments(new TestScenario(
+ "NoVolumesHaveEnoughFreeSpace",
+ Arrays.asList(
+ new VolumeTestConfig("disk2", 0.10, 500L * MB), // Lowest
utilization - index 0
+ new VolumeTestConfig("disk3", 0.20, 500L * MB), // Second
lowest - index 1
+ new VolumeTestConfig("disk1", 0.90) // Highest utilization -
index 2
+ ),
+ 10.0,
+ 500L * MB, // Container size larger than both disk2's and disk3's
available space
+ false, // Should not find pair - no destination has enough
space
+ null,
+ null,
+ null, // No source index expected
+ null // No destination index expected
+ )),
+
+ // Scenario 12: Only one volume
+ // Disk1: 80%
+ // Cannot balance with only one volume
+ // Expected: No pair should be found
+ Arguments.arguments(new TestScenario(
+ "OnlyOneVolume",
+ Arrays.asList(
+ new VolumeTestConfig("disk1", 0.80) // Only volume
+ ),
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ false, // Should not find pair - need at least 2 volumes
+ null,
+ null,
+ null, // No source index expected
+ null // No destination index expected
+ )),
+
+ // Scenario 13: Zero volumes (empty volume set)
+ // No volumes at all
+ // Expected: No pair should be found
+ Arguments.arguments(new TestScenario(
+ "ZeroVolumes",
+ Arrays.asList(), // Empty list
+ 10.0,
+ DEFAULT_CONTAINER_SIZE,
+ false, // Should not find pair - no volumes available
+ null,
+ null,
+ null, // No source index expected
+ null // No destination index expected
+ ))
+ );
+ }
+}
+
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/scm/node/TestVolumeChoosingPolicy.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/scm/node/TestVolumeChoosingPolicy.java
index 1bb20c1ce71..cdbe046635c 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/scm/node/TestVolumeChoosingPolicy.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/scm/node/TestVolumeChoosingPolicy.java
@@ -68,8 +68,9 @@ public class TestVolumeChoosingPolicy {
private static final int NUM_THREADS = 10;
private static final int NUM_ITERATIONS = 10000;
private static final double THRESHOLD = 10; // 10% threshold
+ private static final long DEFAULT_CONTAINER_SIZE = 100L * 1024 * 1024; //
100MB
- private static final OzoneConfiguration CONF = new OzoneConfiguration();
+ private OzoneConfiguration conf;
@TempDir
private Path baseDir;
@@ -84,6 +85,8 @@ public class TestVolumeChoosingPolicy {
@BeforeEach
public void setup() throws Exception {
+ // Create a fresh configuration for each test to avoid interference
between tests
+ conf = new OzoneConfiguration();
hddsVolumes = new ArrayList<>();
createVolumes();
volumeChoosingPolicy = new DefaultVolumeChoosingPolicy(new
ReentrantLock());
@@ -114,12 +117,14 @@ public void cleanUp() {
@Test
@Timeout(30)
public void testVolumeChoosingFailureDueToDiskFull() {
+ // Create a separate configuration for this test to avoid affecting other
tests
+ OzoneConfiguration testConf = new OzoneConfiguration();
// update volume configure, set a huge min free space
- CONF.set("hdds.datanode.volume.min.free.space", "990GB");
+ testConf.set("hdds.datanode.volume.min.free.space", "990GB");
for (StorageVolume volume: volumeSet.getVolumesList()) {
- volume.setConf(CONF);
+ volume.setConf(testConf);
}
- assertNull(volumeChoosingPolicy.chooseVolume(volumeSet, THRESHOLD,
deltaSizes, 0));
+ assertNull(volumeChoosingPolicy.chooseVolume(volumeSet, THRESHOLD,
deltaSizes, DEFAULT_CONTAINER_SIZE));
assertEquals(NUM_VOLUMES, volumeSet.getVolumesList().size());
}
@@ -174,20 +179,27 @@ private void testPolicyPerformance(String policyName,
DiskBalancerVolumeChoosing
long threadStart = System.nanoTime();
try {
- Pair<HddsVolume, HddsVolume> pair =
policy.chooseVolume(volumeSet, THRESHOLD, deltaSizes, 0);
+ // Use a reasonable container size for the policy check
+ // The policy checks if containerSize < computeUsableSpace()
+ Pair<HddsVolume, HddsVolume> pair = policy.chooseVolume(
+ volumeSet, THRESHOLD, deltaSizes, DEFAULT_CONTAINER_SIZE);
if (pair == null) {
volumeNotChosen++;
} else {
volumeChosen++;
- // Note: In a real DiskBalancerService, after a successful
choice,
- // the committed bytes on the destination and delta on the
source
- // would be reverted/adjusted once the move completes or fails.
- // This simulation focuses on the state before the policy
makes a choice,
- // assuming some background activity. For simplicity, we are
not
- // reverting these simulated incCommittedBytes or deltaSizes
updates
- // within this loop for each "chosen" pair. The random updates
- // provide the dynamic background.
+ HddsVolume sourceVolume = pair.getLeft();
+
+ // The policy automatically calls
destVolume.incCommittedBytes(containerSize)
+ // when a pair is chosen, so we need to simulate the
corresponding
+ // deltaMap update for the source volume (negative value =
space to be freed)
+ // This aligns with how DiskBalancerService updates deltaSizes
after choosing
+ deltaSizes.compute(sourceVolume, (k, v) -> (v == null ? 0L :
v) - DEFAULT_CONTAINER_SIZE);
+
+ // Note: In a real DiskBalancerService, after a successful
container move,
+ // the committed bytes on the destination would be adjusted
and the delta
+ // on the source would be cleared. For this performance test,
we simulate
+ // the ongoing state changes that occur during disk balancing
operations.
}
} catch (Exception e) {
failures++;
@@ -229,10 +241,12 @@ private void createVolumes() throws IOException {
// set a dummy path for initialisation, as we will override its internal
volumeMap.
String initialDnDir =
baseDir.resolve("initialDnDir").toFile().getAbsolutePath();
Files.createDirectories(baseDir.resolve("initialDnDir"));
- CONF.set(ScmConfigKeys.HDDS_DATANODE_DIR_KEY, initialDnDir);
+ conf.set(ScmConfigKeys.HDDS_DATANODE_DIR_KEY, initialDnDir);
+ conf.set("hdds.datanode.volume.min.free.space", "20GB");
+ conf.setFloat("hdds.datanode.volume.min.free.space.percent", 0.02f);
StateContext mockContext = mock(StateContext.class);
- volumeSet = new MutableVolumeSet(UUID.randomUUID().toString(), CONF,
+ volumeSet = new MutableVolumeSet(UUID.randomUUID().toString(), conf,
mockContext, StorageVolume.VolumeType.DATA_VOLUME, null);
// This map will replace the one inside 'volumeSet'
@@ -254,9 +268,11 @@ private void createVolumes() throws IOException {
SpaceUsageCheckFactory factory = MockSpaceUsageCheckFactory.of(
source, Duration.ZERO, SpaceUsagePersistence.None.INSTANCE);
HddsVolume volume = new HddsVolume.Builder(volumePath)
- .conf(CONF)
+ .conf(conf)
.usageCheckFactory(factory)
.build();
+ // Ensure configuration is applied to the volume
+ volume.setConf(conf);
hddsVolumes.add(volume);
newVolumeMap.put(volume.getStorageDir().getPath(), volume);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]