Improvements to PortUtil
This commit is contained in:
parent
6ff92abecc
commit
c482d22cff
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.util;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -20,7 +20,6 @@ package ca.uhn.fhir.util;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -28,21 +27,137 @@ import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
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
|
@CoverageIgnore
|
||||||
public class PortUtil {
|
public class PortUtil {
|
||||||
|
public static final int SPACE_SIZE = 100;
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
|
||||||
|
private static final PortUtil INSTANCE = new PortUtil();
|
||||||
|
private List<ServerSocket> myControlSockets = new ArrayList<>();
|
||||||
|
private Integer myCurrentControlSocketPort = null;
|
||||||
|
private int myCurrentOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Non instantiable
|
* Non instantiable
|
||||||
*/
|
*/
|
||||||
private PortUtil() {
|
public PortUtil() {
|
||||||
// nothing
|
// 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
|
* 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.
|
* 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.
|
* so it can be reused across modules. Use with caution.
|
||||||
*/
|
*/
|
||||||
public static int findFreePort() {
|
public static int findFreePort() {
|
||||||
ServerSocket server;
|
return INSTANCE.getNextFreePort();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
package ca.uhn.fhir.util;
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
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 {
|
public class PortUtilTest {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(PortUtilTest.class);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFindFreePort() throws IOException {
|
public void testFindFreePort() throws IOException {
|
||||||
int port = PortUtil.findFreePort();
|
int port = PortUtil.findFreePort();
|
||||||
|
@ -28,4 +39,49 @@ public class PortUtilTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPortsAreNotReused() throws InterruptedException {
|
||||||
|
|
||||||
|
List<Integer> ports = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
List<PortUtil> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue