| /** |
| * 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.order; |
| |
| import static org.apache.hadoop.hdfs.server.federation.resolver.order.AvailableSpaceResolver.BALANCER_PREFERENCE_DEFAULT; |
| import static org.apache.hadoop.hdfs.server.federation.resolver.order.AvailableSpaceResolver.BALANCER_PREFERENCE_KEY; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.fail; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.hdfs.server.federation.resolver.MultipleDestinationMountTableResolver; |
| import org.apache.hadoop.hdfs.server.federation.resolver.PathLocation; |
| import org.apache.hadoop.hdfs.server.federation.resolver.order.AvailableSpaceResolver.SubclusterAvailableSpace; |
| import org.apache.hadoop.hdfs.server.federation.resolver.order.AvailableSpaceResolver.SubclusterSpaceComparator; |
| import org.apache.hadoop.hdfs.server.federation.router.Router; |
| import org.apache.hadoop.hdfs.server.federation.store.MembershipStore; |
| import org.apache.hadoop.hdfs.server.federation.store.StateStoreService; |
| import org.apache.hadoop.hdfs.server.federation.store.protocol.GetNamenodeRegistrationsRequest; |
| import org.apache.hadoop.hdfs.server.federation.store.protocol.GetNamenodeRegistrationsResponse; |
| import org.apache.hadoop.hdfs.server.federation.store.records.MembershipState; |
| import org.apache.hadoop.hdfs.server.federation.store.records.MembershipStats; |
| import org.apache.hadoop.hdfs.server.federation.store.records.MountTable; |
| import org.apache.hadoop.hdfs.server.federation.store.records.impl.pb.MembershipStatsPBImpl; |
| import org.apache.hadoop.test.GenericTestUtils; |
| import org.junit.Test; |
| |
| /** |
| * Test the {@link AvailableSpaceResolver}. |
| */ |
| public class TestAvailableSpaceResolver { |
| |
| private static final int SUBCLUSTER_NUM = 10; |
| |
| @Test |
| public void testResolverWithNoPreference() throws IOException { |
| MultipleDestinationMountTableResolver mountTableResolver = |
| mockAvailableSpaceResolver(1.0f); |
| // Since we don't have any preference, it will |
| // always chose the maximum-available-space subcluster. |
| PathLocation loc = mountTableResolver.getDestinationForPath("/space"); |
| assertEquals("subcluster9", |
| loc.getDestinations().get(0).getNameserviceId()); |
| |
| loc = mountTableResolver.getDestinationForPath("/space/subdir"); |
| assertEquals("subcluster9", |
| loc.getDestinations().get(0).getNameserviceId()); |
| } |
| |
| @Test |
| public void testResolverWithDefaultPreference() throws IOException { |
| MultipleDestinationMountTableResolver mountTableResolver = |
| mockAvailableSpaceResolver(BALANCER_PREFERENCE_DEFAULT); |
| |
| int retries = 10; |
| int retryTimes = 0; |
| // There is chance we won't always chose the |
| // maximum-available-space subcluster. |
| for (retryTimes = 0; retryTimes < retries; retryTimes++) { |
| PathLocation loc = mountTableResolver.getDestinationForPath("/space"); |
| if (!"subcluster9" |
| .equals(loc.getDestinations().get(0).getNameserviceId())) { |
| break; |
| } |
| } |
| assertNotEquals(retries, retryTimes); |
| } |
| |
| /** |
| * Mock the available space based resolver. |
| * |
| * @param balancerPreference The balancer preference for the resolver. |
| * @throws IOException |
| * @return MultipleDestinationMountTableResolver instance. |
| */ |
| @SuppressWarnings("unchecked") |
| private MultipleDestinationMountTableResolver mockAvailableSpaceResolver( |
| float balancerPreference) throws IOException { |
| Configuration conf = new Configuration(); |
| conf.setFloat(BALANCER_PREFERENCE_KEY, balancerPreference); |
| Router router = mock(Router.class); |
| StateStoreService stateStore = mock(StateStoreService.class); |
| MembershipStore membership = mock(MembershipStore.class); |
| when(router.getStateStore()).thenReturn(stateStore); |
| when(stateStore.getRegisteredRecordStore(any(Class.class))) |
| .thenReturn(membership); |
| GetNamenodeRegistrationsResponse response = |
| GetNamenodeRegistrationsResponse.newInstance(); |
| // Set the mapping for each client |
| List<MembershipState> records = new LinkedList<>(); |
| for (int i = 0; i < SUBCLUSTER_NUM; i++) { |
| records.add(newMembershipState("subcluster" + i, i)); |
| } |
| response.setNamenodeMemberships(records); |
| |
| when(membership |
| .getNamenodeRegistrations(any(GetNamenodeRegistrationsRequest.class))) |
| .thenReturn(response); |
| |
| // construct available space resolver |
| AvailableSpaceResolver resolver = new AvailableSpaceResolver(conf, router); |
| MultipleDestinationMountTableResolver mountTableResolver = |
| new MultipleDestinationMountTableResolver(conf, router); |
| mountTableResolver.addResolver(DestinationOrder.SPACE, resolver); |
| |
| // We point /space to subclusters [0,..9] with the SPACE order |
| Map<String, String> destinations = new HashMap<>(); |
| for (int i = 0; i < SUBCLUSTER_NUM; i++) { |
| destinations.put("subcluster" + i, "/space"); |
| } |
| MountTable spaceEntry = MountTable.newInstance("/space", destinations); |
| spaceEntry.setDestOrder(DestinationOrder.SPACE); |
| mountTableResolver.addEntry(spaceEntry); |
| |
| return mountTableResolver; |
| } |
| |
| public static MembershipState newMembershipState(String nameservice, |
| long availableSpace) { |
| MembershipState record = MembershipState.newInstance(); |
| record.setNameserviceId(nameservice); |
| |
| MembershipStats stats = new MembershipStatsPBImpl(); |
| stats.setAvailableSpace(availableSpace); |
| record.setStats(stats); |
| return record; |
| } |
| |
| @Test |
| public void testSubclusterSpaceComparator() { |
| verifyRank(0.0f, true, false); |
| verifyRank(1.0f, true, true); |
| verifyRank(0.5f, false, false); |
| verifyRank(BALANCER_PREFERENCE_DEFAULT, false, false); |
| |
| // test for illegal cases |
| try { |
| verifyRank(2.0f, false, false); |
| fail("Subcluster comparison should be failed."); |
| } catch (IllegalArgumentException e) { |
| GenericTestUtils.assertExceptionContains( |
| "The balancer preference value should be in the range 0.0 - 1.0", e); |
| } |
| |
| try { |
| verifyRank(-1.0f, false, false); |
| fail("Subcluster comparison should be failed."); |
| } catch (IllegalArgumentException e) { |
| GenericTestUtils.assertExceptionContains( |
| "The balancer preference value should be in the range 0.0 - 1.0", e); |
| } |
| } |
| |
| /** |
| * Verify result rank with {@link SubclusterSpaceComparator}. |
| * @param balancerPreference The balancer preference used |
| * in {@link SubclusterSpaceComparator}. |
| * @param shouldOrdered The result rank should be ordered. |
| * @param isDesc If the rank result is in a descending order. |
| */ |
| private void verifyRank(float balancerPreference, boolean shouldOrdered, |
| boolean isDesc) { |
| List<SubclusterAvailableSpace> subclusters = new LinkedList<>(); |
| for (int i = 0; i < SUBCLUSTER_NUM; i++) { |
| subclusters.add(new SubclusterAvailableSpace("subcluster" + i, i)); |
| } |
| |
| // shuffle the cluster list if we expect rank to be ordered |
| if (shouldOrdered) { |
| Collections.shuffle(subclusters); |
| } |
| |
| SubclusterSpaceComparator comparator = new SubclusterSpaceComparator( |
| balancerPreference); |
| Collections.sort(subclusters, comparator); |
| |
| int i = SUBCLUSTER_NUM - 1; |
| for (; i >= 0; i--) { |
| SubclusterAvailableSpace cluster = subclusters |
| .get(SUBCLUSTER_NUM - 1 - i); |
| |
| if (shouldOrdered) { |
| if (isDesc) { |
| assertEquals("subcluster" + i, cluster.getNameserviceId()); |
| assertEquals(i, cluster.getAvailableSpace()); |
| } else { |
| assertEquals("subcluster" + (SUBCLUSTER_NUM - 1 - i), |
| cluster.getNameserviceId()); |
| assertEquals(SUBCLUSTER_NUM - 1 - i, cluster.getAvailableSpace()); |
| } |
| } else { |
| // If catch one cluster is not in ordered, that's expected behavior. |
| if (!cluster.getNameserviceId().equals("subcluster" + i) |
| && cluster.getAvailableSpace() != i) { |
| break; |
| } |
| } |
| } |
| |
| // The var i won't reach to 0 since cluster list won't be completely |
| // ordered. |
| if (!shouldOrdered) { |
| assertNotEquals(0, i); |
| } |
| subclusters.clear(); |
| } |
| } |