package com.mastfrog.acteur.mongo; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.google.inject.name.Names; import com.mastfrog.giulius.ShutdownHookRegistry; import com.mastfrog.util.Checks; import com.mastfrog.util.Exceptions; import java.io.File; import java.io.IOException; import java.net.ConnectException; import java.net.DatagramSocket; import java.net.ServerSocket; import java.net.Socket; import java.util.Random; /** * Starts a local mongodb over java.io.tmpdir and cleans it up on shutdown; uses * a random, available port. Simply request this be injected in your test, and * use MongoHarness.Module, to have the db started for you and automatically * cleaned up. * <p/> * Test <code>failed()</code> if you want to detect if you're running on a * machine where mongodb is not installed. * * @author Tim Boudreau */ @Singleton public class MongoHarness { private final int port; private final Init mongo; private static int count = 1; @Inject MongoHarness(@Named("mongoPort") int port, Init mongo) throws IOException, InterruptedException { this.port = port; this.mongo = mongo; } @Singleton static class Init extends MongoInitializer implements Runnable { private final File mongoDir; private Process mongo; private int port; @SuppressWarnings("LeakingThisInConstructor") @Inject public Init(MongoInitializer.Registry registry, ShutdownHookRegistry shutdownHooks) { super(registry); shutdownHooks.add(this); mongoDir = createMongoDir(); } @Override protected void onBeforeCreateMongoClient(String host, int port) { try { this.port = port; mongo = startMongoDB(port); } catch (IOException | InterruptedException ex) { Exceptions.chuck(ex); } } public void stop() { if (mongo != null) { ProcessBuilder pb = new ProcessBuilder().command("mongod", "--dbpath", mongoDir.getAbsolutePath(), "--shutdown", "--port", "" + port); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); try { System.out.println("Try graceful mongodb shutdown " + pb); Process shutdown = pb.start(); boolean exited = false; for (int i = 0; i < 19000; i++) { try { int exit = shutdown.exitValue(); System.out.println("Shutdown mongodb call exited with " + exit); break; } catch (IllegalThreadStateException ex) { // System.out.println("no exit code yet, sleeping"); try { Thread.sleep(10); } catch (InterruptedException ex1) { org.openide.util.Exceptions.printStackTrace(ex1); } } } System.out.println("Wait for mongodb exit"); for (int i = 0; i < 10000; i++) { try { int code = mongo.exitValue(); System.out.println("Exit code " + code); exited = true; break; } catch (IllegalThreadStateException ex) { // System.out.println("Not exited yet; sleep 100ms"); try { Thread.sleep(10); } catch (InterruptedException ex1) { Exceptions.printStackTrace(ex1); } } catch (Exception e) { e.printStackTrace(); } if (!exited && i > 30) { System.out.println("Mongodb has not exited; kill it"); mongo.destroy(); } } } catch (IOException ex) { Exceptions.chuck(ex); mongo = null; } mongo = null; } } public void start() throws IOException, InterruptedException { if (mongo != null) { throw new IllegalStateException("MongoDB already started"); } mongo = startMongoDB(port); } @Override public void run() { try { stop(); } finally { if (mongoDir != null && mongoDir.exists()) { cleanup(mongoDir); } } } private File createMongoDir() { File tmp = new File(System.getProperty("java.io.tmpdir")); File mongoDir = new File(tmp, "mongo-" + System.currentTimeMillis() + "-" + count++); if (!mongoDir.mkdirs()) { throw new AssertionError("Could not create " + mongoDir); } return mongoDir; } private volatile boolean failed; public boolean failed() { return failed; } private Process startMongoDB(int port) throws IOException, InterruptedException { Checks.nonZero("port", port); Checks.nonNegative("port", port); System.out.println("Starting mongodb on port " + port + " with data dir " + mongoDir); ProcessBuilder pb = new ProcessBuilder().command("mongod", "--dbpath", mongoDir.getAbsolutePath(), "--nojournal", "--smallfiles", "-nssize", "1", "--noprealloc", "--slowms", "5", "--port", "" + port, "--maxConns", "50", "--nohttpinterface", "--syncdelay", "0", "--oplogSize", "1", "--diaglog", "0"); System.out.println(pb.command()); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); // XXX instead of sleep, loop trying to connect? Process result = pb.start(); Thread.sleep(150); for (int i = 0;; i++) { try { Socket s = new Socket("localhost", port); s.close(); Thread.sleep(50); break; } catch (ConnectException e) { if (i > 1750) { throw new IOException("Could not connect to mongodb " + "after " + i + " attempts. Assuming it's dead."); } Thread.yield(); } } return result; } } private static void cleanup(File dir) { for (File f : dir.listFiles()) { if (f.isDirectory()) { cleanup(f); f.delete(); } } for (File f : dir.listFiles()) { if (f.isFile()) { f.delete(); } } dir.delete(); } /** * Determine if starting MongoDB failed (the process exited with non-zero a * few seconds after launch). Use this to allow tests to pass when building * on a machine which does not have mongodb installed. * * @return True if mongodb was started and failed for some reason (details * will be on system.out) */ public boolean failed() { return mongo.failed(); } /** * Stop mongodb. This is done automatically on system shutdown - only call * this if you want to test the behavior of something when the database is * <i>not</i> there for some reason. */ public void stop() { mongo.stop(); } /** * Start mongodb, if stop has been called. Otehrwise, it is automatically * started for you. * * @throws IOException * @throws InterruptedException */ public void start() throws IOException, InterruptedException { mongo.start(); } /** * Get the randomly selected available port we wnat to use * * @return a port */ public int port() { return port; } public static class Module extends AbstractModule { @Override protected void configure() { bind(String.class).annotatedWith(Names.named("mongoPort")).toInstance("" + findPort()); bind(String.class).annotatedWith(Names.named("mongoHost")).toInstance("localhost"); bind(Init.class).asEagerSingleton(); } private int findPort() { Random r = new Random(System.currentTimeMillis()); int port; do { // Make sure we're out of the way of a running mongo instance, // both the mongo port and the http port int startPort = 28002; port = r.nextInt(65536 - startPort) + startPort; } while (!available(port)); return port; } private boolean available(int port) { try (ServerSocket ss = new ServerSocket(port)) { ss.setReuseAddress(true); try (DatagramSocket ds = new DatagramSocket(port)) { ds.setReuseAddress(true); return true; } catch (IOException e) { return false; } } catch (IOException e) { return false; } } } }