|
39 | 39 | import java.nio.file.Files; |
40 | 40 | import java.nio.file.Path; |
41 | 41 | import java.nio.file.Paths; |
| 42 | +import java.util.Arrays; |
42 | 43 | import java.util.HashMap; |
43 | 44 | import java.util.Iterator; |
44 | 45 | import java.util.List; |
|
47 | 48 | import java.util.concurrent.CompletableFuture; |
48 | 49 | import java.util.concurrent.CountDownLatch; |
49 | 50 | 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; |
50 | 55 | import java.util.concurrent.TimeoutException; |
51 | 56 | import org.apache.commons.io.FileUtils; |
52 | 57 | import org.apache.hadoop.hdds.HddsConfigKeys; |
|
58 | 63 | import org.apache.hadoop.hdds.scm.ScmConfigKeys; |
59 | 64 | import org.apache.hadoop.hdds.scm.container.ContainerID; |
60 | 65 | 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; |
61 | 68 | import org.apache.hadoop.hdds.utils.FaultInjector; |
62 | 69 | import org.apache.hadoop.ozone.container.checksum.ContainerChecksumTreeManager; |
63 | 70 | import org.apache.hadoop.ozone.container.common.helpers.ContainerMetrics; |
@@ -679,6 +686,81 @@ public void testMoveSkippedWhenContainerStateChanged(State invalidState) |
679 | 686 | assertEquals(initialSourceDelta, diskBalancerService.getDeltaSizes().get(sourceVolume)); |
680 | 687 | } |
681 | 688 |
|
| 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 | + |
682 | 764 | private KeyValueContainer createContainer(long containerId, HddsVolume vol, State state) |
683 | 765 | throws IOException { |
684 | 766 | KeyValueContainerData containerData = new KeyValueContainerData( |
|
0 commit comments