org.apache.hadoop.hbase.util.ProcessBasedLocalHBaseCluster.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.util.ProcessBasedLocalHBaseCluster.java

Source

/*
 * 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.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.LargeTests;
import org.apache.hadoop.hbase.MiniHBaseCluster;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.zookeeper.ZKUtil;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.junit.experimental.categories.Category;

/**
 * A helper class for process-based mini-cluster tests. Unlike
 * {@link MiniHBaseCluster}, starts daemons as separate processes, allowing to
 * do real kill testing.
 */
@Category(LargeTests.class)
public class ProcessBasedLocalHBaseCluster {

    private final String hbaseHome, workDir;
    private final Configuration conf;
    private final int numMasters, numRegionServers, numDataNodes;
    private final List<Integer> rsPorts, masterPorts;

    private final int zkClientPort;

    private static final int MAX_FILE_SIZE_OVERRIDE = 10 * 1000 * 1000;

    private static final Log LOG = LogFactory.getLog(ProcessBasedLocalHBaseCluster.class);

    private List<String> daemonPidFiles = Collections.synchronizedList(new ArrayList<String>());;

    private boolean shutdownHookInstalled;

    private String hbaseDaemonScript;

    private MiniDFSCluster dfsCluster;

    private HBaseTestingUtility testUtil;

    private Thread logTailerThread;

    private List<String> logTailDirs = Collections.synchronizedList(new ArrayList<String>());

    private static enum ServerType {
        MASTER("master"), RS("regionserver"), ZK("zookeeper");

        private final String fullName;

        private ServerType(String fullName) {
            this.fullName = fullName;
        }
    }

    /**
     * Constructor. Modifies the passed configuration.
     * @param hbaseHome the top directory of the HBase source tree
     */
    public ProcessBasedLocalHBaseCluster(Configuration conf, int numDataNodes, int numRegionServers) {
        this.conf = conf;
        this.hbaseHome = HBaseHomePath.getHomePath();
        this.numMasters = 1;
        this.numRegionServers = numRegionServers;
        this.workDir = hbaseHome + "/target/local_cluster";
        this.numDataNodes = numDataNodes;

        hbaseDaemonScript = hbaseHome + "/bin/hbase-daemon.sh";
        zkClientPort = HBaseTestingUtility.randomFreePort();

        this.rsPorts = sortedPorts(numRegionServers);
        this.masterPorts = sortedPorts(numMasters);

        conf.set(HConstants.ZOOKEEPER_QUORUM, HConstants.LOCALHOST);
        conf.setInt(HConstants.ZOOKEEPER_CLIENT_PORT, zkClientPort);
    }

    /**
     * Makes this local HBase cluster use a mini-DFS cluster. Must be called before
     * {@link #startHBase()}.
     * @throws IOException
     */
    public void startMiniDFS() throws Exception {
        if (testUtil == null) {
            testUtil = new HBaseTestingUtility(conf);
        }
        dfsCluster = testUtil.startMiniDFSCluster(numDataNodes);
    }

    /**
     * Generates a list of random port numbers in the sorted order. A sorted
     * order makes sense if we ever want to refer to these servers by their index
     * in the returned array, e.g. server #0, #1, etc.
     */
    private static List<Integer> sortedPorts(int n) {
        List<Integer> ports = new ArrayList<Integer>(n);
        for (int i = 0; i < n; ++i) {
            ports.add(HBaseTestingUtility.randomFreePort());
        }
        Collections.sort(ports);
        return ports;
    }

