
org.cinchapi.concourse.server.ManagedConcourseServer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of concourse-test Show documentation
Show all versions of concourse-test Show documentation
A framework for writing end-to-end integration tests using the Concourse client and server
The newest version!
/*
* Copyright (c) 2015 Cinchapi, Inc.
*
* 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.cinchapi.concourse.server;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import jline.TerminalFactory;
import org.cinchapi.concourse.Concourse;
import org.cinchapi.concourse.Timestamp;
import org.cinchapi.concourse.config.ConcoursePreferences;
import org.cinchapi.concourse.lang.Criteria;
import org.cinchapi.concourse.thrift.Operator;
import org.cinchapi.concourse.time.Time;
import org.cinchapi.concourse.util.ConcourseServerDownloader;
import org.cinchapi.concourse.util.Processes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* A {@link ManagedConcourseServer} is an external server process that can be
* programmatically controlled within another application. This class is useful
* for applications that want to "embed" a Concourse Server for the duration of
* the application's life cycle and then forget about its existence afterwards.
*
* @author jnelson
*/
public class ManagedConcourseServer {
/**
* Give the path to a local concourse {@code repo}, build an installer and
* return the path to the installer.
*
* @param repo
* @return the installer path
*/
public static String buildInstallerFromRepo(String repo) {
try {
Process p;
p = new ProcessBuilder("bash", "gradlew", "clean", "installer")
.directory(new File(repo)).start();
Processes.waitForSuccessfulCompletion(p);
p = new ProcessBuilder("ls", repo
+ "/concourse-server/build/distributions").start();
Processes.waitForSuccessfulCompletion(p);
String installer = Processes.getStdOut(p).get(0);
installer = repo + "/concourse-server/build/distributions/"
+ installer;
return installer;
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Return an {@link ManagedConcourseServer} that controls an instance
* located in the {@code installDirectory}.
*
* @param installDirectory
* @return the ManagedConcourseServer
*/
public static ManagedConcourseServer manageExistingServer(
String installDirectory) {
return new ManagedConcourseServer(installDirectory);
}
/**
* Create an {@link ManagedConcourseServer from the {@code installer}.
*
* @param installer
* @return the ManagedConcourseServer
*/
public static ManagedConcourseServer manageNewServer(File installer) {
return manageNewServer(installer, DEFAULT_INSTALL_HOME + File.separator
+ Time.now());
}
/**
* Create an {@link ManagedConcourseServer} from the {@code installer} in
* {@code directory}.
*
* @param installer
* @param directory
* @return the EmbeddedConcourseServer
*/
public static ManagedConcourseServer manageNewServer(File installer,
String directory) {
return new ManagedConcourseServer(install(installer.getAbsolutePath(),
directory));
}
/**
* Create an {@link ManagedConcourseServer} at {@code version}.
*
* @param version
* @return the ManagedConcourseServer
*/
public static ManagedConcourseServer manageNewServer(String version) {
return manageNewServer(version, DEFAULT_INSTALL_HOME + File.separator
+ Time.now());
}
/**
* Create an {@link ManagedConcourseServer} at {@code version} in
* {@code directory}.
*
* @param version
* @param directory
* @return the ManagedConcourseServer
*/
public static ManagedConcourseServer manageNewServer(String version,
String directory) {
return manageNewServer(
new File(ConcourseServerDownloader.download(version)),
directory);
}
/**
* Tweak some of the preferences to make this more palatable for testing
* (i.e. reduce the possibility of port conflicts, etc).
*
* @param installDirectory
*/
private static void configure(String installDirectory) {
ConcoursePreferences prefs = ConcoursePreferences.load(installDirectory
+ File.separator + CONF + File.separator + "concourse.prefs");
String data = installDirectory + File.separator + "data";
prefs.setBufferDirectory(data + File.separator + "buffer");
prefs.setDatabaseDirectory(data + File.separator + "database");
prefs.setClientPort(getOpenPort());
prefs.setJmxPort(getOpenPort());
prefs.setLogLevel(Level.DEBUG);
prefs.setShutdownPort(getOpenPort());
}
/**
* Collect and return all the {@code jar} files that are located in the
* directory at {@code path}. If {@code path} is not a directory, but is
* instead, itself, a jar file, then return a list that contains in.
*
* @param path
* @return the list of jar file URL paths
*/
private static URL[] gatherJars(String path) {
List jars = Lists.newArrayList();
gatherJars(path, jars);
return jars.toArray(new URL[] {});
}
/**
* Collect all the {@code jar} files that are located in the directory at
* {@code path} and place them into the list of {@code jars}. If
* {@code path} is not a directory, but is instead, itself a jar file, then
* place it in the list.
*
* @param path
* @param jars
*/
private static void gatherJars(String path, List jars) {
try {
if(Files.isDirectory(Paths.get(path))) {
for (Path p : Files.newDirectoryStream(Paths.get(path))) {
gatherJars(p.toString(), jars);
}
}
else if(path.endsWith(".jar")) {
jars.add(new URL("file://" + path.toString()));
}
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Get an open port.
*
* @return the port
*/
private static int getOpenPort() {
int min = 49512;
int max = 65535;
int port = RAND.nextInt(min) + (max - min);
return isPortAvailable(port) ? port : getOpenPort();
}
/**
* Install a Concourse Server in {@code directory} using {@code installer}.
*
* @param installer
* @param directory
* @return the server install directory
*/
private static String install(String installer, String directory) {
try {
Files.createDirectories(Paths.get(directory));
Path binary = Paths.get(directory + File.separator
+ TARGET_BINARY_NAME);
Files.deleteIfExists(binary);
Files.copy(Paths.get(installer), binary);
ProcessBuilder builder = new ProcessBuilder(Lists.newArrayList(
"sh", binary.toString()));
builder.directory(new File(directory));
builder.redirectErrorStream();
Process process = builder.start();
// The concourse-server installer prompts for an admin password in
// order to make optional system wide In order to get around this
// prompt, we have to "kill" the process, otherwise the server
// install will hang.
Stopwatch watch = Stopwatch.createStarted();
while (watch.elapsed(TimeUnit.SECONDS) < 1) {
continue;
}
watch.stop();
process.destroy();
TerminalFactory.get().restore();
String application = directory + File.separator
+ "concourse-server"; // the install directory for the
// concourse-server application
process = Runtime.getRuntime().exec("ls " + application);
List output = Processes.getStdOut(process);
if(!output.isEmpty()) {
configure(application);
log.info("Successfully installed server in {}", application);
return application;
}
else {
throw new RuntimeException(
MessageFormat
.format("Unsuccesful attempt to "
+ "install server at {0} "
+ "using binary from {1}", directory,
installer));
}
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Return {@code true} if the {@code port} is available on the local
* machine.
*
* @param port
* @return {@code true} if the port is available
*/
private static boolean isPortAvailable(int port) {
try {
new ServerSocket(port).close();
return true;
}
catch (SocketException e) {
return false;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
private static final String BIN = "bin";
// ---relative paths
private static final String CONF = "conf";
/**
* The default location where the the test server is installed if a
* particular location is not specified.
*/
private static final String DEFAULT_INSTALL_HOME = System
.getProperty("user.home") + File.separator + ".concourse-testing";
// ---logger
private static final Logger log = LoggerFactory
.getLogger(ManagedConcourseServer.class);
// ---random
private static final Random RAND = new Random();
/**
* The filename of the binary installer from which the test server will be
* created.
*/
private static final String TARGET_BINARY_NAME = "concourse-server.bin";
/**
* The server application install directory;
*/
private final String installDirectory;
/**
* A connection to the remote MBean server running in the managed
* concourse-server process.
*/
private MBeanServerConnection mBeanServerConnection = null;
/**
* The handler for the server's preferences.
*/
private final ConcoursePreferences prefs;
/**
* Construct a new instance.
*
* @param installDirectory
*/
private ManagedConcourseServer(String installDirectory) {
this.installDirectory = installDirectory;
this.prefs = ConcoursePreferences.load(installDirectory
+ File.separator + CONF + File.separator + "concourse.prefs");
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
destroy();
}
}));
}
/**
* Return a connection handler to the server using the default "admin"
* credentials.
*
* @return the connection handler
*/
public Concourse connect() {
return connect("admin", "admin");
}
/**
* Return a connection handler to the server using the specified
* {@code username} and {@code password}.
*
* @param username
* @param password
* @return the connection handler
*/
public Concourse connect(String username, String password) {
return new Client(username, password);
}
/**
* Stop the server, if it is running, and permanently delete the application
* files and associated data.
*/
public void destroy() {
if(Files.exists(Paths.get(installDirectory))) { // check if server has
// been manually
// destroyed
if(isRunning()) {
stop();
}
try {
deleteDirectory(Paths.get(installDirectory).getParent()
.toString());
log.info("Deleted server install directory at {}",
installDirectory);
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
}
/**
* Return the client port for this server.
*
* @return the client port
*/
public int getClientPort() {
return prefs.getClientPort();
}
/**
* Get a collection of stats about the heap memory usage for the managed
* concourse-server process.
*
* @return the heap memory usage
*/
public MemoryUsage getHeapMemoryStats() {
try {
MemoryMXBean memory = ManagementFactory.newPlatformMXBeanProxy(
getMBeanServerConnection(),
ManagementFactory.MEMORY_MXBEAN_NAME, MemoryMXBean.class);
return memory.getHeapMemoryUsage();
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Return the {@link #installDirectory} for this server.
*
* @return the install directory
*/
public String getInstallDirectory() {
return installDirectory;
}
/**
* Return the connection to the MBean sever of the managed concourse-server
* process.
*
* @return the mbean server connection
*/
public MBeanServerConnection getMBeanServerConnection() {
if(mBeanServerConnection == null) {
try {
JMXServiceURL url = new JMXServiceURL(
"service:jmx:rmi:///jndi/rmi://localhost:"
+ prefs.getJmxPort() + "/jmxrmi");
JMXConnector connector = JMXConnectorFactory.connect(url);
mBeanServerConnection = connector.getMBeanServerConnection();
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
return mBeanServerConnection;
}
/**
* Get a collection of stats about the non heap memory usage for the managed
* concourse-server process.
*
* @return the non-heap memory usage
*/
public MemoryUsage getNonHeapMemoryStats() {
try {
MemoryMXBean memory = ManagementFactory.newPlatformMXBeanProxy(
getMBeanServerConnection(),
ManagementFactory.MEMORY_MXBEAN_NAME, MemoryMXBean.class);
return memory.getNonHeapMemoryUsage();
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Return {@code true} if the server is currently running.
*
* @return {@code true} if the server is running
*/
public boolean isRunning() {
return Iterables.get(execute("concourse", "status"), 0).contains(
"is running");
}
/**
* Start the server.
*/
public void start() {
try {
for (String line : execute("start")) {
log.info(line);
}
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Stop the server.
*/
public void stop() {
try {
for (String line : execute("stop")) {
log.info(line);
}
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Recursively delete a directory and all of its contents.
*
* @param directory
*/
private void deleteDirectory(String directory) {
try {
File dir = new File(directory);
for (File file : dir.listFiles()) {
if(file.isDirectory()) {
deleteDirectory(file.getAbsolutePath());
}
else {
file.delete();
}
}
dir.delete();
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Execute a command line interface script and return the output.
*
* @param cli
* @param args
* @return the script output
*/
private List execute(String cli, String... args) {
try {
String command = "sh " + cli;
for (String arg : args) {
command += " " + arg;
}
Process process = Runtime.getRuntime().exec(command, null,
new File(installDirectory + File.separator + BIN));
return Processes.getStdOut(process);
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* A class that extends {@link Concourse} with additional methods. This
* abstraction allows casting while keeping the {@link Client} class
* private.
*
* @author Jeff Nelson
*/
public abstract class ReflectiveClient extends Concourse {
/**
* Reflectively call a client method. This is useful for cases where
* this API depends on an older version of the Concourse API and a test
* needs to use a method added in a later version. This procedure will
* work because the underlying client delegates to a client that uses
* the classpath of the managed server.
*
* @param methodName
* @param args
* @return the result
*/
public abstract T call(String methodName, Object... args);
}
/**
* A {@link Concourse} client wrapper that delegates to the jars located in
* the server's lib directory so that it uses the same version of the code.
*
* @author jnelson
*/
private final class Client extends ReflectiveClient {
private final Class> clazz;
private final Object delegate;
private ClassLoader loader;
/**
* Construct a new instance.
*
* @param username
* @param password
* @throws Exception
*/
public Client(String username, String password) {
try {
this.loader = new URLClassLoader(
gatherJars(getInstallDirectory()), null);
this.clazz = loader
.loadClass("org.cinchapi.concourse.Concourse");
this.delegate = clazz.getMethod("connect", String.class,
int.class, String.class, String.class).invoke(null,
"localhost", getClientPort(), username, password);
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
@Override
public void abort() {
invoke("abort").with();
}
@Override
public Map add(String key, Object value,
Collection records) {
return invoke("add", String.class, Object.class, Collection.class)
.with(key, value, records);
}
@Override
public boolean add(String key, T value, long record) {
return invoke("add", String.class, Object.class, long.class).with(
key, value, record);
}
@Override
public Map audit(long record) {
return invoke("audit", long.class).with(record);
}
@Override
public Map audit(String key, long record) {
return invoke("audit", String.class, long.class).with(key, record);
}
@Override
public Map>> browse(
Collection records) {
return invoke("browse", Collection.class).with(records);
}
@Override
public Map>> browse(
Collection records, Timestamp timestamp) {
return invoke("browse", Collection.class, Timestamp.class).with(
records, timestamp);
}
@Override
public Map> browse(long record) {
return invoke("browse", long.class).with(record);
}
@Override
public Map> browse(long record, Timestamp timestamp) {
return invoke("browse", long.class, Timestamp.class).with(record,
timestamp);
}
@Override
public Map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy