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