    public void startHBase() throws IOException {
        startDaemonLogTailer();
        cleanupOldState();

        // start ZK
        LOG.info("Starting ZooKeeper on port " + zkClientPort);
        startZK();

        HBaseTestingUtility.waitForHostPort(HConstants.LOCALHOST, zkClientPort);

        for (int masterPort : masterPorts) {
            startMaster(masterPort);
        }

        ZKUtil.waitForBaseZNode(conf);

        for (int rsPort : rsPorts) {
            startRegionServer(rsPort);
        }

        LOG.info("Waiting for HBase startup by scanning META");
        int attemptsLeft = 10;
        while (attemptsLeft-- > 0) {
            try {
                new HTable(conf, TableName.META_TABLE_NAME);
            } catch (Exception e) {
                LOG.info("Waiting for HBase to startup. Retries left: " + attemptsLeft, e);
                Threads.sleep(1000);
            }
        }

        LOG.info("Process-based HBase Cluster with " + numRegionServers + " region servers up and running... \n\n");
    }

    public void startRegionServer(int port) {
        startServer(ServerType.RS, port);
    }

    public void startMaster(int port) {
        startServer(ServerType.MASTER, port);
    }

    public void killRegionServer(int port) throws IOException {
        killServer(ServerType.RS, port);
    }

    public void killMaster() throws IOException {
        killServer(ServerType.MASTER, 0);
    }

    public void startZK() {
        startServer(ServerType.ZK, 0);
    }

    private void executeCommand(String command) {
        executeCommand(command, null);
    }

    private void executeCommand(String command, Map<String, String> envOverrides) {
        ensureShutdownHookInstalled();
        LOG.debug("Command : " + command);

        try {
            String[] envp = null;
            if (envOverrides != null) {
                Map<String, String> map = new HashMap<String, String>(System.getenv());
                map.putAll(envOverrides);
                envp = new String[map.size()];
                int idx = 0;
                for (Map.Entry<String, String> e : map.entrySet()) {
                    envp[idx++] = e.getKey() + "=" + e.getValue();
                }
            }

            Process p = Runtime.getRuntime().exec(command, envp);

            BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
            BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()));

            // read the output from the command
            String s = null;
            while ((s = stdInput.readLine()) != null) {
                System.out.println(s);
            }

