/* * Copyright 2016-2017 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.cassandra.test.integration; import static java.util.concurrent.TimeUnit.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.service.CassandraDaemon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.FileCopyUtils; /** * Imported Embedded Cassandra server startup helper. * * @author Mark Paluch */ class EmbeddedCassandraServerHelper { private static Logger log = LoggerFactory.getLogger(EmbeddedCassandraServerHelper.class); public static final long DEFAULT_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20); public static final String DEFAULT_TMP_DIR = "target/embeddedCassandra"; private static final AtomicReference<Object> sync = new AtomicReference<>(); private static final AtomicReference<CassandraDaemon> cassandraRef = new AtomicReference<>(); private static String launchedYamlFile; /** * Get the embedded cassandra cluster name * * @return the cluster name */ public static String getClusterName() { return DatabaseDescriptor.getClusterName(); } /** * Get embedded cassandra host. * * @return the cassandra host */ public static String getHost() { return DatabaseDescriptor.getRpcAddress().getHostName(); } /** * Get embedded cassandra RPC port. * * @return the cassandra RPC port */ public static int getRpcPort() { return DatabaseDescriptor.getRpcPort(); } /** * Get embedded cassandra native transport port. * * @return the cassandra native transport port. */ public static int getNativeTransportPort() { return DatabaseDescriptor.getNativeTransportPort(); } /** * Start an embedded Cassandra instance. * * @param yamlResource * @param timeout * @throws Exception */ public static void startEmbeddedCassandra(String yamlResource, long timeout) throws Exception { startEmbeddedCassandra(yamlResource, DEFAULT_TMP_DIR, timeout); } /** * Start an embedded Cassandra instance. * * @param yamlResource * @param tmpDir * @param timeout * @throws Exception */ public static void startEmbeddedCassandra(String yamlResource, String tmpDir, long timeout) throws Exception { if (cassandraRef.get() != null) { /* nothing to do Cassandra is already started */ return; } if (!sync.compareAndSet(null, new Object())) { /* A different Thread was faster, so nothing to do for us here */ return; } File yamlFile = new File(tmpDir, new File(yamlResource).getName()); prepareCassandraDirectory(yamlResource, tmpDir, yamlFile); startEmbeddedCassandra(yamlFile, timeout); } /** * Cleanup directory, copy YAML file to configuration directory and * * @param yamlFileName * @param cassandraDirectoryName * @param yamlFile * @throws IOException */ private static void prepareCassandraDirectory(String yamlFileName, String cassandraDirectoryName, File yamlFile) throws IOException { File cassandraDirectory = new File(cassandraDirectoryName); rmdirs(cassandraDirectory); copy(yamlFileName, cassandraDirectory); } /** * Set embedded cassandra up and spawn it in a new thread. */ private static void startEmbeddedCassandra(File file, long timeout) throws Exception { checkConfigNameForRestart(file.getAbsolutePath()); log.debug("Starting cassandra..."); log.debug("Initialization needed"); System.setProperty("cassandra.config", "file:" + file.getAbsolutePath()); System.setProperty("cassandra-foreground", "true"); System.setProperty("cassandra.native.epoll.enabled", "false"); // JNA doesn't cope with relocated netty cleanupAndRecreateDirectories(); final CassandraDaemon cassandraDaemon = new CassandraDaemon(); ExecutorService executor = Executors.newSingleThreadExecutor(); Future<?> future = executor.submit(() -> { cassandraDaemon.activate(); cassandraRef.compareAndSet(null, cassandraDaemon); }); try { future.get(timeout, MILLISECONDS); } catch (ExecutionException e) { log.error("Cassandra daemon did not start after " + timeout + " ms. Consider increasing the timeout"); throw new IllegalStateException("Cassandra daemon did not start within timeout", e); } catch (InterruptedException e) { log.error("Interrupted waiting for Cassandra daemon to start:", e); Thread.currentThread().interrupt(); throw new IllegalStateException(e); } finally { executor.shutdown(); } } private static void cleanupAndRecreateDirectories() throws IOException { DatabaseDescriptor.daemonInitialization(); createCassandraDirectories(); cleanup(); createCassandraDirectories(); CommitLog commitLog = CommitLog.instance; commitLog.getCurrentPosition(); // wait for commit log allocator instantiation to avoid hanging on a race condition commitLog.resetUnsafe(true); // cleanup screws w/ CommitLog, this brings it back to safe state } private static void cleanup() throws IOException { // clean up commitlog and data locations rmdirs(DatabaseDescriptor.getCommitLogLocation()); rmdirs(DatabaseDescriptor.getAllDataFileLocations()); } private static void checkConfigNameForRestart(String yamlFile) { boolean wasPreviouslyLaunched = cassandraRef.get() != null; if (wasPreviouslyLaunched && !launchedYamlFile.equals(yamlFile)) { throw new UnsupportedOperationException("We can't launch two Cassandra configurations in the same JVM instance"); } launchedYamlFile = yamlFile; } /** * Copies a resource from within the jar to a directory. * * @param resource name of the resource * @param targetDirectory name of the target directory * @throws IOException */ private static void copy(String resource, File targetDirectory) throws IOException { FileUtils.createDirectory(targetDirectory); File file = new File(targetDirectory, new File(resource).getName()); InputStream is = EmbeddedCassandraServerHelper.class.getClassLoader().getResourceAsStream(resource); OutputStream out = new FileOutputStream(file); FileCopyUtils.copy(is, out); out.close(); is.close(); } private static void createCassandraDirectories() { DatabaseDescriptor.createAllDirectories(); } private static void rmdirs(String... fileOrDirectories) throws IOException { for (String fileOrDirectory : fileOrDirectories) { rmdirs(new File(fileOrDirectory)); } } private static void rmdirs(File... fileOrDirectories) throws IOException { Arrays.stream(fileOrDirectories).filter(File::exists).forEach(FileUtils::deleteRecursive); } }