org.springframework.xd.distributed.util.ServerProcessUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.xd.distributed.util.ServerProcessUtils.java

Source

/*
 * Copyright 2014 the original author or 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.springframework.xd.distributed.util;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.curator.test.TestingServer;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.hateoas.PagedResources;
import org.springframework.util.Assert;
import org.springframework.xd.batch.hsqldb.server.HsqlServerApplication;
import org.springframework.xd.dirt.server.AdminServerApplication;
import org.springframework.xd.dirt.server.ContainerServerApplication;
import org.springframework.xd.rest.client.impl.SpringXDTemplate;
import org.springframework.xd.rest.domain.DetailedContainerResource;

import com.oracle.tools.runtime.PropertiesBuilder;
import com.oracle.tools.runtime.console.SystemApplicationConsole;
import com.oracle.tools.runtime.java.JavaApplication;
import com.oracle.tools.runtime.java.NativeJavaApplicationBuilder;
import com.oracle.tools.runtime.java.SimpleJavaApplication;
import com.oracle.tools.runtime.java.SimpleJavaApplicationSchema;

/**
 * Collection of utilities for starting external processes required
 * for a distributed XD system.
 *
 * @author Patrick Peralta
 */
public class ServerProcessUtils {

    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ServerProcessUtils.class);

    /**
     * Start an instance of ZooKeeper. Upon test completion, the method
     * {@link org.apache.curator.test.TestingServer#stop()} should be invoked
     * to shut down the server.
     *
     * @param port ZooKeeper port
     * @return ZooKeeper testing server
     * @throws Exception
     */
    public static TestingServer startZooKeeper(int port) throws Exception {
        return new TestingServer(port);
    }

    /**
     * Start an instance of the admin server. This method will block until
     * the admin server is capable of processing HTTP requests. Upon test
     * completion, the method {@link com.oracle.tools.runtime.java.JavaApplication#close()}
     * should be invoked to shut down the server.
     *
     * @param properties system properties to pass to the container; at minimum
     *                   must contain key {@code zk.client.connect} to indicate
     *                   the ZooKeeper connect string and key {@code server.port}
     *                   to indicate the admin server port
     *
     * @return admin server application reference
     * @throws IOException            if an exception is thrown launching the process
     * @throws InterruptedException   if the executing thread is interrupted
     */
    public static JavaApplication<SimpleJavaApplication> startAdmin(Properties properties)
            throws IOException, InterruptedException {
        Assert.state(properties.containsKey("zk.client.connect"), "Property 'zk.client.connect' required");
        Assert.state(properties.containsKey("server.port"), "Property 'server.port' required");

        JavaApplication<SimpleJavaApplication> adminServer = launch(AdminServerApplication.class, false, properties,
                null);
        logger.debug("waiting for admin server");
        waitForAdminServer("http://localhost:" + properties.getProperty("server.port"));
        logger.debug("admin server ready");

        return adminServer;
    }

    /**
     * Start a container instance.  Upon test completion, the method
     * {@link com.oracle.tools.runtime.java.JavaApplication#close()}
     * should be invoked to shut down the server. This method may also be
     * invoked as part of failover testing.
     * <p />
     * Note that this method returns immediately. In order to verify
     * that the container was started, invoke {@link #waitForContainers}
     * to block until the container(s) are started.
     *
     * @param properties system properties to pass to the admin server; at minimum
     *                   must contain key {@code zk.client.connect} to indicate
     *                   the ZooKeeper connect string
     *
     * @return container server application reference
     * @throws IOException if an exception is thrown launching the process
     *
     * @see #waitForContainers
     */
    public static JavaApplication<SimpleJavaApplication> startContainer(Properties properties) throws IOException {
        Assert.state(properties.containsKey("zk.client.connect"), "Property 'zk.client.connect' required");

        return launch(ContainerServerApplication.class, false, properties, null);
    }

    /**
     * Start an instance of HSQL. Upon test completion, the method
     * {@link com.oracle.tools.runtime.java.JavaApplication#close()}
     * should be invoked to shut down the server.
     *
     * @param systemProperties system properties for new process
     * @return HSQL server application reference
     * @throws IOException if an exception is thrown launching the process
     */
    public static JavaApplication<SimpleJavaApplication> startHsql(Properties systemProperties) throws IOException {
        return launch(HsqlServerApplication.class, false, systemProperties, null);
    }

    /**
     * Block the executing thread until all of the indicated process IDs
     * have been identified in the list of runtime containers as indicated
     * by the admin server. If an empty list is provided, the executing
     * thread will block until there are no containers running.
     *
     * @param template REST template used to communicate with the admin server
     * @param pids     set of process IDs for the expected containers
     * @return map of process id to container id
     * @throws InterruptedException            if the executing thread is interrupted
     * @throws java.lang.IllegalStateException if the number of containers identified
     *         does not match the number of PIDs provided
     */
    public static Map<Long, String> waitForContainers(SpringXDTemplate template, Set<Long> pids)
            throws InterruptedException, IllegalStateException {
        int pidCount = pids.size();
        Map<Long, String> mapPidUuid = getRunningContainers(template);
        long expiry = System.currentTimeMillis() + 3 * 60000;

        while (mapPidUuid.size() != pidCount && System.currentTimeMillis() < expiry) {
            Thread.sleep(500);
            mapPidUuid = getRunningContainers(template);
        }

        if (mapPidUuid.size() == pidCount && mapPidUuid.keySet().containsAll(pids)) {
            return mapPidUuid;
        }

        Set<Long> missingPids = new HashSet<Long>(pids);
        missingPids.removeAll(mapPidUuid.keySet());

        Set<Long> unexpectedPids = new HashSet<Long>(mapPidUuid.keySet());
        unexpectedPids.removeAll(pids);

        StringBuilder builder = new StringBuilder();
        if (!missingPids.isEmpty()) {
            builder.append("Admin server did not find the following container PIDs:").append(missingPids);
        }
        if (!unexpectedPids.isEmpty()) {
            if (builder.length() > 0) {
                builder.append("; ");
            }
            builder.append("Admin server found the following unexpected container PIDs:").append(unexpectedPids);
        }

        throw new IllegalStateException(builder.toString());
    }

    /**
     * Return a map of running containers. Map key is pid; value is
     * the container ID.
     *
     * @param template REST template used to communicate with the admin server
     * @return map of process id to container id
     */
    public static Map<Long, String> getRunningContainers(SpringXDTemplate template) {
        Map<Long, String> mapPidUuid = new HashMap<Long, String>();
        PagedResources<DetailedContainerResource> containers = template.runtimeOperations().listContainers();
        for (DetailedContainerResource container : containers) {
            logger.trace("Container: {}", container);
            long pid = Long.parseLong(container.getAttribute("pid"));
            mapPidUuid.put(pid, container.getAttribute("id"));
        }

        return mapPidUuid;
    }

    /**
     * Block the executing thread until the admin server is responding to
     * HTTP requests.
     *
     * @param url URL for admin server
     * @throws InterruptedException            if the executing thread is interrupted
     * @throws java.lang.IllegalStateException if a successful connection to the
     *         admin server was not established
     */
    private static void waitForAdminServer(String url) throws InterruptedException, IllegalStateException {
        boolean connected = false;
        Exception exception = null;
        int httpStatus = 0;
        long expiry = System.currentTimeMillis() + 30000;
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();

        try {
            do {
                try {
                    Thread.sleep(100);

                    HttpGet httpGet = new HttpGet(url);
                    httpStatus = httpClient.execute(httpGet).getStatusLine().getStatusCode();
                    if (httpStatus == HttpStatus.SC_OK) {
                        connected = true;
                    }
                } catch (IOException e) {
                    exception = e;
                }
            } while ((!connected) && System.currentTimeMillis() < expiry);
        } finally {
            try {
                httpClient.close();
            } catch (IOException e) {
                // ignore exception on close
            }
        }

        if (!connected) {
            StringBuilder builder = new StringBuilder();
            builder.append("Failed to connect to '").append(url).append("'");
            if (httpStatus > 0) {
                builder.append("; last HTTP status: ").append(httpStatus);
            }
            if (exception != null) {
                StringWriter writer = new StringWriter();
                exception.printStackTrace(new PrintWriter(writer));
                builder.append("; exception: ").append(exception.toString()).append(", ").append(writer.toString());
            }
            throw new IllegalStateException(builder.toString());
        }
    }

    /**
     * Launch the given class's {@code main} method in a separate JVM.
     *
     * @param clz               class to launch
     * @param remoteDebug       if true, enable remote debugging
     * @param systemProperties  system properties for new process
     * @param args              command line arguments
     * @return launched application
     *
     * @throws IOException if an exception was thrown launching the process
     */
    private static JavaApplication<SimpleJavaApplication> launch(Class<?> clz, boolean remoteDebug,
            Properties systemProperties, List<String> args) throws IOException {
        String classpath = System.getProperty("java.class.path");

        logger.info("Launching {}", clz);
        logger.info("   args: {}", args);
        logger.info("   properties: {}", systemProperties);
        logger.info("   classpath: {}", classpath);

        SimpleJavaApplicationSchema schema = new SimpleJavaApplicationSchema(clz.getName(), classpath);
        if (args != null) {
            for (String arg : args) {
                schema.addArgument(arg);
            }
        }
        if (systemProperties != null) {
            schema.setSystemProperties(new PropertiesBuilder(systemProperties));
        }

        NativeJavaApplicationBuilder<SimpleJavaApplication, SimpleJavaApplicationSchema> builder = new NativeJavaApplicationBuilder<SimpleJavaApplication, SimpleJavaApplicationSchema>();
        builder.setRemoteDebuggingEnabled(remoteDebug);
        return builder.realize(schema, clz.getName(), new SystemApplicationConsole());
    }

}