001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.master;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertNotEquals;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023
024import java.io.IOException;
025import java.util.List;
026import java.util.concurrent.TimeUnit;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.ClusterMetrics;
029import org.apache.hadoop.hbase.HBaseConfiguration;
030import org.apache.hadoop.hbase.HBaseTestingUtil;
031import org.apache.hadoop.hbase.LocalHBaseCluster;
032import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
033import org.apache.hadoop.hbase.StartTestingClusterOption;
034import org.apache.hadoop.hbase.client.RetriesExhaustedException;
035import org.apache.hadoop.hbase.exceptions.ConnectionClosedException;
036import org.apache.hadoop.hbase.testclassification.LargeTests;
037import org.apache.hadoop.hbase.testclassification.MasterTests;
038import org.apache.hadoop.hbase.util.JVMClusterUtil.MasterThread;
039import org.apache.hadoop.hbase.zookeeper.ReadOnlyZKClient;
040import org.junit.jupiter.api.BeforeEach;
041import org.junit.jupiter.api.Tag;
042import org.junit.jupiter.api.Test;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046import org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
047
048@Tag(MasterTests.TAG)
049@Tag(LargeTests.TAG)
050public class TestMasterShutdown {
051  private static final Logger LOG = LoggerFactory.getLogger(TestMasterShutdown.class);
052
053  private HBaseTestingUtil htu;
054
055  @BeforeEach
056  public void shutdownCluster() throws IOException {
057    if (htu != null) {
058      // an extra check in case the test cluster was not terminated after HBaseClassTestRule's
059      // Timeout interrupted the test thread.
060      LOG.warn("found non-null TestingUtility -- previous test did not terminate cleanly.");
061      htu.shutdownMiniCluster();
062    }
063  }
064
065  /**
066   * Simple test of shutdown.
067   * <p>
068   * Starts with three masters. Tells the active master to shutdown the cluster. Verifies that all
069   * masters are properly shutdown.
070   */
071  @Test
072  public void testMasterShutdown() throws Exception {
073    // Create config to use for this cluster
074    Configuration conf = HBaseConfiguration.create();
075
076    // Start the cluster
077    try {
078      htu = new HBaseTestingUtil(conf);
079      StartTestingClusterOption option = StartTestingClusterOption.builder().numMasters(3)
080        .numRegionServers(1).numDataNodes(1).build();
081      final SingleProcessHBaseCluster cluster = htu.startMiniCluster(option);
082
083      // wait for all master thread to spawn and start their run loop.
084      final long thirtySeconds = TimeUnit.SECONDS.toMillis(30);
085      final long oneSecond = TimeUnit.SECONDS.toMillis(1);
086      assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond, () -> {
087        final List<MasterThread> masterThreads = cluster.getMasterThreads();
088        return masterThreads != null && masterThreads.size() >= 3
089          && masterThreads.stream().allMatch(Thread::isAlive);
090      }));
091
092      // find the active master
093      final HMaster active = cluster.getMaster();
094      assertNotNull(active);
095
096      // make sure the other two are backup masters
097      ClusterMetrics status = active.getClusterMetrics();
098      assertEquals(2, status.getBackupMasterNames().size());
099
100      // tell the active master to shutdown the cluster
101      active.shutdown();
102      assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond,
103        () -> CollectionUtils.isEmpty(cluster.getLiveMasterThreads())));
104      assertNotEquals(-1, htu.waitFor(thirtySeconds, oneSecond,
105        () -> CollectionUtils.isEmpty(cluster.getLiveRegionServerThreads())));
106    } finally {
107      if (htu != null) {
108        htu.shutdownMiniCluster();
109        htu = null;
110      }
111    }
112  }
113
114  /**
115   * This test appears to be an intentional race between a thread that issues a shutdown RPC to the
116   * master, while the master is concurrently realizing it cannot initialize because there are no
117   * region servers available to it. The expected behavior is that master initialization is
118   * interruptable via said shutdown RPC.
119   */
120  @Test
121  public void testMasterShutdownBeforeStartingAnyRegionServer() throws Exception {
122    LocalHBaseCluster hbaseCluster = null;
123    try {
124      htu = new HBaseTestingUtil(createMasterShutdownBeforeStartingAnyRegionServerConfiguration());
125
126      // configure a cluster with
127      final StartTestingClusterOption options = StartTestingClusterOption.builder().numDataNodes(1)
128        .numMasters(1).numRegionServers(0).masterClass(HMaster.class)
129        .rsClass(SingleProcessHBaseCluster.MiniHBaseClusterRegionServer.class).createRootDir(true)
130        .build();
131
132      // Can't simply `htu.startMiniCluster(options)` because that method waits for the master to
133      // start completely. However, this test's premise is that a partially started master should
134      // still respond to a shutdown RPC. So instead, we manage each component lifecycle
135      // independently.
136      // I think it's not worth refactoring HTU's helper methods just for this class.
137      htu.startMiniDFSCluster(options.getNumDataNodes());
138      htu.startMiniZKCluster(options.getNumZkServers());
139      htu.createRootDir();
140      hbaseCluster = new LocalHBaseCluster(htu.getConfiguration(), options.getNumMasters(),
141        options.getNumRegionServers(), options.getMasterClass(), options.getRsClass());
142      final MasterThread masterThread = hbaseCluster.getMasters().get(0);
143
144      masterThread.start();
145      // Switching to master registry exacerbated a race in the master bootstrap that can result
146      // in a lost shutdown command (HBASE-8422, HBASE-23836). The race is essentially because
147      // the server manager in HMaster is not initialized by the time shutdown() RPC (below) is
148      // made to the master. The suspected reason as to why it was uncommon before HBASE-18095
149      // is because the connection creation with ZK registry is so slow that by then the server
150      // manager is usually init'ed in time for the RPC to be made. For now, adding an explicit
151      // wait() in the test, waiting for the server manager to become available.
152      final long timeout = TimeUnit.MINUTES.toMillis(10);
153      assertNotEquals(-1,
154        htu.waitFor(timeout, () -> masterThread.getMaster().getServerManager() != null),
155        "timeout waiting for server manager to become available.");
156
157      // Master has come up far enough that we can terminate it without creating a zombie.
158      try {
159        // HBASE-24327 : (Resolve Flaky connection issues)
160        // shutdown() RPC can have flaky ZK connection issues.
161        // e.g
162        // ERROR [RpcServer.priority.RWQ.Fifo.read.handler=1,queue=1,port=53033]
163        // master.HMaster(2878): ZooKeeper exception trying to set cluster as down in ZK
164        // org.apache.zookeeper.KeeperException$SystemErrorException:
165        // KeeperErrorCode = SystemError
166        //
167        // However, even when above flakes happen, shutdown call does get completed even if
168        // RPC call has failure. Hence, subsequent retries will never succeed as HMaster is
169        // already shutdown. Hence, it can fail. To resolve it, after making one shutdown()
170        // call, we are ignoring IOException.
171        htu.getConnection().getAdmin().shutdown();
172      } catch (RetriesExhaustedException e) {
173        if (e.getCause() instanceof ConnectionClosedException) {
174          LOG.info("Connection is Closed to the cluster. The cluster is already down.", e);
175        } else {
176          throw e;
177        }
178      }
179      LOG.info("Shutdown RPC sent.");
180      masterThread.join();
181    } finally {
182      if (hbaseCluster != null) {
183        hbaseCluster.shutdown();
184      }
185      if (htu != null) {
186        htu.shutdownMiniCluster();
187        htu = null;
188      }
189    }
190  }
191
192  /**
193   * Create a cluster configuration suitable for
194   * {@link #testMasterShutdownBeforeStartingAnyRegionServer()}.
195   */
196  private static Configuration createMasterShutdownBeforeStartingAnyRegionServerConfiguration() {
197    final Configuration conf = HBaseConfiguration.create();
198    // make sure the master will wait forever in the absence of a RS.
199    conf.setInt(ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, 1);
200    // don't need a long write pipeline for this test.
201    conf.setInt("dfs.replication", 1);
202    // reduce client retries
203    conf.setInt("hbase.client.retries.number", 1);
204    // Recoverable ZK configs are tuned more aggressively
205    conf.setInt(ReadOnlyZKClient.RECOVERY_RETRY, 3);
206    conf.setInt(ReadOnlyZKClient.RECOVERY_RETRY_INTERVAL_MILLIS, 100);
207    return conf;
208  }
209}