blob: cd8c4d2a16c6c4618dbde704147bcf1f67e99056 [file] [log] [blame]
/*
* 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.hbase.master;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.ClusterMetrics;
import org.apache.hadoop.hbase.HBaseClassTestRule;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.LocalHBaseCluster;
import org.apache.hadoop.hbase.MiniHBaseCluster;
import org.apache.hadoop.hbase.StartMiniClusterOption;
import org.apache.hadoop.hbase.client.RetriesExhaustedException;
import org.apache.hadoop.hbase.exceptions.ConnectionClosedException;
import org.apache.hadoop.hbase.testclassification.LargeTests;
import org.apache.hadoop.hbase.testclassification.MasterTests;
import org.apache.hadoop.hbase.util.JVMClusterUtil.MasterThread;
import org.apache.hadoop.hbase.zookeeper.ReadOnlyZKClient;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
@Category({MasterTests.class, LargeTests.class})
public class TestMasterShutdown {
private static final Logger LOG = LoggerFactory.getLogger(TestMasterShutdown.class);
@ClassRule
public static final HBaseClassTestRule CLASS_RULE =
HBaseClassTestRule.forClass(TestMasterShutdown.class);
private HBaseTestingUtility htu;
@Before
public void shutdownCluster() throws IOException {
if (htu != null) {
// an extra check in case the test cluster was not terminated after HBaseClassTestRule's
// Timeout interrupted the test thread.
LOG.warn("found non-null TestingUtility -- previous test did not terminate cleanly.");
htu.shutdownMiniCluster();
}
}
/**
* Simple test of shutdown.
* <p>
* Starts with three masters. Tells the active master to shutdown the cluster.
* Verifies that all masters are properly shutdown.
*/
@Test
public void testMasterShutdown() throws Exception {
// Create config to use for this cluster
Configuration conf = HBaseConfiguration.create();
// Start the cluster
try {
htu = new HBaseTestingUtility(conf);
StartMiniClusterOption option = StartMiniClusterOption.builder()
.numMasters(3)
.numRegionServers(1)
.numDataNodes(1)
.build();
final MiniHBaseCluster cluster = htu.startMiniCluster(option);
// wait for all master thread to spawn and start their run loop.
final long thirtySeconds = TimeUnit.SECONDS.toMillis(30);
final long oneSecond = TimeUnit.SECONDS.toMillis(1);
assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond, () -> {
final List<MasterThread> masterThreads = cluster.getMasterThreads();
return masterThreads != null
&& masterThreads.size() >= 3
&& masterThreads.stream().allMatch(Thread::isAlive);
}));
// find the active master
final HMaster active = cluster.getMaster();
assertNotNull(active);
// make sure the other two are backup masters
ClusterMetrics status = active.getClusterMetrics();
assertEquals(2, status.getBackupMasterNames().size());
// tell the active master to shutdown the cluster
active.shutdown();
assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond,
() -> CollectionUtils.isEmpty(cluster.getLiveMasterThreads())));
assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond,
() -> CollectionUtils.isEmpty(cluster.getLiveRegionServerThreads())));
} finally {
if (htu != null) {
htu.shutdownMiniCluster();
htu = null;
}
}
}
/**
* This test appears to be an intentional race between a thread that issues a shutdown RPC to the
* master, while the master is concurrently realizing it cannot initialize because there are no
* region servers available to it. The expected behavior is that master initialization is
* interruptable via said shutdown RPC.
*/
@Test
public void testMasterShutdownBeforeStartingAnyRegionServer() throws Exception {
LocalHBaseCluster hbaseCluster = null;
try {
htu = new HBaseTestingUtility(
createMasterShutdownBeforeStartingAnyRegionServerConfiguration());
// configure a cluster with
final StartMiniClusterOption options = StartMiniClusterOption.builder()
.numDataNodes(1)
.numMasters(1)
.numRegionServers(0)
.masterClass(HMaster.class)
.rsClass(MiniHBaseCluster.MiniHBaseClusterRegionServer.class)
.createRootDir(true)
.build();
// Can't simply `htu.startMiniCluster(options)` because that method waits for the master to
// start completely. However, this test's premise is that a partially started master should
// still respond to a shutdown RPC. So instead, we manage each component lifecycle
// independently.
// I think it's not worth refactoring HTU's helper methods just for this class.
htu.startMiniDFSCluster(options.getNumDataNodes());
htu.startMiniZKCluster(options.getNumZkServers());
htu.createRootDir();
hbaseCluster = new LocalHBaseCluster(htu.getConfiguration(), options.getNumMasters(),
options.getNumRegionServers(), options.getMasterClass(), options.getRsClass());
final MasterThread masterThread = hbaseCluster.getMasters().get(0);
masterThread.start();
// Switching to master registry exacerbated a race in the master bootstrap that can result
// in a lost shutdown command (HBASE-8422, HBASE-23836). The race is essentially because
// the server manager in HMaster is not initialized by the time shutdown() RPC (below) is
// made to the master. The suspected reason as to why it was uncommon before HBASE-18095
// is because the connection creation with ZK registry is so slow that by then the server
// manager is usually init'ed in time for the RPC to be made. For now, adding an explicit
// wait() in the test, waiting for the server manager to become available.
final long timeout = TimeUnit.MINUTES.toMillis(10);
assertNotEquals("timeout waiting for server manager to become available.", -1,
htu.waitFor(timeout, () -> masterThread.getMaster().getServerManager() != null));
// Master has come up far enough that we can terminate it without creating a zombie.
try {
// HBASE-24327 : (Resolve Flaky connection issues)
// shutdown() RPC can have flaky ZK connection issues.
// e.g
// ERROR [RpcServer.priority.RWQ.Fifo.read.handler=1,queue=1,port=53033]
// master.HMaster(2878): ZooKeeper exception trying to set cluster as down in ZK
// org.apache.zookeeper.KeeperException$SystemErrorException:
// KeeperErrorCode = SystemError
//
// However, even when above flakes happen, shutdown call does get completed even if
// RPC call has failure. Hence, subsequent retries will never succeed as HMaster is
// already shutdown. Hence, it can fail. To resolve it, after making one shutdown()
// call, we are ignoring IOException.
htu.getConnection().getAdmin().shutdown();
} catch (RetriesExhaustedException e) {
if (e.getCause() instanceof ConnectionClosedException) {
LOG.info("Connection is Closed to the cluster. The cluster is already down.", e);
} else {
throw e;
}
}
LOG.info("Shutdown RPC sent.");
masterThread.join();
} finally {
if (hbaseCluster != null) {
hbaseCluster.shutdown();
}
if (htu != null) {
htu.shutdownMiniCluster();
htu = null;
}
}
}
/**
* Create a cluster configuration suitable for
* {@link #testMasterShutdownBeforeStartingAnyRegionServer()}.
*/
private static Configuration createMasterShutdownBeforeStartingAnyRegionServerConfiguration() {
final Configuration conf = HBaseConfiguration.create();
// make sure the master will wait forever in the absence of a RS.
conf.setInt(ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, 1);
// don't need a long write pipeline for this test.
conf.setInt("dfs.replication", 1);
// reduce client retries
conf.setInt("hbase.client.retries.number", 1);
// Recoverable ZK configs are tuned more aggressively
conf.setInt(ReadOnlyZKClient.RECOVERY_RETRY, 3);
conf.setInt(ReadOnlyZKClient.RECOVERY_RETRY_INTERVAL_MILLIS, 100);
return conf;
}
}