// Copyright 2017 JanusGraph Authors
//
// Licensed 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.janusgraph;
import com.google.common.base.Joiner;
import org.janusgraph.diskstorage.configuration.ModifiableConfiguration;
import org.janusgraph.diskstorage.configuration.WriteConfiguration;
import org.janusgraph.diskstorage.hbase.HBaseStoreManager;
import org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration;
import org.janusgraph.graphdb.database.idassigner.placement.SimpleBulkPlacementStrategy;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.HConnection;
import org.apache.hadoop.hbase.client.HConnectionManager;
import org.apache.hadoop.hbase.util.VersionInfo;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HBaseStorageSetup {
private static final Logger log = LoggerFactory.getLogger(HBaseStorageSetup.class);
// hbase config for testing
public static final String HBASE_PARENT_DIR_PROP = "test.hbase.parentdir";
private static final Pattern HBASE_SUPPORTED_VERSION_PATTERN = Pattern.compile("^((0\\.9[8])|(1\\.[012]))\\..*");
private static final String HBASE_VERSION_1_STRING = "1.";
private static final String HBASE_PARENT_DIR;
private static final String HBASE_TARGET_VERSION = VersionInfo.getVersion();
static {
String parentDir = "..";
String tmp = System.getProperty(HBASE_PARENT_DIR_PROP);
if (null != tmp) {
parentDir = tmp;
}
HBASE_PARENT_DIR = parentDir;
}
private static final String HBASE_STAT_FILE = "/tmp/janusgraph-hbase-test-daemon.stat";
private volatile static HBaseStatus HBASE = null;
public static String getScriptDirForHBaseVersion(String hv) {
return getDirForHBaseVersion(hv, "bin");
}
public static String getConfDirForHBaseVersion(String hv) {
return getDirForHBaseVersion(hv, "conf");
}
public static String getDirForHBaseVersion(String hv, String lastSubdir) {
Matcher m = HBASE_SUPPORTED_VERSION_PATTERN.matcher(hv);
if (m.matches()) {
String majorDotMinor = m.group(1);
if (majorDotMinor.startsWith(HBASE_VERSION_1_STRING)) {
// All HBase 1.x maps to 10
majorDotMinor = "1.0";
}
String result = String.format("%s%sjanusgraph-hbase-%s/%s/", HBASE_PARENT_DIR, File.separator, majorDotMinor.replace(".", ""), lastSubdir);
log.debug("Built {} path for HBase version {}: {}", lastSubdir, hv, result);
return result;
} else {
throw new RuntimeException("Unsupported HBase test version " + hv + " does not match pattern " + HBASE_SUPPORTED_VERSION_PATTERN);
}
}
public static ModifiableConfiguration getHBaseConfiguration() {
return getHBaseConfiguration("");
}
public static ModifiableConfiguration getHBaseConfiguration(String tableName) {
ModifiableConfiguration config = GraphDatabaseConfiguration.buildGraphConfiguration();
config.set(GraphDatabaseConfiguration.STORAGE_BACKEND, "hbase");
if (!StringUtils.isEmpty(tableName)) config.set(HBaseStoreManager.HBASE_TABLE,tableName);
config.set(GraphDatabaseConfiguration.TIMESTAMP_PROVIDER, HBaseStoreManager.PREFERRED_TIMESTAMPS);
config.set(SimpleBulkPlacementStrategy.CONCURRENT_PARTITIONS, 1);
return config;
}
public static WriteConfiguration getHBaseGraphConfiguration() {
return getHBaseConfiguration().getConfiguration();
}
/**
* Starts the HBase version described by {@link #HBASE_TARGET_VERSION}
*
* @return a status object describing a successfully-started HBase daemon
* @throws IOException
* passed-through
* @throws RuntimeException
* if starting HBase fails for any other reason
*/
public synchronized static HBaseStatus startHBase() throws IOException {
if (HBASE != null) {
log.info("HBase already started");
return HBASE;
}
killIfRunning();
deleteData();
log.info("Starting HBase");
String scriptPath = getScriptDirForHBaseVersion(HBASE_TARGET_VERSION) + "/hbase-daemon.sh";
runCommand(scriptPath, "--config", getConfDirForHBaseVersion(HBASE_TARGET_VERSION), "start", "master");
HBASE = HBaseStatus.write(HBASE_STAT_FILE, HBASE_TARGET_VERSION);
registerKillerHook(HBASE);
waitForConnection(60L, TimeUnit.SECONDS);
return HBASE;
}
/**
* Check whether {@link #HBASE_STAT_FILE} describes an HBase daemon. If so,
* kill it. Otherwise, do nothing.
*/
public synchronized static void killIfRunning() {
HBaseStatus stat = HBaseStatus.read(HBASE_STAT_FILE);
if (null == stat) {
log.info("HBase is not running");
return;
}
shutdownHBase(stat);
}
public synchronized static void waitForConnection(long timeout, TimeUnit timeoutUnit) {
long before = System.currentTimeMillis();
long after;
long timeoutMS = TimeUnit.MILLISECONDS.convert(timeout, timeoutUnit);
do {
try {
HConnection hc = HConnectionManager.createConnection(HBaseConfiguration.create());
hc.close();
after = System.currentTimeMillis();
log.info("HBase server to started after about {} ms", after - before);
return;
} catch (IOException e) {
log.info("Exception caught while waiting for the HBase server to start", e);
}
after = System.currentTimeMillis();
} while (timeoutMS > after - before);
after = System.currentTimeMillis();
log.warn("HBase server did not start in {} ms", after - before);
}
/**
* Delete HBase data under the current working directory.
*/
private synchronized static void deleteData() {
try {
// please keep in sync with HBASE_CONFIG_DIR/hbase-site.xml, reading HBase XML config is huge pain.
File hbaseRoot = new File("./target/hbase-root");
File zookeeperDataDir = new File("./target/zk-data");
if (hbaseRoot.exists()) {
log.info("Deleting {}", hbaseRoot);
FileUtils.deleteDirectory(hbaseRoot);
}
if (zookeeperDataDir.exists()) {
log.info("Deleting {}", zookeeperDataDir);
FileUtils.deleteDirectory(zookeeperDataDir);
}
} catch (IOException e) {
throw new RuntimeException("Failed to delete old HBase test data directories", e);
}
}
/**
* Register a shutdown hook with the JVM that attempts to kill the external
* HBase daemon
*
* @param stat
* the HBase daemon to kill
*/
private static void registerKillerHook(final HBaseStatus stat) {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
shutdownHBase(stat);
}
});
}
/**
* Runs the {@code hbase-daemon.sh stop master} script corresponding to the
* HBase version described by the parameter.
*
* @param stat
* the running HBase daemon to stop
*/
private synchronized static void shutdownHBase(HBaseStatus stat) {
log.info("Shutting down HBase...");
// First try graceful shutdown through the script...
runCommand(stat.getScriptDir() + "/hbase-daemon.sh", "--config", stat.getConfDir(), "stop", "master");
log.info("Shutdown HBase");
stat.getFile().delete();
log.info("Deleted {}", stat.getFile());
HBASE = null;
}
/**
* Run the parameter as an external process. Returns if the command starts
* without throwing an exception and returns exit status 0. Throws an
* exception if there's any problem invoking the command or if it does not
* return zero exit status.
*
* Blocks indefinitely while waiting for the command to complete.
*
* @param argv
* passed directly to {@link ProcessBuilder}'s constructor
*/
private static void runCommand(String... argv) {
final String cmd = Joiner.on(" ").join(argv);
log.info("Executing {}", cmd);
ProcessBuilder pb = new ProcessBuilder(argv);
pb.redirectErrorStream(true);
Process startup;
try {
startup = pb.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
StreamLogger sl = new StreamLogger(startup.getInputStream());
sl.setDaemon(true);
sl.start();
try {
int exitcode = startup.waitFor(); // wait for script to return
if (0 == exitcode) {
log.info("Command \"{}\" exited with status 0", cmd);
} else {
throw new RuntimeException("Command \"" + cmd + "\" exited with status " + exitcode);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
sl.join(1000L);
} catch (InterruptedException e) {
log.warn("Failed to cleanup stdin handler thread after running command \"{}\"", cmd, e);
}
}
/*
* This could be retired in favor of ProcessBuilder.Redirect when we move to
* source level 1.7.
*/
private static class StreamLogger extends Thread {
private final BufferedReader reader;
private static final Logger log =
LoggerFactory.getLogger(StreamLogger.class);
private StreamLogger(InputStream is) {
this.reader = new BufferedReader(new InputStreamReader(is));
}
@Override
public void run() {
String line;
try {
while (null != (line = reader.readLine())) {
log.info("> {}", line);
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.info("End of stream.");
} catch (IOException e) {
log.error("Unexpected IOException while reading stream {}", reader, e);
}
}
}
}