|
@@ -0,0 +1,350 @@
|
|
|
|
+/**
|
|
|
|
+ * 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.namenode.ha;
|
|
|
|
+
|
|
|
|
+import java.io.IOException;
|
|
|
|
+import java.net.InetSocketAddress;
|
|
|
|
+import java.net.URI;
|
|
|
|
+import java.net.URISyntaxException;
|
|
|
|
+import java.util.Iterator;
|
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
+
|
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
|
+import org.apache.hadoop.hdfs.DFSConfigKeys;
|
|
|
|
+import org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider.ProxyFactory;
|
|
|
|
+import org.apache.hadoop.hdfs.server.protocol.NamenodeProtocols;
|
|
|
|
+import org.apache.hadoop.io.retry.MultiException;
|
|
|
|
+import org.apache.hadoop.security.UserGroupInformation;
|
|
|
|
+import org.apache.hadoop.util.Time;
|
|
|
|
+import org.junit.Assert;
|
|
|
|
+import org.junit.Before;
|
|
|
|
+import org.junit.Test;
|
|
|
|
+import org.mockito.Mockito;
|
|
|
|
+import org.mockito.invocation.InvocationOnMock;
|
|
|
|
+import org.mockito.stubbing.Answer;
|
|
|
|
+
|
|
|
|
+import com.google.common.collect.Lists;
|
|
|
|
+
|
|
|
|
+public class TestRequestHedgingProxyProvider {
|
|
|
|
+
|
|
|
|
+ private Configuration conf;
|
|
|
|
+ private URI nnUri;
|
|
|
|
+ private String ns;
|
|
|
|
+
|
|
|
|
+ @Before
|
|
|
|
+ public void setup() throws URISyntaxException {
|
|
|
|
+ ns = "mycluster-" + Time.monotonicNow();
|
|
|
|
+ nnUri = new URI("hdfs://" + ns);
|
|
|
|
+ conf = new Configuration();
|
|
|
|
+ conf.set(DFSConfigKeys.DFS_NAMESERVICES, ns);
|
|
|
|
+ conf.set(
|
|
|
|
+ DFSConfigKeys.DFS_HA_NAMENODES_KEY_PREFIX + "." + ns, "nn1,nn2");
|
|
|
|
+ conf.set(
|
|
|
|
+ DFSConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY + "." + ns + ".nn1",
|
|
|
|
+ "machine1.foo.bar:8020");
|
|
|
|
+ conf.set(
|
|
|
|
+ DFSConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY + "." + ns + ".nn2",
|
|
|
|
+ "machine2.foo.bar:8020");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void testHedgingWhenOneFails() throws Exception {
|
|
|
|
+ final NamenodeProtocols goodMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(goodMock.getStats()).thenReturn(new long[] {1});
|
|
|
|
+ final NamenodeProtocols badMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(badMock.getStats()).thenThrow(new IOException("Bad mock !!"));
|
|
|
|
+
|
|
|
|
+ RequestHedgingProxyProvider<NamenodeProtocols> provider =
|
|
|
|
+ new RequestHedgingProxyProvider<>(conf, nnUri, NamenodeProtocols.class,
|
|
|
|
+ createFactory(goodMock, badMock));
|
|
|
|
+ long[] stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Mockito.verify(badMock).getStats();
|
|
|
|
+ Mockito.verify(goodMock).getStats();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void testHedgingWhenOneIsSlow() throws Exception {
|
|
|
|
+ final NamenodeProtocols goodMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(goodMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{1};
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ final NamenodeProtocols badMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(badMock.getStats()).thenThrow(new IOException("Bad mock !!"));
|
|
|
|
+
|
|
|
|
+ RequestHedgingProxyProvider<NamenodeProtocols> provider =
|
|
|
|
+ new RequestHedgingProxyProvider<>(conf, nnUri, NamenodeProtocols.class,
|
|
|
|
+ createFactory(goodMock, badMock));
|
|
|
|
+ long[] stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ Mockito.verify(badMock).getStats();
|
|
|
|
+ Mockito.verify(goodMock).getStats();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void testHedgingWhenBothFail() throws Exception {
|
|
|
|
+ NamenodeProtocols badMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(badMock.getStats()).thenThrow(new IOException("Bad mock !!"));
|
|
|
|
+ NamenodeProtocols worseMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(worseMock.getStats()).thenThrow(
|
|
|
|
+ new IOException("Worse mock !!"));
|
|
|
|
+
|
|
|
|
+ RequestHedgingProxyProvider<NamenodeProtocols> provider =
|
|
|
|
+ new RequestHedgingProxyProvider<>(conf, nnUri, NamenodeProtocols.class,
|
|
|
|
+ createFactory(badMock, worseMock));
|
|
|
|
+ try {
|
|
|
|
+ provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.fail("Should fail since both namenodes throw IOException !!");
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ Assert.assertTrue(e instanceof MultiException);
|
|
|
|
+ }
|
|
|
|
+ Mockito.verify(badMock).getStats();
|
|
|
|
+ Mockito.verify(worseMock).getStats();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void testPerformFailover() throws Exception {
|
|
|
|
+ final AtomicInteger counter = new AtomicInteger(0);
|
|
|
|
+ final int[] isGood = {1};
|
|
|
|
+ final NamenodeProtocols goodMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(goodMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ counter.incrementAndGet();
|
|
|
|
+ if (isGood[0] == 1) {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{1};
|
|
|
|
+ }
|
|
|
|
+ throw new IOException("Was Good mock !!");
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ final NamenodeProtocols badMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(badMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ counter.incrementAndGet();
|
|
|
|
+ if (isGood[0] == 2) {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{2};
|
|
|
|
+ }
|
|
|
|
+ throw new IOException("Bad mock !!");
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ RequestHedgingProxyProvider<NamenodeProtocols> provider =
|
|
|
|
+ new RequestHedgingProxyProvider<>(conf, nnUri, NamenodeProtocols.class,
|
|
|
|
+ createFactory(goodMock, badMock));
|
|
|
|
+ long[] stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ Assert.assertEquals(2, counter.get());
|
|
|
|
+ Mockito.verify(badMock).getStats();
|
|
|
|
+ Mockito.verify(goodMock).getStats();
|
|
|
|
+
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ // Ensure only the previous successful one is invoked
|
|
|
|
+ Mockito.verifyNoMoreInteractions(badMock);
|
|
|
|
+ Assert.assertEquals(3, counter.get());
|
|
|
|
+
|
|
|
|
+ // Flip to standby.. so now this should fail
|
|
|
|
+ isGood[0] = 2;
|
|
|
|
+ try {
|
|
|
|
+ provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.fail("Should fail since previously successful proxy now fails ");
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
+ Assert.assertTrue(ex instanceof IOException);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Assert.assertEquals(4, counter.get());
|
|
|
|
+
|
|
|
|
+ provider.performFailover(provider.getProxy().proxy);
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(2, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter shuodl update only once
|
|
|
|
+ Assert.assertEquals(5, counter.get());
|
|
|
|
+
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(2, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter updates only once now
|
|
|
|
+ Assert.assertEquals(6, counter.get());
|
|
|
|
+
|
|
|
|
+ // Flip back to old active.. so now this should fail
|
|
|
|
+ isGood[0] = 1;
|
|
|
|
+ try {
|
|
|
|
+ provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.fail("Should fail since previously successful proxy now fails ");
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
+ Assert.assertTrue(ex instanceof IOException);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Assert.assertEquals(7, counter.get());
|
|
|
|
+
|
|
|
|
+ provider.performFailover(provider.getProxy().proxy);
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ // Ensure correct proxy was called
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void testPerformFailoverWith3Proxies() throws Exception {
|
|
|
|
+ conf.set(DFSConfigKeys.DFS_HA_NAMENODES_KEY_PREFIX + "." + ns,
|
|
|
|
+ "nn1,nn2,nn3");
|
|
|
|
+ conf.set(DFSConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY + "." + ns + ".nn3",
|
|
|
|
+ "machine3.foo.bar:8020");
|
|
|
|
+
|
|
|
|
+ final AtomicInteger counter = new AtomicInteger(0);
|
|
|
|
+ final int[] isGood = {1};
|
|
|
|
+ final NamenodeProtocols goodMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(goodMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ counter.incrementAndGet();
|
|
|
|
+ if (isGood[0] == 1) {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{1};
|
|
|
|
+ }
|
|
|
|
+ throw new IOException("Was Good mock !!");
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ final NamenodeProtocols badMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(badMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ counter.incrementAndGet();
|
|
|
|
+ if (isGood[0] == 2) {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{2};
|
|
|
|
+ }
|
|
|
|
+ throw new IOException("Bad mock !!");
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ final NamenodeProtocols worseMock = Mockito.mock(NamenodeProtocols.class);
|
|
|
|
+ Mockito.when(worseMock.getStats()).thenAnswer(new Answer<long[]>() {
|
|
|
|
+ @Override
|
|
|
|
+ public long[] answer(InvocationOnMock invocation) throws Throwable {
|
|
|
|
+ counter.incrementAndGet();
|
|
|
|
+ if (isGood[0] == 3) {
|
|
|
|
+ Thread.sleep(1000);
|
|
|
|
+ return new long[]{3};
|
|
|
|
+ }
|
|
|
|
+ throw new IOException("Worse mock !!");
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ RequestHedgingProxyProvider<NamenodeProtocols> provider =
|
|
|
|
+ new RequestHedgingProxyProvider<>(conf, nnUri, NamenodeProtocols.class,
|
|
|
|
+ createFactory(goodMock, badMock, worseMock));
|
|
|
|
+ long[] stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ Assert.assertEquals(3, counter.get());
|
|
|
|
+ Mockito.verify(badMock).getStats();
|
|
|
|
+ Mockito.verify(goodMock).getStats();
|
|
|
|
+ Mockito.verify(worseMock).getStats();
|
|
|
|
+
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(1, stats[0]);
|
|
|
|
+ // Ensure only the previous successful one is invoked
|
|
|
|
+ Mockito.verifyNoMoreInteractions(badMock);
|
|
|
|
+ Mockito.verifyNoMoreInteractions(worseMock);
|
|
|
|
+ Assert.assertEquals(4, counter.get());
|
|
|
|
+
|
|
|
|
+ // Flip to standby.. so now this should fail
|
|
|
|
+ isGood[0] = 2;
|
|
|
|
+ try {
|
|
|
|
+ provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.fail("Should fail since previously successful proxy now fails ");
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
+ Assert.assertTrue(ex instanceof IOException);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Assert.assertEquals(5, counter.get());
|
|
|
|
+
|
|
|
|
+ provider.performFailover(provider.getProxy().proxy);
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(2, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter updates twice since both proxies are tried on failure
|
|
|
|
+ Assert.assertEquals(7, counter.get());
|
|
|
|
+
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(2, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter updates only once now
|
|
|
|
+ Assert.assertEquals(8, counter.get());
|
|
|
|
+
|
|
|
|
+ // Flip to Other standby.. so now this should fail
|
|
|
|
+ isGood[0] = 3;
|
|
|
|
+ try {
|
|
|
|
+ provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.fail("Should fail since previously successful proxy now fails ");
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
+ Assert.assertTrue(ex instanceof IOException);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Counter should ipdate only 1 time
|
|
|
|
+ Assert.assertEquals(9, counter.get());
|
|
|
|
+
|
|
|
|
+ provider.performFailover(provider.getProxy().proxy);
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+
|
|
|
|
+ // Ensure correct proxy was called
|
|
|
|
+ Assert.assertEquals(3, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter updates twice since both proxies are tried on failure
|
|
|
|
+ Assert.assertEquals(11, counter.get());
|
|
|
|
+
|
|
|
|
+ stats = provider.getProxy().proxy.getStats();
|
|
|
|
+ Assert.assertTrue(stats.length == 1);
|
|
|
|
+ Assert.assertEquals(3, stats[0]);
|
|
|
|
+
|
|
|
|
+ // Counter updates only once now
|
|
|
|
+ Assert.assertEquals(12, counter.get());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private ProxyFactory<NamenodeProtocols> createFactory(
|
|
|
|
+ NamenodeProtocols... protos) {
|
|
|
|
+ final Iterator<NamenodeProtocols> iterator =
|
|
|
|
+ Lists.newArrayList(protos).iterator();
|
|
|
|
+ return new ProxyFactory<NamenodeProtocols>() {
|
|
|
|
+ @Override
|
|
|
|
+ public NamenodeProtocols createProxy(Configuration conf,
|
|
|
|
+ InetSocketAddress nnAddr, Class<NamenodeProtocols> xface,
|
|
|
|
+ UserGroupInformation ugi, boolean withRetries,
|
|
|
|
+ AtomicBoolean fallbackToSimpleAuth) throws IOException {
|
|
|
|
+ return iterator.next();
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+}
|