diff --git a/CHANGELOG.md b/CHANGELOG.md index ea88496047403449aad3a6125de18020c608839e..ae012a30c68e3f326845ff405b6f3e0ed6781e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # embedmongo-maven-plugin Changelog +## 0.1.8 + +* Add support for random port allocation - new configuration option `randomPort` + ## 0.1.7 * Add `logFile`/`logFileEncoding` configuration options (thanks @matthewadams) diff --git a/README.md b/README.md index d8a7079caf6ac339e3b23ff59b5190a6e670d701..1d441adda2f55b6a6ffa726403ec13e7c201f907 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Usage </goals> <configuration> <port>37017</port> <!-- optional, default 27017 --> + <randomPort>true</randomPort> <!-- optional, default is false, if true overrides "static" port configuration --> <version>2.0.4</version> <!-- optional, default 2.2.1 --> <databaseDirectory>/tmp/mongotest</databaseDirectory> <!-- optional, default is a new dir in java.io.tmpdir --> <logging>file</logging> <!-- optional (file|console|none), default console --> @@ -48,7 +49,8 @@ Notes * By default, the `start` goal is bound to `pre-integration-test`, the `stop` goal is bound to `post-integration-test`. You can of course bind to different phases if required. * If you omit/forget the `stop` goal, any Mongo process spawned by the `start` goal will be stopped when the JVM terminates. -* If you want to run many Maven builds in parallel using Jenkins, try the [Port Allocator Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Port+Allocator+Plugin) to avoid port conflicts. +* If you want to run many Maven builds in parallel you can use `randomPort` to avoid port conflicts. + If you are using Jenkins, you can also try the [Port Allocator Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Port+Allocator+Plugin). * If you need to use a proxy to download MongoDB then you can either use `-Dhttp.proxyHost` and `-Dhttp.proxyPort` as additional Maven arguments (this will affect the entire build) or instruct the plugin to use a proxy when downloading Mongo by adding the `proxyHost` and `proxyPort` configuration properties. * Using the `file` logging mode results in a new log file created at `./embedmongo.log` unless you specify via `<logFile>` (where `.` means the curent working directory, usually the same as `${basedir}`). * If you'd like the start goal to start mongodb and wait, you can add `-Dembedmongo.wait` to your Maven command line arguments diff --git a/pom.xml b/pom.xml index 48a12fa74e98f4c281243931dba03b21f424561f..26eec03e4545a1e449ebe8ddc3e15802b3ed42e3 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,12 @@ <artifactId>maven-plugin-api</artifactId> <version>2.0</version> </dependency> + <!--To be able to use MavenSession#userProperties we need maven-core (version 2.0 is too old, however)--> + <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-core</artifactId> + <version>2.2.1</version> + </dependency> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-project</artifactId> @@ -107,6 +113,12 @@ <artifactId>de.flapdoodle.embed.mongo</artifactId> <version>1.31</version> </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit-dep</artifactId> + <version>4.10</version> + <scope>test</scope> + </dependency> </dependencies> <profiles> diff --git a/src/main/java/com/github/joelittlejohn/embedmongo/StartEmbeddedMongoMojo.java b/src/main/java/com/github/joelittlejohn/embedmongo/StartEmbeddedMongoMojo.java index 76a7717adec7631f9582c986cf79352414344d60..2600afbf8a28ca2101561c5a252d01930a345101 100644 --- a/src/main/java/com/github/joelittlejohn/embedmongo/StartEmbeddedMongoMojo.java +++ b/src/main/java/com/github/joelittlejohn/embedmongo/StartEmbeddedMongoMojo.java @@ -28,8 +28,11 @@ import java.net.SocketAddress; import java.net.URI; import java.net.UnknownHostException; import java.util.List; +import java.util.Properties; import java.util.concurrent.TimeUnit; +import com.github.joelittlejohn.embedmongo.port.PortHelper; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -53,6 +56,7 @@ import de.flapdoodle.embed.process.distribution.GenericVersion; import de.flapdoodle.embed.process.distribution.IVersion; import de.flapdoodle.embed.process.exceptions.DistributionException; import de.flapdoodle.embed.process.runtime.Network; +import org.apache.maven.project.MavenProject; /** * When invoked, this goal starts an instance of mongo. The required binaries @@ -76,6 +80,14 @@ public class StartEmbeddedMongoMojo extends AbstractMojo { */ private int port; + /** + * Random port should be used for MongoDB instead of the one specified by {@code port}. + * + * @parameter expression="${embedmongo.randomPort}" default-value="false" + * @since 0.1.8 + */ + private boolean randomPort; + /** * The version of MongoDB to run e.g. 2.1.1, 1.6 v1.8.2, V2_0_4, * @@ -161,6 +173,23 @@ public class StartEmbeddedMongoMojo extends AbstractMojo { */ private String proxyPassword; + /** + * The maven project. + * + * @parameter expression="${project}" + * @readonly + */ + private MavenProject project; + + /** + * The Maven Session Object for setting allocated port to session's user properties. + * + * @parameter expression="${session}" + * @readonly + */ + private MavenSession session; + + @Override @SuppressWarnings("unchecked") public void execute() throws MojoExecutionException, MojoFailureException { @@ -176,7 +205,10 @@ public class StartEmbeddedMongoMojo extends AbstractMojo { .defaults(Command.MongoD) .processOutput(getOutputConfig()) .build(); - + if (randomPort) { + port = new PortHelper().allocateRandomPort(); + } + savePortToSessionUserProperties(); MongodConfig mongoConfig = new MongodConfig(getVersion(), new Net(bindIp, port, Network.localhostIsIPv6()), new Storage(getDataDirectory(), null, 0), @@ -208,6 +240,25 @@ public class StartEmbeddedMongoMojo extends AbstractMojo { } } + /** + * Saves port to the {@link MavenSession#userProperties} to provide client with ability to retrieve port + * later in integration-test-phase via {@link PortHelper#getMongoPort(String)} method. + * Port is saved as a property {@code MONGO_PORT_PROPERTY + "." + artifactId} where {@code artifactId} + * is id of project artifact where the integration tests are run. + * The {@code artifactId} suffix is necessary because concurrent executions of {@code embedmongo-maven-plugin} + * cannot share the same property. + * <p> + * {@code userProperties} seems to be the only way how to propagate property to the forked JVM run + * started by failsafe plugin. + * </p> + */ + private void savePortToSessionUserProperties() { + final String portKey = PortHelper.MONGO_PORT_PROPERTY + "." + project.getArtifactId(); + final String portValue = Integer.toString(port); + final Properties userProperties = session.getUserProperties(); + userProperties.setProperty(portKey, portValue); + } + private ProcessOutput getOutputConfig() throws MojoFailureException { LoggingStyle loggingStyle = LoggingStyle.valueOf(logging.toUpperCase()); diff --git a/src/main/java/com/github/joelittlejohn/embedmongo/port/PortHelper.java b/src/main/java/com/github/joelittlejohn/embedmongo/port/PortHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..303059879d3749690d9fed5e0c8afb52c86a2941 --- /dev/null +++ b/src/main/java/com/github/joelittlejohn/embedmongo/port/PortHelper.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2012 Joe Littlejohn + * + * 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 com.github.joelittlejohn.embedmongo.port; + +import org.codehaus.plexus.util.StringUtils; + +import java.io.IOException; +import java.net.ServerSocket; + +/** + * Port helper for common operations with port. + * <p> + * It can : + * <ul> + * <li>allocate random free port - Inspired by + * <a href="https://github.com/sonatype/port-allocator-maven-plugin/blob/master/src/main/java/org/sonatype/plugins/portallocator/PortAllocatorMojo.java"> + * PortAllocatorMojo</a>.</li> + * <li>Return port already allocated by invoking start goal of {@code embedmongo-maven-plugin}</li> + * </ul> + +* </p> + */ +public class PortHelper { + + public static final String MONGO_PORT_PROPERTY = "embedmongo.port"; + + + /** + * Finds port where the mongodb started in previous phase by {@code embedmongo-maven-plugin} should be running. + * This assumes that embedmongo-maven-plugin' goal start has already been invoked for {@code artifactId} + * and allocates port properly. + * + * @param artifactId maven artifact id of module for which the port set by {@code embedmongo-maven-plugin} should be found. + * @return port of mongodb for running integration tests of given {@code artifactId} + */ + public static int getMongoPort(String artifactId) { + String portProperty; + if (artifactId == null || artifactId.length() == 0) { + throw new IllegalArgumentException("maven artifactId has to be specified to find port for proper maven module."); + } else { + portProperty = MONGO_PORT_PROPERTY + "." + artifactId; + } + final String port = System.getProperty(portProperty); + if (StringUtils.isEmpty(port)) { + throw new IllegalStateException("No mongo port has been set by embedmongo maven plugin via system property '" + + portProperty + "'!\n Check plugin configuration and make sure that mongoDb is started before" + + " you are trying to access its port."); + } + return Integer.valueOf(port); + } + + + /** + * Allocates new free random port. + * + * <p> + * This implementation allocate random free port and closes it immediately. + * In some (hopefully) rare situations the port may be occupied in the meantime between calling this method + * and using returned port on client side. + * </p> + * @return random free port + */ + public int allocateRandomPort() { + return allocate(0); + } + + + //--------------------------------------------------- HELPER METHODS ----------------------------------------------- + + private static int allocate(int portNumber) { + ServerSocket server; + try { + server = new ServerSocket(portNumber); + } catch (IOException e) { + throw new PortUnavailableException(portNumber, e); + } + + portNumber = server.getLocalPort(); + try { + server.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to release port " + portNumber, e); + } + return portNumber; + } +} diff --git a/src/main/java/com/github/joelittlejohn/embedmongo/port/PortUnavailableException.java b/src/main/java/com/github/joelittlejohn/embedmongo/port/PortUnavailableException.java new file mode 100644 index 0000000000000000000000000000000000000000..2add5af195cd7bb91bd2afac802edc8fd38fad54 --- /dev/null +++ b/src/main/java/com/github/joelittlejohn/embedmongo/port/PortUnavailableException.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2012 Joe Littlejohn + * + * 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 com.github.joelittlejohn.embedmongo.port; + +/** + * Represents exceptional state when given port is not available. + */ +public class PortUnavailableException extends RuntimeException { + public PortUnavailableException(int port, Throwable cause) { + super("Port " + port + " is not available", cause); + } +} diff --git a/src/test/java/com/github/joelittlejohn/embedmongo/port/PortHelperTest.java b/src/test/java/com/github/joelittlejohn/embedmongo/port/PortHelperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..aa406cc27b83831b9cf0c420544ca414ea8a0b5a --- /dev/null +++ b/src/test/java/com/github/joelittlejohn/embedmongo/port/PortHelperTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2012 Joe Littlejohn + * + * 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 com.github.joelittlejohn.embedmongo.port; + +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class PortHelperTest { + + private final PortHelper portHelper = new PortHelper(); + private final ScheduledExecutorService testPooledExecutor = Executors.newScheduledThreadPool(20); + + @After + public void tearDown() throws Exception { + testPooledExecutor.shutdown(); + } + + /** + * This test executes method {@link com.github.joelittlejohn.embedmongo.port.PortHelper#allocateRandomPort()} many times + * concurrently to make sure that port allocation works correctly under stress. + */ + @Test + public void testAllocateRandomPort() throws Exception { + final int testAllocationCount = 10000; + final CountDownLatch allocationsCounter = new CountDownLatch(testAllocationCount); + + final Runnable allocatePort = new Runnable() { + @Override + public void run() { + int port = -1; + try { + port = portHelper.allocateRandomPort(); + new ServerSocket(port); + // port has been bind successfully + } catch (IOException e) { + throw new RuntimeException("Port " + port + " cannot be bind!"); + } finally { + allocationsCounter.countDown(); + } + } + }; + + final Random randomGenerator = new Random(); + for (int i = 0; i < testAllocationCount; i++) { + // schedule execution a little to in the future to simulate less predictable environment + testPooledExecutor.schedule(allocatePort, randomGenerator.nextInt(10), TimeUnit.MILLISECONDS); + } + allocationsCounter.await(10, TimeUnit.SECONDS); + } + +} diff --git a/src/test/resources/example1/pom.xml b/src/test/resources/example1/pom.xml index 79183ff1e8b2284e8f177f15c8ff006f8d27d01a..e8c9f0cecda60dd0406f0a9c30e52ffbeadd5f8f 100644 --- a/src/test/resources/example1/pom.xml +++ b/src/test/resources/example1/pom.xml @@ -29,7 +29,7 @@ <plugin> <groupId>com.github.joelittlejohn.embedmongo</groupId> <artifactId>embedmongo-maven-plugin</artifactId> - <version>0.1.7-SNAPSHOT</version> + <version>0.1.8-SNAPSHOT</version> <executions> <execution> <id>start</id> diff --git a/src/test/resources/example2/pom.xml b/src/test/resources/example2/pom.xml index cf9c60e491fca2d04964936d042234db952a79fe..4470b9a6eb3b321c3f7d72e9e7f7ce51cee706ac 100644 --- a/src/test/resources/example2/pom.xml +++ b/src/test/resources/example2/pom.xml @@ -29,7 +29,7 @@ <plugin> <groupId>com.github.joelittlejohn.embedmongo</groupId> <artifactId>embedmongo-maven-plugin</artifactId> - <version>0.1.7-SNAPSHOT</version> + <version>0.1.8-SNAPSHOT</version> <executions> <execution> <id>start</id> diff --git a/src/test/resources/example3/pom.xml b/src/test/resources/example3/pom.xml index 37241641a46af90db6aa5f428680f91b5590a1fc..59618aa89223df49ad40a65424d98ba27692a089 100644 --- a/src/test/resources/example3/pom.xml +++ b/src/test/resources/example3/pom.xml @@ -29,7 +29,7 @@ <plugin> <groupId>com.github.joelittlejohn.embedmongo</groupId> <artifactId>embedmongo-maven-plugin</artifactId> - <version>0.1.7-SNAPSHOT</version> + <version>0.1.8-SNAPSHOT</version> <executions> <execution> <id>start</id> diff --git a/src/test/resources/pom.xml b/src/test/resources/pom.xml index a873da6929a9489cd1d64eb6dcf34252e65da99a..21182d8147dc407e03adf008d17b920327fb58ff 100644 --- a/src/test/resources/pom.xml +++ b/src/test/resources/pom.xml @@ -12,8 +12,9 @@ <modules> <module>example1</module> - <module>example2</module> + <module>example2</module> <module>example3</module> + <module>randomport</module> </modules> </project> diff --git a/src/test/resources/randomport/pom.xml b/src/test/resources/randomport/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..96762ac2e77477fa0fe042449747f2729f0d91ea --- /dev/null +++ b/src/test/resources/randomport/pom.xml @@ -0,0 +1,86 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>com.github.joelittlejohn.embedmongo</groupId> + <artifactId>embedmongo-maven-plugin-test-parent</artifactId> + <version>0.1.0-SNAPSHOT</version> + </parent> + + <groupId>com.github.joelittlejohn.embedmongo</groupId> + <artifactId>embedmongo-maven-plugin-random-port-test</artifactId> + <version>0.1.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <url>https://github.com/joelittlejohn/embedmongo-maven-plugin</url> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.4</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + </configuration> + </plugin> + <plugin> + <groupId>com.github.joelittlejohn.embedmongo</groupId> + <artifactId>embedmongo-maven-plugin</artifactId> + <version>0.1.8-SNAPSHOT</version> + <executions> + <execution> + <id>start</id> + <goals> + <goal>start</goal> + </goals> + <configuration> + <port>37017</port> + <databaseDirectory>/tmp/mongotest</databaseDirectory> + <logging>console</logging> + <version>v2.2.0</version> + </configuration> + </execution> + <execution> + <id>stop</id> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> + </plugin> + <!--Runs integration tests and verifies their results--> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <version>2.12.4</version> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit-dep</artifactId> + <version>4.10</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.joelittlejohn.embedmongo</groupId> + <artifactId>embedmongo-maven-plugin</artifactId> + <version>0.1.8-SNAPSHOT</version> + </dependency> + </dependencies> + +</project> diff --git a/src/test/resources/randomport/src/test/java/com/github/joelittlejohn/embedmongo/MongoIT.java b/src/test/resources/randomport/src/test/java/com/github/joelittlejohn/embedmongo/MongoIT.java new file mode 100644 index 0000000000000000000000000000000000000000..8fe6fcc5064cb2d54acca8deb7feffce4b4972c5 --- /dev/null +++ b/src/test/resources/randomport/src/test/java/com/github/joelittlejohn/embedmongo/MongoIT.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2012 Joe Littlejohn + * + * 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 com.github.joelittlejohn.embedmongo; + +import com.github.joelittlejohn.embedmongo.port.PortHelper; +import org.junit.After; +import org.junit.Test; + +import java.net.Socket; + +public class MongoIT { + + private Socket mongoSocket; + + @Test + public void testConnectMongo() throws Exception { + mongoSocket = new Socket("127.0.0.1", PortHelper.getMongoPort("embedmongo-maven-plugin-random-port-test")); + } + + @After + public void tearDown() throws Exception { + if (mongoSocket != null) { + mongoSocket.close(); + } + } +}