All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.test.ExternalNodeService Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.test;

import org.apache.lucene.util.Constants;
import org.apache.lucene.util.NamedThreadFactory;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.logging.support.LoggerMessageFormat;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.Delimiters;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.util.CharsetUtil;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

import static java.lang.Thread.sleep;
import static java.util.Collections.unmodifiableMap;

/**
 * Service that forks and manages Elasticsearch instances. This should be
 * started before any backwards compatibility tests. Its a separate service to
 * keep all forking out of the tests so they can continue to run under seccomp.
 *
 * The service takes single lines of the format "$command $arg $arg $arg\n". It
 * replies over to the command with any number of lines and signals that it is
 * done with the request by terminating the connection. Its up to the client to
 * interpret the reply lines as informational or status.
 */
public class ExternalNodeService {
    public static final int DEFAULT_PORT = 9871;
    public static final String SHUTDOWN_MESSAGE = "shutting down";

    static {
        // Stick this as early as possible to make sure the any Elasticsearch logs get the right prefix.
        System.setProperty("es.logger.prefix", "");
    }

    private static final ESLogger logger = ESLoggerFactory.getLogger("external-node-service");

    public static void main(String[] args) throws IOException, InterruptedException {
        int arg = 0;
        int port = Integer.parseInt(System.getProperty("ens.port", Integer.toString(DEFAULT_PORT)));
        if (args[arg].equals("shutdown")) {
            sendShutdown(port);
            return;
        }
        if (args[arg].equals("kill")) {
            killAllBackwardsNodes();
            return;
        }
        Path elasticsearchStable = getPath(args[arg++]);
        boolean block = true;
        if (arg < args.length) {
            block = !"noblock".equals(args[arg++]);
        }

        killAllBackwardsNodes();

        Map elasticsearches = new HashMap<>();
        logger.info("Scanning for elasticsearches...");
        try (DirectoryStream directoryStream = Files.newDirectoryStream(elasticsearchStable)) {
            for (Path subdir : directoryStream) {
                if (!Files.isDirectory(subdir)) {
                    continue;
                }
                String dirname = subdir.getFileName().toString();
                if (!dirname.startsWith("elasticsearch-")) {
                    continue;
                }
                String version = dirname.substring("elasticsearch-".length());
                logger.info("Found " + version);
                elasticsearches.put(version, subdir);
            }
        }
        if (elasticsearches.isEmpty()) {
            throw new IllegalArgumentException("Couldn't find any elasticsearch installations in " + elasticsearchStable.toAbsolutePath());
        }
        ExternalNodeService service = new ExternalNodeService(port, unmodifiableMap(elasticsearches));
        service.start();
        if (block) {
            /*
             * If run from the command line we want to block. If run from maven
             * we don't want to block. Instead we just let the main thread
             * terminate here so that maven continues with the next steps and
             * the daemon threads handle the connections.
             */
            try {
                while (true) {
                    sleep(1000);
                }
            } catch (InterruptedException e) {
                // This is just ctrl-c. Its cool.
            }
        }
    }

    @SuppressForbidden(reason = "we don't have an environment to read from")
    private static Path getPath(String location) {
        return PathUtils.get(location);
    }

    private static void sendShutdown(int port) {
        new ExternalNodeServiceClient(port).shutdownService();
    }

    /**
     * Running elasticsearch instances indexed by port.
     */
    private final ConcurrentMap runningElasticsearches = new ConcurrentHashMap<>();
    /**
     * Port on which the daemon should listen.
     */
    private final int port;
    /**
     * Map from version to root of the untarred distribution.
     */
    private final Map elasticsearchDistributions;
    /**
     * Is this service shutting down?
     */
    private final AtomicBoolean shuttingDown = new AtomicBoolean(false);

    private Thread shutdownHook;

    private ServerBootstrap server;

    public ExternalNodeService(int port, Map elasticsearches) throws IOException {
        this.port = port;
        this.elasticsearchDistributions = elasticsearches;
    }

    public void start() {
        shutdownHook = new Thread(new ShutdownHandler());
        Runtime.getRuntime().addShutdownHook(shutdownHook);
        ChannelFactory factory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(new DaemonizedThreadFactory("ens-boss")),
                Executors.newCachedThreadPool(new DaemonizedThreadFactory("ens-worker")));
        ServerBootstrap server = new ServerBootstrap(factory);
        server.setPipelineFactory(new ChannelPipelineFactory() {
            @Override
            public ChannelPipeline getPipeline() throws Exception {
                SimpleChannelHandler handler = new SimpleChannelHandler() {
                    AtomicBoolean sendingError = new AtomicBoolean(false);

                    @Override
                    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
                        String line = (String) e.getMessage();
                        logger.debug("Client sent [{}]", line);
                        Deque commandLine = new LinkedList<>();
                        Collections.addAll(commandLine, line.split(" "));
                        new Handler(e, commandLine).handle();
                        e.getChannel().close();
                    }

                    @Override
                    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
                        if (e.getChannel().isOpen()) {
                            if (sendingError.compareAndSet(false, true)) {
                                e.getChannel().write(ChannelBuffers.copiedBuffer(
                                        "Error processing request:  " + e.getCause().getMessage() + '\n', StandardCharsets.UTF_8));
                                sendingError.set(false);
                            }
                            e.getChannel().close();
                        }
                        logger.error("Error", e.getCause());
                    }
                };
                return Channels.pipeline(new DelimiterBasedFrameDecoder(80960, Delimiters.lineDelimiter()),
                        new StringDecoder(CharsetUtil.UTF_8), handler);
            }
        });

        server.setOption("child.tcpNoDelay", true);
        server.setOption("child.keepAlive", true);
        logger.debug("Binding localhost:" + port + "...");
        try {
            server.bind(new InetSocketAddress(InetAddress.getByName("localhost"), port));
        } catch (UnknownHostException e) {
            throw new RuntimeException("Couldn't find localhost!", e);
        }
        this.server = server;
        logger.info("Bound localhost:" + port);
    }

    /**
     * Called on startup to make sure that all backwards compatibility nodes
     * that might have been created by previous runs of this are dead, dead,
     * dead.
     */
    private static void killAllBackwardsNodes() throws IOException, InterruptedException {
        // This method is 1000% hacks and non-portable workarounds.
        List commandLine = new ArrayList<>();
        String bwcPathPart = "backwards";
        String esPattern = "bootstrap.Elasticsearch";
        if (Constants.WINDOWS) {
            commandLine.add("wmic");
            commandLine.add("process");
            commandLine.add("where");
            commandLine.add("Name like 'java%%.exe' and CommandLine like '%%" + bwcPathPart + "%%' and CommandLine like '%%"
                    + esPattern + "%%'");
            commandLine.add("get");
            commandLine.add("ProcessId");
        } else {
            commandLine.add("bash");
            commandLine.add("-c");
            commandLine.add("ps aux | grep java | grep -v grep | grep " + bwcPathPart + " | grep " + esPattern
                    + " | awk '{print $2}'");
        }
        ProcessBuilder builder = new ProcessBuilder(commandLine);
        builder.redirectErrorStream(true);
        Process process = null;
        BufferedReader stdout = null;
        List lines = new ArrayList<>();
        List pids = new ArrayList<>();
        try {
            process = builder.start();
            stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
            if (Constants.WINDOWS) {
                // In windows the first line is a heading or blank line
                lines.add(stdout.readLine());
            }
            String line;
            while ((line = stdout.readLine()) != null) {
                lines.add(line);
                if (line.isEmpty()) {
                    // Windows outputs a bunch of empty lines
                    continue;
                }
                pids.add(line.trim());
            }
            process.waitFor();
            logger.debug("Process list: [{}]", lines);
            if (process.exitValue() != 0) {
                logger.error("Getting pids of backwards nodes failed with output [{}]", lines);
                throw new RuntimeException("Getting pids of backwards nodes failed with exit code: [" + process.exitValue() + ']');
            }
        } finally {
            if (stdout != null) {
                stdout.close();
            }
            if (process != null) {
                process.destroy();
            }
        }
        if (pids.isEmpty() == false) {
            logger.info("Killing backwards nodes running at [{}]", pids);
        }
        for (String pid : pids) {
            new ProcessInfo(null, pid).stop();
        }
    }

    private class Handler {
        private final MessageEvent e;
        private final Deque commandLine;

        public Handler(MessageEvent e, Deque commandLine) {
            this.e = e;
            this.commandLine = commandLine;
        }

        private void handle() throws IOException {
            String command = commandLine.pop();
            switch (command) {
            case "start":
                start(commandLine);
                return;
            case "stop":
                stop(commandLine);
                return;
            case "shutdown":
                message(SHUTDOWN_MESSAGE);
                // Can't run shutdown directly because netty complains that it could cause a deadlock.
                shutdownHook.start();
                return;
            default:
                message("unknown command: " + command);
                return;
            }
        }

        private void start(Deque commandLine) throws IOException {
            if (commandLine.isEmpty()) {
                message("No version sent!");
                return;
            }
            String version = commandLine.pop();
            Path versionRoot = elasticsearchDistributions.get(version);
            if (versionRoot == null) {
                message("Version not found: " + version);
                return;
            }

            String pidFile = getPidFile(commandLine);

            message("starting elasticsearch " + version + "...");
            List command = buildStartCommand(versionRoot.toAbsolutePath().normalize(), commandLine);
            StringBuilder startReproduction = new StringBuilder();
            boolean first = true;
            for (String c : command) {
                if (first) {
                    first = false;
                } else {
                    startReproduction.append(' ');
                }
                startReproduction.append(c);
            }
            logger.debug("Starting elasticsearch with {}", startReproduction);
            ProcessBuilder builder = new ProcessBuilder(command);
            builder.environment().put("ES_HEAP_SIZE", "256m");
            builder.inheritIO();
            Process process = null;
            String pid = null;
            try {
                // we have to delete the pid file which might be leftover from previous tests in the same suite
                // otherwise the ExternalNodeService reads the old pid file and later tries to stop the wrong process
                Files.deleteIfExists(getPath(pidFile));
                process = builder.start();
                pid = getPid(pidFile);
                message("process forked");
                runningElasticsearches.put(pid, new ProcessInfo(process, pid));
                message("pid [" + pid + "]");
                message("started");
            } finally {
                if (pid == null) {
                    logger.error("It looks like we failed to launch elasticsearch.");
                    logger.error("We tried to start it like this: {}", startReproduction);
                    if (process != null) {
                        // In Java 1.8 this should be destroyForcibly
                        process.destroy();
                    }
                }
            }
        }

        private String getPid(String pidFile) throws IOException {
            Path pidPath = getPath(pidFile);
            long maxTimeInMillis = 10000;
            long startTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - startTime < maxTimeInMillis) {
                if (Files.exists(pidPath)) {
                    BufferedReader reader = Files.newBufferedReader(pidPath, StandardCharsets.UTF_8);
                    String pid = reader.readLine();
                    reader.close();
                    if (pid != null) {
                        return pid;
                    }
                }
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    logger.info("Interrupted while waiting for pid file of external node.", e);
                    break;
                }
            }
            throw new ElasticsearchException("Could not start external node, pid was not found.");
        }

        public String getPidFile(Deque commandLine) {
            for (String param: commandLine) {
                if (param.contains("pidfile")) {
                    return param.substring("-Des.pidfile=".length(), param.length());
                }
            }
            throw new ElasticsearchException("Cannot start external node. pidfile parameter missing.");
        }

        private void stop(Deque commandLine) {
            if (commandLine.isEmpty()) {
                message("no pid sent!");
                return;
            }
            String pid = commandLine.pop();
            ProcessInfo elasticsearch = runningElasticsearches.remove(pid);
            if (elasticsearch == null) {
                message("couldn't find elasticsearch with pid " + pid + "!");
                return;
            }
            message("killing elasticsearch with pid " + pid);
            try {
                elasticsearch.stop();
            } catch (InterruptedException e) {
                message("timed out waiting for elasticsearch to stop!");
                runningElasticsearches.put(pid, elasticsearch);
                return;
            } catch (IOException e) {
                message("error waiting for elasticsearch to stop: [" + e.getMessage() + "]");
                logger.warn("Error waiting for elasticsearch to stop", e);
                runningElasticsearches.put(pid, elasticsearch);
                return;
            }
            message("killed");
        }

        private void message(String message) {
            logger.debug("Sending [{}] to client", message);
            e.getChannel().write(ChannelBuffers.copiedBuffer(message + '\n', StandardCharsets.UTF_8));
        }

        private List buildStartCommand(Path versionRoot, Deque commandLine) {
            String executable = Constants.WINDOWS ? "elasticsearch.bat" : "elasticsearch";
            List command = new ArrayList<>();
            command.add(versionRoot.resolve("bin").resolve(executable).toString());
            while (!commandLine.isEmpty()) {
                String arg = commandLine.pop();
                arg = arg.replace("${PATH}", versionRoot.toString());
                command.add(arg);
            }
            // We need at least INFO in http and node
            command.add("-Des.logger.transport=INFO");
            command.add("-Des.logger.node=INFO");
            // It'd be nice to conditionally apply these but that can wait.
            return command;
        }
    }

    /**
     * Catches shutdown and shuts down
     */
    private class ShutdownHandler implements Runnable {
        @Override
        public void run() {
            if (!shuttingDown.compareAndSet(false, true)) {
                logger.debug("Shutting down twice. Weird but ok.");
                return;
            }
            logger.info("Shutting down");
            if (shutdownHook != null) {
                // The hook may be null if we never started
                try {
                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
                } catch (IllegalStateException e) {
                    // Its cool - this is caused by trying to remove the hook
                    // during shutdown
                }
            }
            for (Map.Entry elasticsearch : runningElasticsearches.entrySet()) {
                logger.debug("Kill -9ing elasticsearch running at localhost:{}", elasticsearch.getKey());
                // In Java 1.8 this should be destroyForcibly.
                try {
                    elasticsearch.getValue().stop();
                } catch (IOException | InterruptedException e) {
                    logger.error("Error stopping elasticsearch", e);
                }
            }
            if (server != null) {
                logger.debug("Shutting down server");
                server.shutdown();
            }
        }
    }

    /**
     * Daemonized ThreadFactory used so this won't block in maven.
     */
    private static class DaemonizedThreadFactory extends NamedThreadFactory {
        public DaemonizedThreadFactory(String threadNamePrefix) {
            super(threadNamePrefix);
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = super.newThread(r);
            t.setDaemon(true);
            return t;
        }
    }

    private static class ProcessInfo {
        private final Process process;
        private final String pid;

        public ProcessInfo(@Nullable Process process, String pid) {
            this.process = process;
            this.pid = pid;
        }

        public void stop() throws IOException, InterruptedException {
            /*
             * process.destroy doesn't work properly on windows and sometimes we
             * want to kill by pid so we just go with the super aggressive
             * option every time.....
             */
            List commandLine = new ArrayList<>();
            if (Constants.WINDOWS) {
                commandLine.add("taskkill");
                commandLine.add("/F"); // Force
                commandLine.add("/PID");
                commandLine.add(pid);
            } else {
                commandLine.add("kill");
                commandLine.add("-9"); // Force
                commandLine.add(pid);
            }
            ProcessBuilder builder = new ProcessBuilder(commandLine);
            Process killProcess = null;
            try {
                logger.debug("Killing [{}]", pid);
                killProcess = builder.start();
                killProcess.waitFor();
                if (killProcess.exitValue() != 0) {
                    throw new RuntimeException(
                            LoggerMessageFormat.format("Killing [{}] failed with exit code [{}]", pid, killProcess.exitValue()));
                }
            } finally {
                if (killProcess != null) {
                    killProcess.destroy();
                }
            }
            if (process != null) {
                process.waitFor();
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy