Java tutorial
/** * 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. See accompanying LICENSE file. */ package org.kududb.client; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.net.HostAndPort; import org.apache.commons.io.FileUtils; import org.kududb.util.NetUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Utility class to start and manipulate Kudu clusters. Relies on being IN the Kudu source code with * both the kudu-master and kudu-tserver binaries already compiled. {@link BaseKuduTest} should be * extended instead of directly using this class in almost all cases. */ public class MiniKuduCluster implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(MiniKuduCluster.class); // TS and Master ports will be assigned starting with this one. private static final int PORT_START = 64030; // List of threads that print private final List<Thread> PROCESS_INPUT_PRINTERS = new ArrayList<>(); // Map of ports to master servers. private final Map<Integer, Process> masterProcesses = new ConcurrentHashMap<>(); // Map of ports to tablet servers. private final Map<Integer, Process> tserverProcesses = new ConcurrentHashMap<>(); private final List<String> pathsToDelete = new ArrayList<>(); private final List<HostAndPort> masterHostPorts = new ArrayList<>(); private String masterAddresses; private MiniKuduCluster(int numMasters, int numTservers) throws Exception { startCluster(numMasters, numTservers); } /** * Starts a Kudu cluster composed of the provided masters and tablet servers. * @param numMasters how many masters to start * @param numTservers how many tablet servers to start * @throws Exception */ private void startCluster(int numMasters, int numTservers) throws Exception { Preconditions.checkArgument(numMasters > 0, "Need at least one master"); Preconditions.checkArgument(numTservers > 0, "Need at least one tablet server"); // The following props are set via kudu-client's pom. String baseDirPath = TestUtils.getBaseDir(); long now = System.currentTimeMillis(); LOG.info("Starting {} masters...", numMasters); int port = startMasters(PORT_START, numMasters, baseDirPath); LOG.info("Starting {} tablet servers...", numTservers); for (int i = 0; i < numTservers; i++) { port = TestUtils.findFreePort(port); String dataDirPath = baseDirPath + "/ts-" + i + "-" + now; String flagsPath = TestUtils.getFlagsPath(); String[] tsCmdLine = { TestUtils.findBinary("kudu-tserver"), "--flagfile=" + flagsPath, "--fs_wal_dir=" + dataDirPath, "--fs_data_dirs=" + dataDirPath, "--tserver_master_addrs=" + masterAddresses, "--rpc_bind_addresses=127.0.0.1:" + port }; tserverProcesses.put(port, configureAndStartProcess(tsCmdLine)); port++; if (flagsPath.startsWith(baseDirPath)) { // We made a temporary copy of the flags; delete them later. pathsToDelete.add(flagsPath); } pathsToDelete.add(dataDirPath); } } /** * Start the specified number of master servers with ports starting from a specified * number. Finds free web and RPC ports up front for all of the masters first, then * starts them on those ports, populating 'masters' map. * @param masterStartPort the starting point of the port range for the masters * @param numMasters number of masters to start * @param baseDirPath the base directory where the mini cluster stores its data * @return the next free port * @throws Exception if we are unable to start the masters */ private int startMasters(int masterStartPort, int numMasters, String baseDirPath) throws Exception { LOG.info("Starting {} masters...", numMasters); // Get the list of web and RPC ports to use for the master consensus configuration: // request NUM_MASTERS * 2 free ports as we want to also reserve the web // ports for the consensus configuration. List<Integer> ports = TestUtils.findFreePorts(masterStartPort, numMasters * 2); int lastFreePort = ports.get(ports.size() - 1); List<Integer> masterRpcPorts = Lists.newArrayListWithCapacity(numMasters); List<Integer> masterWebPorts = Lists.newArrayListWithCapacity(numMasters); for (int i = 0; i < numMasters * 2; i++) { if (i % 2 == 0) { masterRpcPorts.add(ports.get(i)); masterHostPorts.add(HostAndPort.fromParts("127.0.0.1", ports.get(i))); } else { masterWebPorts.add(ports.get(i)); } } masterAddresses = NetUtil.hostsAndPortsToString(masterHostPorts); for (int i = 0; i < numMasters; i++) { long now = System.currentTimeMillis(); String dataDirPath = baseDirPath + "/master-" + i + "-" + now; String flagsPath = TestUtils.getFlagsPath(); // The web port must be reserved in the call to findFreePorts above and specified // to avoid the scenario where: // 1) findFreePorts finds RPC ports a, b, c for the 3 masters. // 2) start master 1 with RPC port and let it bind to any (specified as 0) web port. // 3) master 1 happens to bind to port b for the web port, as master 2 hasn't been // started yet and findFreePort(s) is "check-time-of-use" (it does not reserve the // ports, only checks that when it was last called, these ports could be used). List<String> masterCmdLine = Lists.newArrayList(TestUtils.findBinary("kudu-master"), "--flagfile=" + flagsPath, "--fs_wal_dir=" + dataDirPath, "--fs_data_dirs=" + dataDirPath, "--rpc_bind_addresses=127.0.0.1:" + masterRpcPorts.get(i), "--webserver_port=" + masterWebPorts.get(i)); if (numMasters > 1) { masterCmdLine.add("--master_addresses=" + masterAddresses); } masterProcesses.put(masterRpcPorts.get(i), configureAndStartProcess(masterCmdLine.toArray(new String[masterCmdLine.size()]))); if (flagsPath.startsWith(baseDirPath)) { // We made a temporary copy of the flags; delete them later. pathsToDelete.add(flagsPath); } pathsToDelete.add(dataDirPath); } return lastFreePort + 1; } /** * Starts a process using the provided command and configures it to be daemon, * redirects the stderr to stdout, and starts a thread that will read from the process' input * stream and redirect that to LOG. * @param command Process and options * @return The started process * @throws Exception Exception if an error prevents us from starting the process, * or if we were able to start the process but noticed that it was then killed (in which case * we'll log the exit value). */ private Process configureAndStartProcess(String[] command) throws Exception { LOG.info("Starting process: {}", Joiner.on(" ").join(command)); ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.redirectErrorStream(true); Process proc = processBuilder.start(); ProcessInputStreamLogPrinterRunnable printer = new ProcessInputStreamLogPrinterRunnable( proc.getInputStream()); Thread thread = new Thread(printer); thread.setDaemon(true); thread.setName(command[0]); PROCESS_INPUT_PRINTERS.add(thread); thread.start(); Thread.sleep(300); try { int ev = proc.exitValue(); throw new Exception( "We tried starting a process (" + command[0] + ") but it exited with " + "value=" + ev); } catch (IllegalThreadStateException ex) { // This means the process is still alive, it's like reverse psychology. } return proc; } /** * Kills the TS listening on the provided port. Doesn't do anything if the TS was already killed. * @param port port on which the tablet server is listening on * @throws InterruptedException */ public void killTabletServerOnPort(int port) throws InterruptedException { Process ts = tserverProcesses.remove(port); if (ts == null) { // The TS is already dead, good. return; } LOG.info("Killing server at port " + port); ts.destroy(); ts.waitFor(); } /** * Kills the master listening on the provided port. Doesn't do anything if the master was * already killed. * @param port port on which the master is listening on * @throws InterruptedException */ public void killMasterOnPort(int port) throws InterruptedException { Process master = masterProcesses.remove(port); if (master == null) { // The master is already dead, good. return; } LOG.info("Killing master at port " + port); master.destroy(); master.waitFor(); } /** * See {@link #shutdown()}. * @throws Exception never thrown, exceptions are logged */ @Override public void close() throws Exception { shutdown(); } /** * Stops all the processes and deletes the folders used to store data and the flagfile. */ public void shutdown() { for (Iterator<Process> masterIter = masterProcesses.values().iterator(); masterIter.hasNext();) { masterIter.next().destroy(); masterIter.remove(); } for (Iterator<Process> tsIter = tserverProcesses.values().iterator(); tsIter.hasNext();) { tsIter.next().destroy(); tsIter.remove(); } for (Thread thread : PROCESS_INPUT_PRINTERS) { thread.interrupt(); } for (String path : pathsToDelete) { try { File f = new File(path); if (f.isDirectory()) { FileUtils.deleteDirectory(f); } else { f.delete(); } } catch (Exception e) { LOG.warn("Could not delete path {}", path, e); } } } /** * Returns the comma-separated list of master addresses. * @return master addresses */ public String getMasterAddresses() { return masterAddresses; } /** * Returns a list of master addresses. * @return master addresses */ public List<HostAndPort> getMasterHostPorts() { return masterHostPorts; } /** * Helper runnable that can log what the processes are sending on their stdout and stderr that * we'd otherwise miss. */ static class ProcessInputStreamLogPrinterRunnable implements Runnable { private final InputStream is; public ProcessInputStreamLogPrinterRunnable(InputStream is) { this.is = is; } @Override public void run() { try { String line; BufferedReader in = new BufferedReader(new InputStreamReader(is)); while ((line = in.readLine()) != null) { LOG.info(line); } in.close(); } catch (Exception e) { if (!e.getMessage().contains("Stream closed")) { LOG.error("Caught error while reading a process' output", e); } } } } public static class MiniKuduClusterBuilder { private int numMasters = 1; private int numTservers = 3; public MiniKuduClusterBuilder numMasters(int numMasters) { this.numMasters = numMasters; return this; } public MiniKuduClusterBuilder numTservers(int numTservers) { this.numTservers = numTservers; return this; } public MiniKuduCluster build() throws Exception { return new MiniKuduCluster(numMasters, numTservers); } } }