            // read any errors from the attempted command
            while ((s = stdError.readLine()) != null) {
                System.out.println(s);
            }
        } catch (IOException e) {
            LOG.error("Error running: " + command, e);
        }
    }

    private void shutdownAllProcesses() {
        LOG.info("Killing daemons using pid files");
        final List<String> pidFiles = new ArrayList<String>(daemonPidFiles);
        for (String pidFile : pidFiles) {
            int pid = 0;
            try {
                pid = readPidFromFile(pidFile);
            } catch (IOException ex) {
                LOG.error("Could not read pid from file " + pidFile);
            }

            if (pid > 0) {
                LOG.info("Killing pid " + pid + " (" + pidFile + ")");
                killProcess(pid);
            }
        }
    }

    private void ensureShutdownHookInstalled() {
        if (shutdownHookInstalled) {
            return;
        }

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                shutdownAllProcesses();
            }
        }));

        shutdownHookInstalled = true;
    }

    private void cleanupOldState() {
        executeCommand("rm -rf " + workDir);
    }

    private void writeStringToFile(String s, String fileName) {
        try {
            BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
            out.write(s);
            out.close();
        } catch (IOException e) {
            LOG.error("Error writing to: " + fileName, e);
        }
    }

    private String serverWorkingDir(ServerType serverType, int port) {
        return workDir + "/" + serverType + "-" + port;
    }

    private int getServerPID(ServerType serverType, int port) throws IOException {
        String pidFile = pidFilePath(serverType, port);
        return readPidFromFile(pidFile);
    }

    private static int readPidFromFile(String pidFile) throws IOException {
        Scanner scanner = new Scanner(new File(pidFile));
        try {
            return scanner.nextInt();
        } finally {
            scanner.close();
        }
    }

    private String pidFilePath(ServerType serverType, int port) {
        String dir = serverWorkingDir(serverType, port);
        String user = System.getenv("USER");
        String pidFile = String.format("%s/hbase-%s-%s.pid", dir, user, serverType.fullName);
        return pidFile;
    }

    private void killServer(ServerType serverType, int port) throws IOException {
        int pid = getServerPID(serverType, port);
        if (pid > 0) {
            LOG.info("Killing " + serverType + "; pid=" + pid);
            killProcess(pid);
        }
    }

    private void killProcess(int pid) {
        String cmd = "kill -s KILL " + pid;
        executeCommand(cmd);
    }

    private void startServer(ServerType serverType, int rsPort) {
        // create working directory for this region server.
        String dir = serverWorkingDir(serverType, rsPort);
        String confStr = generateConfig(serverType, rsPort, dir);
        LOG.debug("Creating directory " + dir);
        new File(dir).mkdirs();

        writeStringToFile(confStr, dir + "/hbase-site.xml");

        // Set debug options to an empty string so that hbase-config.sh does not configure them
        // using default ports. If we want to run remote debugging on process-based local cluster's
        // daemons, we can automatically choose non-conflicting JDWP and JMX ports for each daemon
        // and specify them here.
        writeStringToFile(
                "unset HBASE_MASTER_OPTS\n" + "unset HBASE_REGIONSERVER_OPTS\n" + "unset HBASE_ZOOKEEPER_OPTS\n"
                        + "HBASE_MASTER_DBG_OPTS=' '\n" + "HBASE_REGIONSERVER_DBG_OPTS=' '\n"
                        + "HBASE_ZOOKEEPER_DBG_OPTS=' '\n" + "HBASE_MASTER_JMX_OPTS=' '\n"
                        + "HBASE_REGIONSERVER_JMX_OPTS=' '\n" + "HBASE_ZOOKEEPER_JMX_OPTS=' '\n",
                dir + "/hbase-env.sh");

        Map<String, String> envOverrides = new HashMap<String, String>();
        envOverrides.put("HBASE_LOG_DIR", dir);
        envOverrides.put("HBASE_PID_DIR", dir);
        try {
            FileUtils.copyFile(new File(hbaseHome, "conf/log4j.properties"), new File(dir, "log4j.properties"));
        } catch (IOException ex) {
            LOG.error("Could not install log4j.properties into " + dir);
        }

        executeCommand(hbaseDaemonScript + " --config " + dir + " start " + serverType.fullName, envOverrides);
        daemonPidFiles.add(pidFilePath(serverType, rsPort));
        logTailDirs.add(dir);
    }

    private final String generateConfig(ServerType serverType, int rpcPort, String daemonDir) {
        StringBuilder sb = new StringBuilder();
        Map<String, Object> confMap = new TreeMap<String, Object>();
        confMap.put(HConstants.CLUSTER_DISTRIBUTED, true);

        if (serverType == ServerType.MASTER) {
            confMap.put(HConstants.MASTER_PORT, rpcPort);

            int masterInfoPort = HBaseTestingUtility.randomFreePort();
            reportWebUIPort("master", masterInfoPort);
            confMap.put(HConstants.MASTER_INFO_PORT, masterInfoPort);
        } else if (serverType == ServerType.RS) {
            confMap.put(HConstants.REGIONSERVER_PORT, rpcPort);

            int rsInfoPort = HBaseTestingUtility.randomFreePort();
            reportWebUIPort("region server", rsInfoPort);
            confMap.put(HConstants.REGIONSERVER_INFO_PORT, rsInfoPort);
        } else {
            confMap.put(HConstants.ZOOKEEPER_DATA_DIR, daemonDir);
        }

        confMap.put(HConstants.ZOOKEEPER_CLIENT_PORT, zkClientPort);
        confMap.put(HConstants.HREGION_MAX_FILESIZE, MAX_FILE_SIZE_OVERRIDE);

        if (dfsCluster != null) {
            String fsURL = "hdfs://" + HConstants.LOCALHOST + ":" + dfsCluster.getNameNodePort();
            confMap.put("fs.defaultFS", fsURL);
            confMap.put("hbase.rootdir", fsURL + "/hbase_test");
        }

        sb.append("<configuration>\n");
        for (Map.Entry<String, Object> entry : confMap.entrySet()) {
            sb.append("  <property>\n");
            sb.append("    <name>" + entry.getKey() + "</name>\n");
            sb.append("    <value>" + entry.getValue() + "</value>\n");
            sb.append("  </property>\n");
        }
        sb.append("</configuration>\n");
        return sb.toString();
    }

    private static void reportWebUIPort(String daemon, int port) {
        LOG.info("Local " + daemon + " web UI is at http://" + HConstants.LOCALHOST + ":" + port);
    }

    public Configuration getConf() {
        return conf;
    }

    public void shutdown() {
        if (dfsCluster != null) {
            dfsCluster.shutdown();
        }
        shutdownAllProcesses();
    }

    private static final Pattern TO_REMOVE_FROM_LOG_LINES_RE = Pattern.compile("org\\.apache\\.hadoop\\.hbase\\.");

    private static final Pattern LOG_PATH_FORMAT_RE = Pattern.compile("^.*/([A-Z]+)-(\\d+)/[^/]+$");

    private static String processLine(String line) {
        Matcher m = TO_REMOVE_FROM_LOG_LINES_RE.matcher(line);
        return m.replaceAll("");
    }

    private final class LocalDaemonLogTailer implements Runnable {
        private final Set<String> tailedFiles = new HashSet<String>();
        private final List<String> dirList = new ArrayList<String>();
        private final Object printLock = new Object();

        private final FilenameFilter LOG_FILES = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".out") || name.endsWith(".log");
            }
        };

        @Override
        public void run() {
            try {
                runInternal();
            } catch (IOException ex) {
                LOG.error(ex);
            }
        }

        private void runInternal() throws IOException {
            Thread.currentThread().setName(getClass().getSimpleName());
            while (true) {
                scanDirs();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    LOG.error("Log tailer thread interrupted", e);
                    break;
                }
            }
        }

        private void scanDirs() throws FileNotFoundException {
            dirList.clear();
            dirList.addAll(logTailDirs);
            for (String d : dirList) {
                for (File f : new File(d).listFiles(LOG_FILES)) {
                    String filePath = f.getAbsolutePath();
                    if (!tailedFiles.contains(filePath)) {
                        tailedFiles.add(filePath);
                        startTailingFile(filePath);
                    }
                }
            }
        }

        private void startTailingFile(final String filePath) throws FileNotFoundException {
            final PrintStream dest = filePath.endsWith(".log") ? System.err : System.out;
            final ServerType serverType;
            final int serverPort;
            Matcher m = LOG_PATH_FORMAT_RE.matcher(filePath);
            if (m.matches()) {
                serverType = ServerType.valueOf(m.group(1));
                serverPort = Integer.valueOf(m.group(2));
            } else {
                LOG.error("Unrecognized log path format: " + filePath);
                return;
            }
            final String logMsgPrefix = "[" + serverType + (serverPort != 0 ? ":" + serverPort : "") + "] ";

            LOG.debug("Tailing " + filePath);
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        FileInputStream fis = new FileInputStream(filePath);
                        BufferedReader br = new BufferedReader(new InputStreamReader(fis));
                        String line;
                        while (true) {
                            try {
                                Thread.sleep(200);
                            } catch (InterruptedException e) {
                                LOG.error("Tailer for " + filePath + " interrupted");
                                break;
                            }
                            while ((line = br.readLine()) != null) {
                                line = logMsgPrefix + processLine(line);
                                synchronized (printLock) {
                                    if (line.endsWith("\n")) {
                                        dest.print(line);
                                    } else {
                                        dest.println(line);
                                    }
                                    dest.flush();
                                }
                            }
                        }
                    } catch (IOException ex) {
                        LOG.error("Failed tailing " + filePath, ex);
                    }
                }
            });
            t.setDaemon(true);
            t.setName("Tailer for " + filePath);
            t.start();
        }

    }

    private void startDaemonLogTailer() {
        logTailerThread = new Thread(new LocalDaemonLogTailer());
        logTailerThread.setDaemon(true);
        logTailerThread.start();
    }

}