001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with this
004 * work for additional information regarding copyright ownership. The ASF
005 * licenses this file to you under the Apache License, Version 2.0 (the
006 * "License"); you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017package org.apache.hadoop.hbase.util;
018
019import java.io.BufferedReader;
020import java.io.BufferedWriter;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileWriter;
025import java.io.FilenameFilter;
026import java.io.IOException;
027import java.io.InputStreamReader;
028import java.io.PrintStream;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Scanner;
036import java.util.Set;
037import java.util.TreeMap;
038import java.util.regex.Matcher;
039import java.util.regex.Pattern;
040
041import org.apache.commons.io.FileUtils;
042import org.apache.hadoop.conf.Configuration;
043import org.apache.hadoop.hbase.HBaseTestingUtility;
044import org.apache.hadoop.hbase.HConstants;
045import org.apache.hadoop.hbase.MiniHBaseCluster;
046import org.apache.hadoop.hbase.TableName;
047import org.apache.hadoop.hbase.testclassification.LargeTests;
048import org.apache.hadoop.hbase.testclassification.MiscTests;
049import org.apache.hadoop.hbase.zookeeper.ZKUtil;
050import org.apache.hadoop.hdfs.MiniDFSCluster;
051import org.junit.experimental.categories.Category;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055/**
056 * A helper class for process-based mini-cluster tests. Unlike
057 * {@link MiniHBaseCluster}, starts daemons as separate processes, allowing to
058 * do real kill testing.
059 */
060@Category({MiscTests.class, LargeTests.class})
061public class ProcessBasedLocalHBaseCluster {
062
063  private final String hbaseHome, workDir;
064  private final Configuration conf;
065  private final int numMasters, numRegionServers, numDataNodes;
066  private final List<Integer> rsPorts, masterPorts;
067
068  private final int zkClientPort;
069
070  private static final int MAX_FILE_SIZE_OVERRIDE = 10 * 1000 * 1000;
071
072  private static final Logger LOG = LoggerFactory.getLogger(
073      ProcessBasedLocalHBaseCluster.class);
074
075  private List<String> daemonPidFiles =
076      Collections.synchronizedList(new ArrayList<String>());;
077
078  private boolean shutdownHookInstalled;
079
080  private String hbaseDaemonScript;
081
082  private MiniDFSCluster dfsCluster;
083
084  private HBaseTestingUtility testUtil;
085
086  private Thread logTailerThread;
087
088  private List<String> logTailDirs = Collections.synchronizedList(new ArrayList<String>());
089
090  private static enum ServerType {
091    MASTER("master"),
092    RS("regionserver"),
093    ZK("zookeeper");
094
095    private final String fullName;
096
097    private ServerType(String fullName) {
098      this.fullName = fullName;
099    }
100  }
101
102  /**
103   * Constructor. Modifies the passed configuration.
104   * @param hbaseHome the top directory of the HBase source tree
105   */
106  public ProcessBasedLocalHBaseCluster(Configuration conf,
107      int numDataNodes, int numRegionServers) {
108    this.conf = conf;
109    this.hbaseHome = HBaseHomePath.getHomePath();
110    this.numMasters = 1;
111    this.numRegionServers = numRegionServers;
112    this.workDir = hbaseHome + "/target/local_cluster";
113    this.numDataNodes = numDataNodes;
114
115    hbaseDaemonScript = hbaseHome + "/bin/hbase-daemon.sh";
116    zkClientPort = HBaseTestingUtility.randomFreePort();
117
118    this.rsPorts = sortedPorts(numRegionServers);
119    this.masterPorts = sortedPorts(numMasters);
120
121    conf.set(HConstants.ZOOKEEPER_QUORUM, HConstants.LOCALHOST);
122    conf.setInt(HConstants.ZOOKEEPER_CLIENT_PORT, zkClientPort);
123  }
124
125  /**
126   * Makes this local HBase cluster use a mini-DFS cluster. Must be called before
127   * {@link #startHBase()}.
128   * @throws IOException
129   */
130  public void startMiniDFS() throws Exception {
131    if (testUtil == null) {
132      testUtil = new HBaseTestingUtility(conf);
133    }
134    dfsCluster = testUtil.startMiniDFSCluster(numDataNodes);
135  }
136
137  /**
138   * Generates a list of random port numbers in the sorted order. A sorted
139   * order makes sense if we ever want to refer to these servers by their index
140   * in the returned array, e.g. server #0, #1, etc.
141   */
142  private static List<Integer> sortedPorts(int n) {
143    List<Integer> ports = new ArrayList<>(n);
144    for (int i = 0; i < n; ++i) {
145      ports.add(HBaseTestingUtility.randomFreePort());
146    }
147    Collections.sort(ports);
148    return ports;
149  }
150
151  public void startHBase() throws IOException {
152    startDaemonLogTailer();
153    cleanupOldState();
154
155    // start ZK
156    LOG.info("Starting ZooKeeper on port " + zkClientPort);
157    startZK();
158
159    HBaseTestingUtility.waitForHostPort(HConstants.LOCALHOST, zkClientPort);
160
161    for (int masterPort : masterPorts) {
162      startMaster(masterPort);
163    }
164
165    ZKUtil.waitForBaseZNode(conf);
166
167    for (int rsPort : rsPorts) {
168      startRegionServer(rsPort);
169    }
170
171    LOG.info("Waiting for HBase startup by scanning META");
172    int attemptsLeft = 10;
173    while (attemptsLeft-- > 0) {
174      try {
175        testUtil.getConnection().getTable(TableName.META_TABLE_NAME);
176      } catch (Exception e) {
177        LOG.info("Waiting for HBase to startup. Retries left: " + attemptsLeft,
178            e);
179        Threads.sleep(1000);
180      }
181    }
182
183    LOG.info("Process-based HBase Cluster with " + numRegionServers +
184        " region servers up and running... \n\n");
185  }
186
187  public void startRegionServer(int port) {
188    startServer(ServerType.RS, port);
189  }
190
191  public void startMaster(int port) {
192    startServer(ServerType.MASTER, port);
193  }
194
195  public void killRegionServer(int port) throws IOException {
196    killServer(ServerType.RS, port);
197  }
198
199  public void killMaster() throws IOException {
200    killServer(ServerType.MASTER, 0);
201  }
202
203  public void startZK() {
204    startServer(ServerType.ZK, 0);
205  }
206
207  private void executeCommand(String command) {
208    executeCommand(command, null);
209  }
210
211  private void executeCommand(String command, Map<String,
212      String> envOverrides) {
213    ensureShutdownHookInstalled();
214    LOG.debug("Command : " + command);
215
216    try {
217      String [] envp = null;
218      if (envOverrides != null) {
219        Map<String, String> map = new HashMap<>(System.getenv());
220        map.putAll(envOverrides);
221        envp = new String[map.size()];
222        int idx = 0;
223        for (Map.Entry<String, String> e: map.entrySet()) {
224          envp[idx++] = e.getKey() + "=" + e.getValue();
225        }
226      }
227
228      Process p = Runtime.getRuntime().exec(command, envp);
229
230      BufferedReader stdInput = new BufferedReader(
231          new InputStreamReader(p.getInputStream()));
232      BufferedReader stdError = new BufferedReader(
233          new InputStreamReader(p.getErrorStream()));
234
235      // read the output from the command
236      String s = null;
237      while ((s = stdInput.readLine()) != null) {
238        System.out.println(s);
239      }
240
241      // read any errors from the attempted command
242      while ((s = stdError.readLine()) != null) {
243        System.out.println(s);
244      }
245    } catch (IOException e) {
246      LOG.error("Error running: " + command, e);
247    }
248  }
249
250  private void shutdownAllProcesses() {
251    LOG.info("Killing daemons using pid files");
252    final List<String> pidFiles = new ArrayList<>(daemonPidFiles);
253    for (String pidFile : pidFiles) {
254      int pid = 0;
255      try {
256        pid = readPidFromFile(pidFile);
257      } catch (IOException ex) {
258        LOG.error("Could not read pid from file " + pidFile);
259      }
260
261      if (pid > 0) {
262        LOG.info("Killing pid " + pid + " (" + pidFile + ")");
263        killProcess(pid);
264      }
265    }
266  }
267
268  private void ensureShutdownHookInstalled() {
269    if (shutdownHookInstalled) {
270      return;
271    }
272
273    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
274      @Override
275      public void run() {
276        shutdownAllProcesses();
277      }
278    }));
279
280    shutdownHookInstalled = true;
281  }
282
283  private void cleanupOldState() {
284    executeCommand("rm -rf " + workDir);
285  }
286
287  private void writeStringToFile(String s, String fileName) {
288    try {
289      BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
290      out.write(s);
291      out.close();
292    } catch (IOException e) {
293      LOG.error("Error writing to: " + fileName, e);
294    }
295  }
296
297  private String serverWorkingDir(ServerType serverType, int port) {
298    return workDir + "/" + serverType + "-" + port;
299  }
300
301  private int getServerPID(ServerType serverType, int port) throws IOException {
302    String pidFile = pidFilePath(serverType, port);
303    return readPidFromFile(pidFile);
304  }
305
306  private static int readPidFromFile(String pidFile) throws IOException {
307    Scanner scanner = new Scanner(new File(pidFile));
308    try {
309      return scanner.nextInt();
310    } finally {
311      scanner.close();
312    }
313  }
314
315  private String pidFilePath(ServerType serverType, int port) {
316    String dir = serverWorkingDir(serverType, port);
317    String user = System.getenv("USER");
318    String pidFile = String.format("%s/hbase-%s-%s.pid",
319                                   dir, user, serverType.fullName);
320    return pidFile;
321  }
322
323  private void killServer(ServerType serverType, int port) throws IOException {
324    int pid = getServerPID(serverType, port);
325    if (pid > 0) {
326      LOG.info("Killing " + serverType + "; pid=" + pid);
327      killProcess(pid);
328    }
329  }
330
331  private void killProcess(int pid) {
332    String cmd = "kill -s KILL " + pid;
333    executeCommand(cmd);
334  }
335
336  private void startServer(ServerType serverType, int rsPort) {
337    // create working directory for this region server.
338    String dir = serverWorkingDir(serverType, rsPort);
339    String confStr = generateConfig(serverType, rsPort, dir);
340    LOG.debug("Creating directory " + dir);
341    new File(dir).mkdirs();
342
343    writeStringToFile(confStr, dir + "/hbase-site.xml");
344
345    // Set debug options to an empty string so that hbase-config.sh does not configure them
346    // using default ports. If we want to run remote debugging on process-based local cluster's
347    // daemons, we can automatically choose non-conflicting JDWP and JMX ports for each daemon
348    // and specify them here.
349    writeStringToFile(
350        "unset HBASE_MASTER_OPTS\n" +
351        "unset HBASE_REGIONSERVER_OPTS\n" +
352        "unset HBASE_ZOOKEEPER_OPTS\n" +
353        "HBASE_MASTER_DBG_OPTS=' '\n" +
354        "HBASE_REGIONSERVER_DBG_OPTS=' '\n" +
355        "HBASE_ZOOKEEPER_DBG_OPTS=' '\n" +
356        "HBASE_MASTER_JMX_OPTS=' '\n" +
357        "HBASE_REGIONSERVER_JMX_OPTS=' '\n" +
358        "HBASE_ZOOKEEPER_JMX_OPTS=' '\n",
359        dir + "/hbase-env.sh");
360
361    Map<String, String> envOverrides = new HashMap<>();
362    envOverrides.put("HBASE_LOG_DIR", dir);
363    envOverrides.put("HBASE_PID_DIR", dir);
364    try {
365      FileUtils.copyFile(
366          new File(hbaseHome, "conf/log4j.properties"),
367          new File(dir, "log4j.properties"));
368    } catch (IOException ex) {
369      LOG.error("Could not install log4j.properties into " + dir);
370    }
371
372    executeCommand(hbaseDaemonScript + " --config " + dir +
373                   " start " + serverType.fullName, envOverrides);
374    daemonPidFiles.add(pidFilePath(serverType, rsPort));
375    logTailDirs.add(dir);
376  }
377
378  private final String generateConfig(ServerType serverType, int rpcPort,
379      String daemonDir) {
380    StringBuilder sb = new StringBuilder();
381    Map<String, Object> confMap = new TreeMap<>();
382    confMap.put(HConstants.CLUSTER_DISTRIBUTED, true);
383
384    if (serverType == ServerType.MASTER) {
385      confMap.put(HConstants.MASTER_PORT, rpcPort);
386
387      int masterInfoPort = HBaseTestingUtility.randomFreePort();
388      reportWebUIPort("master", masterInfoPort);
389      confMap.put(HConstants.MASTER_INFO_PORT, masterInfoPort);
390    } else if (serverType == ServerType.RS) {
391      confMap.put(HConstants.REGIONSERVER_PORT, rpcPort);
392
393      int rsInfoPort = HBaseTestingUtility.randomFreePort();
394      reportWebUIPort("region server", rsInfoPort);
395      confMap.put(HConstants.REGIONSERVER_INFO_PORT, rsInfoPort);
396    } else {
397      confMap.put(HConstants.ZOOKEEPER_DATA_DIR, daemonDir);
398    }
399
400    confMap.put(HConstants.ZOOKEEPER_CLIENT_PORT, zkClientPort);
401    confMap.put(HConstants.HREGION_MAX_FILESIZE, MAX_FILE_SIZE_OVERRIDE);
402
403    if (dfsCluster != null) {
404      String fsURL = "hdfs://" + HConstants.LOCALHOST + ":" + dfsCluster.getNameNodePort();
405      confMap.put("fs.defaultFS", fsURL);
406      confMap.put("hbase.rootdir", fsURL + "/hbase_test");
407    }
408
409    sb.append("<configuration>\n");
410    for (Map.Entry<String, Object> entry : confMap.entrySet()) {
411      sb.append("  <property>\n");
412      sb.append("    <name>" + entry.getKey() + "</name>\n");
413      sb.append("    <value>" + entry.getValue() + "</value>\n");
414      sb.append("  </property>\n");
415    }
416    sb.append("</configuration>\n");
417    return sb.toString();
418  }
419
420  private static void reportWebUIPort(String daemon, int port) {
421    LOG.info("Local " + daemon + " web UI is at http://"
422        + HConstants.LOCALHOST + ":" + port);
423  }
424
425  public Configuration getConf() {
426    return conf;
427  }
428
429  public void shutdown() {
430    if (dfsCluster != null) {
431      dfsCluster.shutdown();
432    }
433    shutdownAllProcesses();
434  }
435
436  private static final Pattern TO_REMOVE_FROM_LOG_LINES_RE =
437      Pattern.compile("org\\.apache\\.hadoop\\.hbase\\.");
438
439  private static final Pattern LOG_PATH_FORMAT_RE =
440      Pattern.compile("^.*/([A-Z]+)-(\\d+)/[^/]+$");
441
442  private static String processLine(String line) {
443    Matcher m = TO_REMOVE_FROM_LOG_LINES_RE.matcher(line);
444    return m.replaceAll("");
445  }
446
447  private final class LocalDaemonLogTailer implements Runnable {
448    private final Set<String> tailedFiles = new HashSet<>();
449    private final List<String> dirList = new ArrayList<>();
450    private final Object printLock = new Object();
451
452    private final FilenameFilter LOG_FILES = new FilenameFilter() {
453      @Override
454      public boolean accept(File dir, String name) {
455        return name.endsWith(".out") || name.endsWith(".log");
456      }
457    };
458
459    @Override
460    public void run() {
461      try {
462        runInternal();
463      } catch (IOException ex) {
464        LOG.error(ex.toString(), ex);
465      }
466    }
467
468    private void runInternal() throws IOException {
469      Thread.currentThread().setName(getClass().getSimpleName());
470      while (true) {
471        scanDirs();
472        try {
473          Thread.sleep(500);
474        } catch (InterruptedException e) {
475          LOG.error("Log tailer thread interrupted", e);
476          break;
477        }
478      }
479    }
480
481    private void scanDirs() throws FileNotFoundException {
482      dirList.clear();
483      dirList.addAll(logTailDirs);
484      for (String d : dirList) {
485        for (File f : new File(d).listFiles(LOG_FILES)) {
486          String filePath = f.getAbsolutePath();
487          if (!tailedFiles.contains(filePath)) {
488            tailedFiles.add(filePath);
489            startTailingFile(filePath);
490          }
491        }
492      }
493    }
494
495    private void startTailingFile(final String filePath) throws FileNotFoundException {
496      final PrintStream dest = filePath.endsWith(".log") ? System.err : System.out;
497      final ServerType serverType;
498      final int serverPort;
499      Matcher m = LOG_PATH_FORMAT_RE.matcher(filePath);
500      if (m.matches()) {
501        serverType = ServerType.valueOf(m.group(1));
502        serverPort = Integer.valueOf(m.group(2));
503      } else {
504        LOG.error("Unrecognized log path format: " + filePath);
505        return;
506      }
507      final String logMsgPrefix =
508          "[" + serverType + (serverPort != 0 ? ":" + serverPort : "") + "] ";
509
510      LOG.debug("Tailing " + filePath);
511      Thread t = new Thread(new Runnable() {
512        @Override
513        public void run() {
514          try {
515            FileInputStream fis = new FileInputStream(filePath);
516            BufferedReader br = new BufferedReader(new InputStreamReader(fis));
517            String line;
518            while (true) {
519              try {
520                Thread.sleep(200);
521              } catch (InterruptedException e) {
522                LOG.error("Tailer for " + filePath + " interrupted");
523                break;
524              }
525              while ((line = br.readLine()) != null) {
526                line = logMsgPrefix + processLine(line);
527                synchronized (printLock) {
528                  if (line.endsWith("\n")) {
529                    dest.print(line);
530                  } else {
531                    dest.println(line);
532                  }
533                  dest.flush();
534                }
535              }
536            }
537          } catch (IOException ex) {
538            LOG.error("Failed tailing " + filePath, ex);
539          }
540        }
541      });
542      t.setDaemon(true);
543      t.setName("Tailer for " + filePath);
544      t.start();
545    }
546
547  }
548
549  private void startDaemonLogTailer() {
550    logTailerThread = new Thread(new LocalDaemonLogTailer());
551    logTailerThread.setDaemon(true);
552    logTailerThread.start();
553  }
554
555}
556