diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java index ba4a1ef932f..093a19bc08b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.util; * 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. @@ -20,7 +20,6 @@ package ca.uhn.fhir.util; * #L% */ -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,21 +27,137 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.util.ArrayList; +import java.util.List; /** - * Provides server ports + * Provides server ports that are free, in order for tests to use them + * + * This class is not designed for non-testing usage, as it holds on to server ports + * for a long time (potentially lots of them!) */ @CoverageIgnore public class PortUtil { + public static final int SPACE_SIZE = 100; private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class); + private static final PortUtil INSTANCE = new PortUtil(); + private List myControlSockets = new ArrayList<>(); + private Integer myCurrentControlSocketPort = null; + private int myCurrentOffset = 0; + /* * Non instantiable */ - private PortUtil() { + public PortUtil() { // nothing } + public synchronized void clear() { + for (ServerSocket next : myControlSockets) { + ourLog.info("Releasing control port: {}", next.getLocalPort()); + try { + next.close(); + } catch (IOException theE) { + // ignore + } + } + myControlSockets.clear(); + myCurrentControlSocketPort = null; + } + + public synchronized int getNextFreePort() { + + while (true) { + + // Acquire a control port + while (myCurrentControlSocketPort == null) { + int nextCandidate = (int) (Math.random() * 65000.0); + nextCandidate = nextCandidate - (nextCandidate % SPACE_SIZE); + + if (nextCandidate < 10000) { + continue; + } + + try { + ServerSocket server = new ServerSocket(); + server.setReuseAddress(true); + server.bind(new InetSocketAddress("localhost", nextCandidate)); + myControlSockets.add(server); + ourLog.info("Acquired control socket on port {}", nextCandidate); + myCurrentControlSocketPort = nextCandidate; + myCurrentOffset = 0; + } catch (IOException theE) { + ourLog.info("Candidate control socket {} is already taken", nextCandidate); + continue; + } + } + + // Find a free port within the allowable range + while (true) { + myCurrentOffset++; + + if (myCurrentOffset == SPACE_SIZE) { + // Current space is exhausted + myCurrentControlSocketPort = null; + break; + } + + int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset; + + // Try to open a port on this socket and use it + try (ServerSocket server = new ServerSocket()) { + server.setReuseAddress(true); + server.bind(new InetSocketAddress("localhost", nextCandidatePort)); + try (Socket client = new Socket()) { + client.setReuseAddress(true); + client.connect(new InetSocketAddress("localhost", nextCandidatePort)); + } + } catch (IOException e) { + continue; + } + + /* + * This is an attempt to make sure the port is actually + * free before releasing it. For whatever reason on Linux + * it seems like even after we close the ServerSocket there + * is a short while where it is not possible to bind the + * port, even though it should be released by then. + * + * I don't have any solid evidence that this is a good + * way to do this, but it seems to help... + */ + for (int i = 0; i < 10; i++) { + try { + Socket client = new Socket(); + client.setReuseAddress(true); + client.connect(new InetSocketAddress(nextCandidatePort), 1000); + ourLog.info("Socket still seems open"); + Thread.sleep(250); + } catch (Exception e) { + break; + } + } + +// try { +// Thread.sleep(500); +// } catch (InterruptedException theE) { +// // ignore +// } + + // Log who asked for the port, just in case that's useful + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + StackTraceElement previousElement = stackTraceElements[2]; +// ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString()); + + return nextCandidatePort; + + } + + + } + } + /** * The entire purpose here is to find an available port that can then be * bound for by server in a unit test without conflicting with other tests. @@ -51,64 +166,7 @@ public class PortUtil { * so it can be reused across modules. Use with caution. */ public static int findFreePort() { - ServerSocket server; - try { - server = new ServerSocket(0); - server.setReuseAddress(true); - int port = server.getLocalPort(); - - /* - * Try to connect to the newly allocated port to make sure - * it's free - */ - for (int i = 0; i < 10; i++) { - try { - Socket client = new Socket(); - client.connect(new InetSocketAddress(port), 1000); - break; - } catch (Exception e) { - if (i == 9) { - throw new InternalErrorException("Can not connect to port: " + port); - } - Thread.sleep(250); - } - } - - server.close(); - - /* - * This is an attempt to make sure the port is actually - * free before releasing it. For whatever reason on Linux - * it seems like even after we close the ServerSocket there - * is a short while where it is not possible to bind the - * port, even though it should be released by then. - * - * I don't have any solid evidence that this is a good - * way to do this, but it seems to help... - */ - for (int i = 0; i < 10; i++) { - try { - Socket client = new Socket(); - client.connect(new InetSocketAddress(port), 1000); - ourLog.info("Socket still seems open"); - Thread.sleep(250); - } catch (Exception e) { - break; - } - } - - // ....annnd sleep a bit for the same reason. - Thread.sleep(500); - - // Log who asked for the port, just in case that's useful - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - StackTraceElement previousElement = stackTraceElements[2]; - ourLog.info("Returned available port {} for: {}", port, previousElement.toString()); - - return port; - } catch (IOException | InterruptedException e) { - throw new Error(e); - } + return INSTANCE.getNextFreePort(); } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/PortUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/PortUtilTest.java index 327e41117fd..bd9403ba055 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/PortUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/PortUtilTest.java @@ -1,15 +1,26 @@ package ca.uhn.fhir.util; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class PortUtilTest { + private static final Logger ourLog = LoggerFactory.getLogger(PortUtilTest.class); + @Test public void testFindFreePort() throws IOException { int port = PortUtil.findFreePort(); @@ -28,4 +39,49 @@ public class PortUtilTest { } } + @Test + public void testPortsAreNotReused() throws InterruptedException { + + List ports = Collections.synchronizedList(new ArrayList<>()); + List portUtils = Collections.synchronizedList(new ArrayList<>()); + + int tasksCount = 50; + ExecutorService pool = Executors.newFixedThreadPool(tasksCount); + int portsPerTaskCount = 500; + for (int i = 0; i < tasksCount; i++) { + pool.submit(() -> { + PortUtil portUtil = new PortUtil(); + portUtils.add(portUtil); + for (int j = 0; j < portsPerTaskCount; j++) { + int nextFreePort = portUtil.getNextFreePort(); + + try { + ServerSocket ss = new ServerSocket(); + ss.bind(new InetSocketAddress("localhost", nextFreePort)); + } catch (IOException e) { + ourLog.error("Failure binding new port: " + e.toString(), e); + fail(e.toString()); + } + + ports.add(nextFreePort); + } + + }); + } + pool.shutdown(); + pool.awaitTermination(60, TimeUnit.SECONDS); + + assertEquals(tasksCount * portsPerTaskCount, ports.size()); + + while (ports.size() > 0) { + Integer nextPort = ports.remove(0); + if (ports.contains(nextPort)) { + fail("Port " + nextPort + " was given out more than once"); + } + } + + for (PortUtil next : portUtils) { + next.clear(); + } + } }