|
@@ -18,18 +18,193 @@
|
|
|
|
|
|
package org.apache.zookeeper;
|
|
|
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.ServerSocket;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
/** Assign ports to tests */
|
|
|
-public class PortAssignment {
|
|
|
+public final class PortAssignment {
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(PortAssignment.class);
|
|
|
|
|
|
- private static int nextPort = 11221;
|
|
|
+ // The available port range that we use stays away from the ephemeral port
|
|
|
+ // range, which the OS will assign to client socket connections. We can't
|
|
|
+ // coordinate with the OS on the assignment of those ports, so it's best to
|
|
|
+ // stay out of that range to avoid conflicts. Typical ranges for ephemeral
|
|
|
+ // ports are:
|
|
|
+ // - IANA suggests 49152 - 65535
|
|
|
+ // - Linux typically uses 32768 - 61000
|
|
|
+ // - FreeBSD modern versions typically use the IANA suggested range
|
|
|
+ // - Windows modern versions typically use the IANA suggested range
|
|
|
+ private static final int GLOBAL_BASE_PORT = 11221;
|
|
|
+ private static final int GLOBAL_MAX_PORT = 32767;
|
|
|
+
|
|
|
+ private static PortRange portRange = null;
|
|
|
+ private static int nextPort;
|
|
|
|
|
|
- /** Assign a new, unique port to the test */
|
|
|
+ /**
|
|
|
+ * Assign a new, unique port to the test. This method works by assigning
|
|
|
+ * ports from a valid port range as identified by the total number of
|
|
|
+ * concurrent test processes and the ID of this test process. Each
|
|
|
+ * concurrent test process uses an isolated range, so it's not possible for
|
|
|
+ * multiple test processes to collide on the same port. Within the port
|
|
|
+ * range, ports are assigned in monotonic increasing order, wrapping around
|
|
|
+ * to the beginning of the range if needed. As an extra precaution, the
|
|
|
+ * method attempts to bind to the port and immediately close it before
|
|
|
+ * returning it to the caller. If the port cannot be bound, then it tries
|
|
|
+ * the next one in the range. This provides some resiliency in case the port
|
|
|
+ * is otherwise occupied, such as a developer running other servers on the
|
|
|
+ * machine running the tests.
|
|
|
+ *
|
|
|
+ * @return port
|
|
|
+ */
|
|
|
public synchronized static int unique() {
|
|
|
- LOG.info("assigning port " + nextPort);
|
|
|
- return nextPort++;
|
|
|
+ if (portRange == null) {
|
|
|
+ portRange = setupPortRange(System.getProperty("test.junit.threads"),
|
|
|
+ System.getProperty("sun.java.command"));
|
|
|
+ nextPort = portRange.getMinimum();
|
|
|
+ }
|
|
|
+ int candidatePort = nextPort;
|
|
|
+ for (;;) {
|
|
|
+ ++candidatePort;
|
|
|
+ if (candidatePort > portRange.getMaximum()) {
|
|
|
+ candidatePort = portRange.getMinimum();
|
|
|
+ }
|
|
|
+ if (candidatePort == nextPort) {
|
|
|
+ throw new IllegalStateException(String.format(
|
|
|
+ "Could not assign port from range %s. The entire " +
|
|
|
+ "range has been exhausted.", portRange));
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ ServerSocket s = new ServerSocket(candidatePort);
|
|
|
+ s.close();
|
|
|
+ nextPort = candidatePort;
|
|
|
+ LOG.info("Assigned port {} from range {}.", nextPort, portRange);
|
|
|
+ return nextPort;
|
|
|
+ } catch (IOException e) {
|
|
|
+ LOG.debug("Could not bind to port {} from range {}. " +
|
|
|
+ "Attempting next port.", candidatePort, portRange, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets up the port range to be used. In typical usage, Ant invokes JUnit,
|
|
|
+ * possibly using multiple JUnit processes to execute multiple test suites
|
|
|
+ * concurrently. The count of JUnit processes is passed from Ant as a system
|
|
|
+ * property named "test.junit.threads". Ant's JUnit runner receives the
|
|
|
+ * thread ID as a command line argument of the form threadid=N, where N is an
|
|
|
+ * integer in the range [1, ${test.junit.threads}]. It's not otherwise
|
|
|
+ * accessible, so we need to parse it from the command line. This method
|
|
|
+ * uses these 2 pieces of information to split the available ports into
|
|
|
+ * disjoint ranges. Each JUnit process only assigns ports from its own range
|
|
|
+ * in order to prevent bind errors during concurrent test runs. If any of
|
|
|
+ * this information is unavailable or unparseable, then the default behavior
|
|
|
+ * is for this process to use the entire available port range. This is
|
|
|
+ * expected when running tests outside of Ant.
|
|
|
+ *
|
|
|
+ * @param strProcessCount string representation of integer process count,
|
|
|
+ * typically taken from system property test.junit.threads
|
|
|
+ * @param cmdLine command line containing threadid=N argument, typically
|
|
|
+ * taken from system property sun.java.command
|
|
|
+ * @return port range to use
|
|
|
+ */
|
|
|
+ static PortRange setupPortRange(String strProcessCount, String cmdLine) {
|
|
|
+ Integer processCount = null;
|
|
|
+ if (strProcessCount != null && !strProcessCount.isEmpty()) {
|
|
|
+ try {
|
|
|
+ processCount = Integer.valueOf(strProcessCount);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ LOG.warn("Error parsing test.junit.threads = {}.",
|
|
|
+ strProcessCount, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Integer threadId = null;
|
|
|
+ if (processCount != null) {
|
|
|
+ if (cmdLine != null && !cmdLine.isEmpty()) {
|
|
|
+ Matcher m = Pattern.compile("threadid=(\\d+)").matcher(cmdLine);
|
|
|
+ if (m.find()) {
|
|
|
+ try {
|
|
|
+ threadId = Integer.valueOf(m.group(1));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ LOG.warn("Error parsing threadid from {}.", cmdLine, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ final PortRange newPortRange;
|
|
|
+ if (processCount != null && processCount > 1 && threadId != null) {
|
|
|
+ // We know the total JUnit process count and this test process's ID.
|
|
|
+ // Use these values to calculate the valid range for port assignments
|
|
|
+ // within this test process. We lose a few possible ports to the
|
|
|
+ // remainder, but that's acceptable.
|
|
|
+ int portRangeSize = (GLOBAL_MAX_PORT - GLOBAL_BASE_PORT) /
|
|
|
+ processCount;
|
|
|
+ int minPort = GLOBAL_BASE_PORT + ((threadId - 1) * portRangeSize);
|
|
|
+ int maxPort = minPort + portRangeSize - 1;
|
|
|
+ newPortRange = new PortRange(minPort, maxPort);
|
|
|
+ LOG.info("Test process {}/{} using ports from {}.", threadId,
|
|
|
+ processCount, newPortRange);
|
|
|
+ } else {
|
|
|
+ // If running outside the context of Ant or Ant is using a single
|
|
|
+ // test process, then use all valid ports.
|
|
|
+ newPortRange = new PortRange(GLOBAL_BASE_PORT, GLOBAL_MAX_PORT);
|
|
|
+ LOG.info("Single test process using ports from {}.", newPortRange);
|
|
|
+ }
|
|
|
+
|
|
|
+ return newPortRange;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Contains the minimum and maximum (both inclusive) in a range of ports.
|
|
|
+ */
|
|
|
+ static final class PortRange {
|
|
|
+ private final int minimum;
|
|
|
+ private final int maximum;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a new PortRange.
|
|
|
+ *
|
|
|
+ * @param minimum lower bound port number
|
|
|
+ * @param maximum upper bound port number
|
|
|
+ */
|
|
|
+ PortRange(int minimum, int maximum) {
|
|
|
+ this.minimum = minimum;
|
|
|
+ this.maximum = maximum;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns maximum port in the range.
|
|
|
+ *
|
|
|
+ * @return maximum
|
|
|
+ */
|
|
|
+ int getMaximum() {
|
|
|
+ return maximum;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns minimum port in the range.
|
|
|
+ *
|
|
|
+ * @return minimum
|
|
|
+ */
|
|
|
+ int getMinimum() {
|
|
|
+ return minimum;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return String.format("%d - %d", minimum, maximum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * There is no reason to instantiate this class.
|
|
|
+ */
|
|
|
+ private PortAssignment() {
|
|
|
}
|
|
|
}
|