/* * 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.admin.AdminServerApplication; import org.springframework.xd.dirt.server.container.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()); } }