Skip to content

Commit 8322282

Browse files
authored
HDDS-15524. [DiskBalancer] Container parallel moves can overwrite pending source replica deletions (#10489).
1 parent 33ba501 commit 8322282

2 files changed

Lines changed: 116 additions & 16 deletions

File tree

hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/diskbalancer/DiskBalancerService.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
import java.util.List;
3939
import java.util.Map;
4040
import java.util.Objects;
41+
import java.util.Queue;
4142
import java.util.Set;
4243
import java.util.concurrent.ConcurrentHashMap;
44+
import java.util.concurrent.ConcurrentLinkedQueue;
4345
import java.util.concurrent.ConcurrentSkipListMap;
4446
import java.util.concurrent.TimeUnit;
4547
import java.util.concurrent.atomic.AtomicLong;
@@ -112,7 +114,8 @@ public class DiskBalancerService extends BackgroundService {
112114
private AtomicLong nextAvailableTime = new AtomicLong(Time.monotonicNow());
113115

114116
private Set<ContainerID> inProgressContainers;
115-
private ConcurrentSkipListMap<Long, Container> pendingDeletionContainers = new ConcurrentSkipListMap();
117+
private final ConcurrentSkipListMap<Long, Queue<Container>> pendingDeletionContainers =
118+
new ConcurrentSkipListMap<>();
116119
private static FaultInjector injector;
117120

118121
/**
@@ -639,14 +642,16 @@ public BackgroundTaskResult call() {
639642
}
640643
if (moveSucceeded && newContainer != null) {
641644
// Add current old container to pendingDeletionContainers.
642-
pendingDeletionContainers.put(clock.millis() + replicaDeletionDelay,
643-
container);
645+
long deadline = clock.millis() + replicaDeletionDelay;
646+
pendingDeletionContainers
647+
.computeIfAbsent(deadline, ignored -> new ConcurrentLinkedQueue<>())
648+
.add(container);
644649
ContainerLogger.logMoveSuccess(newContainer.getContainerData(), sourceVolume,
645650
destVolume, containerSize, Time.monotonicNow() - startTime);
646651
}
647652
postCall(moveSucceeded && newContainer != null, startTime);
648653

649-
// pick one expired container from pendingDeletionContainers to delete
654+
// Attempt to delete any pending-deletion buckets whose deadline has elapsed.
650655
tryCleanupOnePendingDeletionContainer();
651656
}
652657
return BackgroundTaskResult.EmptyTaskResult.newResult();
@@ -686,7 +691,8 @@ private void deleteContainer(Container container) {
686691
}
687692
}
688693

689-
private void cleanupPendingDeletionContainers() {
694+
@VisibleForTesting
695+
public void cleanupPendingDeletionContainers() {
690696
// delete all pending deletion containers before stop the service
691697
boolean ret;
692698
do {
@@ -695,18 +701,18 @@ private void cleanupPendingDeletionContainers() {
695701
}
696702

697703
private boolean tryCleanupOnePendingDeletionContainer() {
698-
Map.Entry<Long, Container> entry = pendingDeletionContainers.pollFirstEntry();
699-
if (entry != null) {
700-
if (entry.getKey() <= clock.millis()) {
701-
// entry container is expired
702-
deleteContainer(entry.getValue());
703-
return true;
704-
} else {
705-
// put back the container
706-
pendingDeletionContainers.put(entry.getKey(), entry.getValue());
707-
}
704+
// peek first, only remove when expired
705+
Map.Entry<Long, Queue<Container>> entry = pendingDeletionContainers.firstEntry();
706+
if (entry == null || entry.getKey() > clock.millis()) {
707+
return false;
708708
}
709-
return false;
709+
if (!pendingDeletionContainers.remove(entry.getKey(), entry.getValue())) {
710+
return false;
711+
}
712+
for (Container pending : entry.getValue()) {
713+
deleteContainer(pending);
714+
}
715+
return true;
710716
}
711717

712718
public DiskBalancerInfo getDiskBalancerInfo() {
@@ -887,4 +893,16 @@ private static void pauseInjector() {
887893
public void setReplicaDeletionDelay(long durationMills) {
888894
this.replicaDeletionDelay = durationMills;
889895
}
896+
897+
@VisibleForTesting
898+
public int getPendingDeletionDeadlineCount() {
899+
return pendingDeletionContainers.size();
900+
}
901+
902+
@VisibleForTesting
903+
public int getPendingDeletionQueueSize() {
904+
return pendingDeletionContainers.values().stream()
905+
.mapToInt(Queue::size)
906+
.sum();
907+
}
890908
}

hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/diskbalancer/TestDiskBalancerTask.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.nio.file.Files;
4040
import java.nio.file.Path;
4141
import java.nio.file.Paths;
42+
import java.util.Arrays;
4243
import java.util.HashMap;
4344
import java.util.Iterator;
4445
import java.util.List;
@@ -47,6 +48,10 @@
4748
import java.util.concurrent.CompletableFuture;
4849
import java.util.concurrent.CountDownLatch;
4950
import java.util.concurrent.ExecutionException;
51+
import java.util.concurrent.ExecutorService;
52+
import java.util.concurrent.Executors;
53+
import java.util.concurrent.Future;
54+
import java.util.concurrent.TimeUnit;
5055
import java.util.concurrent.TimeoutException;
5156
import org.apache.commons.io.FileUtils;
5257
import org.apache.hadoop.hdds.HddsConfigKeys;
@@ -58,6 +63,8 @@
5863
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
5964
import org.apache.hadoop.hdds.scm.container.ContainerID;
6065
import org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
66+
import org.apache.hadoop.hdds.utils.BackgroundTaskQueue;
67+
import org.apache.hadoop.hdds.utils.BackgroundTaskResult;
6168
import org.apache.hadoop.hdds.utils.FaultInjector;
6269
import org.apache.hadoop.ozone.container.checksum.ContainerChecksumTreeManager;
6370
import org.apache.hadoop.ozone.container.common.helpers.ContainerMetrics;
@@ -679,6 +686,81 @@ public void testMoveSkippedWhenContainerStateChanged(State invalidState)
679686
assertEquals(initialSourceDelta, diskBalancerService.getDeltaSizes().get(sourceVolume));
680687
}
681688

689+
@ContainerTestVersionInfo.ContainerTest
690+
public void testPendingDeletionDoesNotDropReplicasOnSameMillisecondKey(
691+
ContainerTestVersionInfo versionInfo)
692+
throws Exception {
693+
setLayoutAndSchemaForTest(versionInfo);
694+
695+
long delayMs = 2_000L;
696+
diskBalancerService.setReplicaDeletionDelay(delayMs);
697+
698+
long id1 = CONTAINER_ID;
699+
long id2 = CONTAINER_ID + 1;
700+
long initialSourceUsed = sourceVolume.getCurrentUsage().getUsedSpace();
701+
702+
Container c1 = createContainer(id1, sourceVolume, State.CLOSED);
703+
Container c2 = createContainer(id2, sourceVolume, State.CLOSED);
704+
705+
File oldDir1 = new File(c1.getContainerData().getContainerPath());
706+
File oldDir2 = new File(c2.getContainerData().getContainerPath());
707+
assertTrue(oldDir1.exists());
708+
assertTrue(oldDir2.exists());
709+
710+
// Reserve dest space like the choosing policy would.
711+
destVolume.incCommittedBytes(c1.getContainerData().getBytesUsed());
712+
destVolume.incCommittedBytes(c2.getContainerData().getBytesUsed());
713+
714+
// Schedule two moves (parallelThread default is 5 in config).
715+
BackgroundTaskQueue queue = diskBalancerService.getTasks();
716+
assertEquals(2, queue.size());
717+
DiskBalancerService.DiskBalancerTask task1 =
718+
(DiskBalancerService.DiskBalancerTask) queue.poll();
719+
DiskBalancerService.DiskBalancerTask task2 =
720+
(DiskBalancerService.DiskBalancerTask) queue.poll();
721+
assertNotNull(task1);
722+
assertNotNull(task2);
723+
724+
// Run both moves concurrently; fixed TestClock => same deadline key.
725+
ExecutorService pool = Executors.newFixedThreadPool(2);
726+
try {
727+
List<Future<BackgroundTaskResult>> futures = pool.invokeAll(Arrays.asList(
728+
task1::call,
729+
task2::call));
730+
for (Future<BackgroundTaskResult> future : futures) {
731+
future.get(30, TimeUnit.SECONDS);
732+
}
733+
} finally {
734+
pool.shutdownNow();
735+
}
736+
737+
assertEquals(2, diskBalancerService.getMetrics().getSuccessCount());
738+
739+
assertEquals(1, diskBalancerService.getPendingDeletionDeadlineCount(),
740+
"both moves should share one deadline key");
741+
assertEquals(2, diskBalancerService.getPendingDeletionQueueSize(),
742+
"both container replicas should be queued for deletion");
743+
744+
// Not deleted yet — delay has not elapsed.
745+
assertTrue(oldDir1.exists());
746+
assertTrue(oldDir2.exists());
747+
748+
clock.fastForward(delayMs);
749+
diskBalancerService.cleanupPendingDeletionContainers();
750+
751+
assertEquals(0, diskBalancerService.getPendingDeletionQueueSize());
752+
assertFalse(oldDir1.exists());
753+
assertFalse(oldDir2.exists());
754+
assertFalse(sourceVolume.getContainerIterator().hasNext(),
755+
"source volume should have no containers after delayed deletion");
756+
assertEquals(initialSourceUsed, sourceVolume.getCurrentUsage().getUsedSpace(),
757+
"source volume used space should return to pre-move level after old replicas are deleted");
758+
759+
// New replicas live on dest volume.
760+
assertTrue(new File(containerSet.getContainer(id1).getContainerData().getContainerPath()).exists());
761+
assertTrue(new File(containerSet.getContainer(id2).getContainerData().getContainerPath()).exists());
762+
}
763+
682764
private KeyValueContainer createContainer(long containerId, HddsVolume vol, State state)
683765
throws IOException {
684766
KeyValueContainerData containerData = new KeyValueContainerData(

0 commit comments

Comments
 (0)