|
@@ -0,0 +1,419 @@
|
|
|
+/**
|
|
|
+ * 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.hdfs.server.federation.resolver;
|
|
|
+
|
|
|
+import static org.apache.hadoop.hdfs.server.federation.resolver.order.HashResolver.extractTempFileName;
|
|
|
+import static org.junit.Assert.assertEquals;
|
|
|
+import static org.junit.Assert.assertTrue;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Random;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.TreeSet;
|
|
|
+
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
+import org.apache.hadoop.hdfs.server.federation.resolver.order.DestinationOrder;
|
|
|
+import org.apache.hadoop.hdfs.server.federation.store.records.MountTable;
|
|
|
+import org.junit.Before;
|
|
|
+import org.junit.Test;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Test the multiple destination resolver.
|
|
|
+ */
|
|
|
+public class TestMultipleDestinationResolver {
|
|
|
+
|
|
|
+ private MultipleDestinationMountTableResolver resolver;
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void setup() throws IOException {
|
|
|
+ Configuration conf = new Configuration();
|
|
|
+ resolver = new MultipleDestinationMountTableResolver(conf, null);
|
|
|
+
|
|
|
+ // We manually point /tmp to only subcluster0
|
|
|
+ Map<String, String> map1 = new HashMap<>();
|
|
|
+ map1.put("subcluster0", "/tmp");
|
|
|
+ resolver.addEntry(MountTable.newInstance("/tmp", map1));
|
|
|
+
|
|
|
+ // We manually point / to subcluster0,1,2 with default order (hash)
|
|
|
+ Map<String, String> mapDefault = new HashMap<>();
|
|
|
+ mapDefault.put("subcluster0", "/");
|
|
|
+ mapDefault.put("subcluster1", "/");
|
|
|
+ mapDefault.put("subcluster2", "/");
|
|
|
+ MountTable defaultEntry = MountTable.newInstance("/", mapDefault);
|
|
|
+ resolver.addEntry(defaultEntry);
|
|
|
+
|
|
|
+ // We manually point /hash to subcluster0,1,2 with hashing
|
|
|
+ Map<String, String> mapHash = new HashMap<>();
|
|
|
+ mapHash.put("subcluster0", "/hash");
|
|
|
+ mapHash.put("subcluster1", "/hash");
|
|
|
+ mapHash.put("subcluster2", "/hash");
|
|
|
+ MountTable hashEntry = MountTable.newInstance("/hash", mapHash);
|
|
|
+ hashEntry.setDestOrder(DestinationOrder.HASH);
|
|
|
+ resolver.addEntry(hashEntry);
|
|
|
+
|
|
|
+ // We manually point /hashall to subcluster0,1,2 with hashing (full tree)
|
|
|
+ Map<String, String> mapHashAll = new HashMap<>();
|
|
|
+ mapHashAll.put("subcluster0", "/hashall");
|
|
|
+ mapHashAll.put("subcluster1", "/hashall");
|
|
|
+ mapHashAll.put("subcluster2", "/hashall");
|
|
|
+ MountTable hashEntryAll = MountTable.newInstance("/hashall", mapHashAll);
|
|
|
+ hashEntryAll.setDestOrder(DestinationOrder.HASH_ALL);
|
|
|
+ resolver.addEntry(hashEntryAll);
|
|
|
+
|
|
|
+ // We point /local to subclusters 0, 1, 2 with the local order
|
|
|
+ Map<String, String> mapLocal = new HashMap<>();
|
|
|
+ mapLocal.put("subcluster0", "/local");
|
|
|
+ mapLocal.put("subcluster1", "/local");
|
|
|
+ mapLocal.put("subcluster2", "/local");
|
|
|
+ MountTable localEntry = MountTable.newInstance("/local", mapLocal);
|
|
|
+ localEntry.setDestOrder(DestinationOrder.LOCAL);
|
|
|
+ resolver.addEntry(localEntry);
|
|
|
+
|
|
|
+ // We point /random to subclusters 0, 1, 2 with the random order
|
|
|
+ Map<String, String> mapRandom = new HashMap<>();
|
|
|
+ mapRandom.put("subcluster0", "/random");
|
|
|
+ mapRandom.put("subcluster1", "/random");
|
|
|
+ mapRandom.put("subcluster2", "/random");
|
|
|
+ MountTable randomEntry = MountTable.newInstance("/random", mapRandom);
|
|
|
+ randomEntry.setDestOrder(DestinationOrder.RANDOM);
|
|
|
+ resolver.addEntry(randomEntry);
|
|
|
+
|
|
|
+ // Read only mount point
|
|
|
+ Map<String, String> mapReadOnly = new HashMap<>();
|
|
|
+ mapReadOnly.put("subcluster0", "/readonly");
|
|
|
+ mapReadOnly.put("subcluster1", "/readonly");
|
|
|
+ mapReadOnly.put("subcluster2", "/readonly");
|
|
|
+ MountTable readOnlyEntry = MountTable.newInstance("/readonly", mapReadOnly);
|
|
|
+ readOnlyEntry.setReadOnly(true);
|
|
|
+ resolver.addEntry(readOnlyEntry);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testHashEqualDistribution() throws IOException {
|
|
|
+ // First level
|
|
|
+ testEvenDistribution("/hash");
|
|
|
+ testEvenDistribution("/hash/folder0", false);
|
|
|
+
|
|
|
+ // All levels
|
|
|
+ testEvenDistribution("/hashall");
|
|
|
+ testEvenDistribution("/hashall/folder0");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testHashAll() throws IOException {
|
|
|
+ // Files should be spread across subclusters
|
|
|
+ PathLocation dest0 = resolver.getDestinationForPath("/hashall/file0.txt");
|
|
|
+ assertDest("subcluster0", dest0);
|
|
|
+ PathLocation dest1 = resolver.getDestinationForPath("/hashall/file1.txt");
|
|
|
+ assertDest("subcluster1", dest1);
|
|
|
+
|
|
|
+ // Files within folder should be spread across subclusters
|
|
|
+ PathLocation dest2 = resolver.getDestinationForPath("/hashall/folder0");
|
|
|
+ assertDest("subcluster2", dest2);
|
|
|
+ PathLocation dest3 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder0/file0.txt");
|
|
|
+ assertDest("subcluster1", dest3);
|
|
|
+ PathLocation dest4 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder0/file1.txt");
|
|
|
+ assertDest("subcluster0", dest4);
|
|
|
+
|
|
|
+ PathLocation dest5 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder0/folder0/file0.txt");
|
|
|
+ assertDest("subcluster1", dest5);
|
|
|
+ PathLocation dest6 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder0/folder0/file1.txt");
|
|
|
+ assertDest("subcluster1", dest6);
|
|
|
+ PathLocation dest7 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder0/folder0/file2.txt");
|
|
|
+ assertDest("subcluster0", dest7);
|
|
|
+
|
|
|
+ PathLocation dest8 = resolver.getDestinationForPath("/hashall/folder1");
|
|
|
+ assertDest("subcluster1", dest8);
|
|
|
+ PathLocation dest9 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder1/file0.txt");
|
|
|
+ assertDest("subcluster0", dest9);
|
|
|
+ PathLocation dest10 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder1/file1.txt");
|
|
|
+ assertDest("subcluster1", dest10);
|
|
|
+
|
|
|
+ PathLocation dest11 = resolver.getDestinationForPath("/hashall/folder2");
|
|
|
+ assertDest("subcluster2", dest11);
|
|
|
+ PathLocation dest12 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder2/file0.txt");
|
|
|
+ assertDest("subcluster0", dest12);
|
|
|
+ PathLocation dest13 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder2/file1.txt");
|
|
|
+ assertDest("subcluster0", dest13);
|
|
|
+ PathLocation dest14 = resolver.getDestinationForPath(
|
|
|
+ "/hashall/folder2/file2.txt");
|
|
|
+ assertDest("subcluster1", dest14);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testHashFirst() throws IOException {
|
|
|
+ PathLocation dest0 = resolver.getDestinationForPath("/hashall/file0.txt");
|
|
|
+ assertDest("subcluster0", dest0);
|
|
|
+ PathLocation dest1 = resolver.getDestinationForPath("/hashall/file1.txt");
|
|
|
+ assertDest("subcluster1", dest1);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster0
|
|
|
+ PathLocation dest2 = resolver.getDestinationForPath("/hash/folder0");
|
|
|
+ assertDest("subcluster0", dest2);
|
|
|
+ PathLocation dest3 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder0/file0.txt");
|
|
|
+ assertDest("subcluster0", dest3);
|
|
|
+ PathLocation dest4 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder0/file1.txt");
|
|
|
+ assertDest("subcluster0", dest4);
|
|
|
+
|
|
|
+ PathLocation dest5 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder0/folder0/file0.txt");
|
|
|
+ assertDest("subcluster0", dest5);
|
|
|
+ PathLocation dest6 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder0/folder0/file1.txt");
|
|
|
+ assertDest("subcluster0", dest6);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster2
|
|
|
+ PathLocation dest7 = resolver.getDestinationForPath("/hash/folder1");
|
|
|
+ assertDest("subcluster2", dest7);
|
|
|
+ PathLocation dest8 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder1/file0.txt");
|
|
|
+ assertDest("subcluster2", dest8);
|
|
|
+ PathLocation dest9 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder1/file1.txt");
|
|
|
+ assertDest("subcluster2", dest9);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster2
|
|
|
+ PathLocation dest10 = resolver.getDestinationForPath("/hash/folder2");
|
|
|
+ assertDest("subcluster2", dest10);
|
|
|
+ PathLocation dest11 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder2/file0.txt");
|
|
|
+ assertDest("subcluster2", dest11);
|
|
|
+ PathLocation dest12 = resolver.getDestinationForPath(
|
|
|
+ "/hash/folder2/file1.txt");
|
|
|
+ assertDest("subcluster2", dest12);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testRandomEqualDistribution() throws IOException {
|
|
|
+ testEvenDistribution("/random");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testSingleDestination() throws IOException {
|
|
|
+ // All the files in /tmp should be in subcluster0
|
|
|
+ for (int f = 0; f < 100; f++) {
|
|
|
+ String filename = "/tmp/b/c/file" + f + ".txt";
|
|
|
+ PathLocation destination = resolver.getDestinationForPath(filename);
|
|
|
+ RemoteLocation loc = destination.getDefaultLocation();
|
|
|
+ assertEquals("subcluster0", loc.getNameserviceId());
|
|
|
+ assertEquals(filename, loc.getDest());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testResolveSubdirectories() throws Exception {
|
|
|
+ // Simulate a testdir under a multi-destination mount.
|
|
|
+ Random r = new Random();
|
|
|
+ String testDir = "/sort/testdir" + r.nextInt();
|
|
|
+ String file1 = testDir + "/file1" + r.nextInt();
|
|
|
+ String file2 = testDir + "/file2" + r.nextInt();
|
|
|
+
|
|
|
+ // Verify both files resolve to the same namespace as the parent dir.
|
|
|
+ PathLocation testDirLocation = resolver.getDestinationForPath(testDir);
|
|
|
+ RemoteLocation defaultLoc = testDirLocation.getDefaultLocation();
|
|
|
+ String testDirNamespace = defaultLoc.getNameserviceId();
|
|
|
+
|
|
|
+ PathLocation file1Location = resolver.getDestinationForPath(file1);
|
|
|
+ RemoteLocation defaultLoc1 = file1Location.getDefaultLocation();
|
|
|
+ assertEquals(testDirNamespace, defaultLoc1.getNameserviceId());
|
|
|
+
|
|
|
+ PathLocation file2Location = resolver.getDestinationForPath(file2);
|
|
|
+ RemoteLocation defaultLoc2 = file2Location.getDefaultLocation();
|
|
|
+ assertEquals(testDirNamespace, defaultLoc2.getNameserviceId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testExtractTempFileName() {
|
|
|
+ for (String teststring : new String[] {
|
|
|
+ "testfile1.txt.COPYING",
|
|
|
+ "testfile1.txt._COPYING_",
|
|
|
+ "testfile1.txt._COPYING_.attempt_1486662804109_0055_m_000042_0",
|
|
|
+ "testfile1.txt.tmp",
|
|
|
+ "_temp/testfile1.txt",
|
|
|
+ "_temporary/testfile1.txt.af77e2ab-4bc5-4959-ae08-299c880ee6b8",
|
|
|
+ "_temporary/0/_temporary/attempt_201706281636_0007_m_000003_46/" +
|
|
|
+ "testfile1.txt" }) {
|
|
|
+ String finalName = extractTempFileName(teststring);
|
|
|
+ assertEquals("testfile1.txt", finalName);
|
|
|
+ }
|
|
|
+
|
|
|
+ // False cases
|
|
|
+ assertEquals(
|
|
|
+ "file1.txt.COPYING1", extractTempFileName("file1.txt.COPYING1"));
|
|
|
+ assertEquals("file1.txt.tmp2", extractTempFileName("file1.txt.tmp2"));
|
|
|
+
|
|
|
+ // Speculation patterns
|
|
|
+ String finalName = extractTempFileName(
|
|
|
+ "_temporary/part-00007.af77e2ab-4bc5-4959-ae08-299c880ee6b8");
|
|
|
+ assertEquals("part-00007", finalName);
|
|
|
+ finalName = extractTempFileName(
|
|
|
+ "_temporary/0/_temporary/attempt_201706281636_0007_m_000003_46/" +
|
|
|
+ "part-00003");
|
|
|
+ assertEquals("part-00003", finalName);
|
|
|
+
|
|
|
+ // Subfolders
|
|
|
+ finalName = extractTempFileName("folder0/testfile1.txt._COPYING_");
|
|
|
+ assertEquals("folder0/testfile1.txt", finalName);
|
|
|
+ finalName = extractTempFileName(
|
|
|
+ "folder0/folder1/testfile1.txt._COPYING_");
|
|
|
+ assertEquals("folder0/folder1/testfile1.txt", finalName);
|
|
|
+ finalName = extractTempFileName(
|
|
|
+ "processedHrsData.txt/_temporary/0/_temporary/" +
|
|
|
+ "attempt_201706281636_0007_m_000003_46/part-00003");
|
|
|
+ assertEquals("processedHrsData.txt/part-00003", finalName);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testReadOnly() throws IOException {
|
|
|
+ MountTable mount = resolver.getMountPoint("/readonly");
|
|
|
+ assertTrue(mount.isReadOnly());
|
|
|
+
|
|
|
+ PathLocation dest0 = resolver.getDestinationForPath("/readonly/file0.txt");
|
|
|
+ assertDest("subcluster1", dest0);
|
|
|
+ PathLocation dest1 = resolver.getDestinationForPath("/readonly/file1.txt");
|
|
|
+ assertDest("subcluster2", dest1);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster0
|
|
|
+ PathLocation dest2 = resolver.getDestinationForPath("/readonly/folder0");
|
|
|
+ assertDest("subcluster1", dest2);
|
|
|
+ PathLocation dest3 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder0/file0.txt");
|
|
|
+ assertDest("subcluster1", dest3);
|
|
|
+ PathLocation dest4 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder0/file1.txt");
|
|
|
+ assertDest("subcluster1", dest4);
|
|
|
+
|
|
|
+ PathLocation dest5 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder0/folder0/file0.txt");
|
|
|
+ assertDest("subcluster1", dest5);
|
|
|
+ PathLocation dest6 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder0/folder0/file1.txt");
|
|
|
+ assertDest("subcluster1", dest6);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster2
|
|
|
+ PathLocation dest7 = resolver.getDestinationForPath("/readonly/folder1");
|
|
|
+ assertDest("subcluster2", dest7);
|
|
|
+ PathLocation dest8 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder1/file0.txt");
|
|
|
+ assertDest("subcluster2", dest8);
|
|
|
+ PathLocation dest9 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder1/file1.txt");
|
|
|
+ assertDest("subcluster2", dest9);
|
|
|
+
|
|
|
+ // All these must be in the same location: subcluster2
|
|
|
+ PathLocation dest10 = resolver.getDestinationForPath("/readonly/folder2");
|
|
|
+ assertDest("subcluster1", dest10);
|
|
|
+ PathLocation dest11 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder2/file0.txt");
|
|
|
+ assertDest("subcluster1", dest11);
|
|
|
+ PathLocation dest12 = resolver.getDestinationForPath(
|
|
|
+ "/readonly/folder2/file1.txt");
|
|
|
+ assertDest("subcluster1", dest12);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testLocalResolver() throws IOException {
|
|
|
+ PathLocation dest0 =
|
|
|
+ resolver.getDestinationForPath("/local/folder0/file0.txt");
|
|
|
+ assertDest("subcluster0", dest0);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testRandomResolver() throws IOException {
|
|
|
+ Set<String> destinations = new HashSet<>();
|
|
|
+ for (int i = 0; i < 30; i++) {
|
|
|
+ PathLocation dest =
|
|
|
+ resolver.getDestinationForPath("/random/folder0/file0.txt");
|
|
|
+ RemoteLocation firstDest = dest.getDestinations().get(0);
|
|
|
+ String nsId = firstDest.getNameserviceId();
|
|
|
+ destinations.add(nsId);
|
|
|
+ }
|
|
|
+ assertEquals(3, destinations.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that a path has files distributed across destinations evenly.
|
|
|
+ * @param path Path to check.
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private void testEvenDistribution(final String path) throws IOException {
|
|
|
+ testEvenDistribution(path, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that a path has files distributed across destinations evenly or not.
|
|
|
+ * @param path Path to check.
|
|
|
+ * @param even If the distribution should be even or not.
|
|
|
+ * @throws IOException If it cannot check it.
|
|
|
+ */
|
|
|
+ private void testEvenDistribution(final String path, final boolean even)
|
|
|
+ throws IOException {
|
|
|
+
|
|
|
+ // Subcluster -> Files
|
|
|
+ Map<String, Set<String>> results = new HashMap<>();
|
|
|
+ for (int f = 0; f < 10000; f++) {
|
|
|
+ String filename = path + "/file" + f + ".txt";
|
|
|
+ PathLocation destination = resolver.getDestinationForPath(filename);
|
|
|
+ RemoteLocation loc = destination.getDefaultLocation();
|
|
|
+ assertEquals(filename, loc.getDest());
|
|
|
+
|
|
|
+ String nsId = loc.getNameserviceId();
|
|
|
+ if (!results.containsKey(nsId)) {
|
|
|
+ results.put(nsId, new TreeSet<>());
|
|
|
+ }
|
|
|
+ results.get(nsId).add(filename);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!even) {
|
|
|
+ // All files should be in one subcluster
|
|
|
+ assertEquals(1, results.size());
|
|
|
+ } else {
|
|
|
+ // Files should be distributed somewhat evenly
|
|
|
+ assertEquals(3, results.size());
|
|
|
+ int count = 0;
|
|
|
+ for (Set<String> files : results.values()) {
|
|
|
+ count = count + files.size();
|
|
|
+ }
|
|
|
+ int avg = count / results.keySet().size();
|
|
|
+ for (Set<String> files : results.values()) {
|
|
|
+ int filesCount = files.size();
|
|
|
+ // Check that the count in each namespace is within 20% of avg
|
|
|
+ assertTrue(filesCount > 0);
|
|
|
+ assertTrue(Math.abs(filesCount - avg) < (avg / 5));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertDest(String expectedDest, PathLocation loc) {
|
|
|
+ assertEquals(expectedDest, loc.getDestinations().get(0).getNameserviceId());
|
|
|
+ }
|
|
|
+}
|