blob: 9144756402c3ed29dfcd772b218c70066914dc9a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.tomee.server.composer;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Will allocate a block of ports as an iterable. The ports are held open
* and reserved until the time the user calls `next()` on the iterable.
*
* After the user calls `next()` the port stays in a reserved status for
* one minute and will not be issued again by this class within that time.
*
* Ports returned are guaranteed to be unique with no duplicates.
*
* It is important to note once the user calls `next()` the port is released
* and nothing stops an external process from grabbing the port. The one minute
* reserved status is only effective for code in this VM using this utility.
*/
public class Ports {
private static final List<Port> allocated = new CopyOnWriteArrayList<>();
private Ports() {
}
/**
* Allocates a single port which is immediately released
* and available.
*/
public static int allocate() {
return allocate(1).iterator().next();
}
/**
* Allocates N ports all of which are open and being held
* and not available until the iterable is consumed.
*/
public static Iterable<Integer> allocate(final int count) {
// Trim any old ports from the list
allocated.removeIf(Port::isOld);
// Allocate new ports
final List<Port> ports = Stream.generate(Port::new)
.filter(Ports::isNotReserved)
.limit(count)
.collect(Collectors.toList());
// Add them to the allocated list so we don't issue them
// again for at least a minute
allocated.addAll(ports);
return () -> new Iterator<Integer>() {
@Override
public boolean hasNext() {
return ports.size() > 0;
}
@Override
public Integer next() {
final Port port = ports.remove(0);
// A port's "age" starts right now
//
// The person who called this method
// now has one minute to consume the port
return port.release();
}
};
}
/**
* If this port is in the allocated list, we must immediately
* release the port and not include it in the new list
*/
private static boolean isNotReserved(Port port) {
if (allocated.contains(port)) {
port.release();
return false;
}
return true;
}
static class Port {
private final ServerSocket serverSocket;
private final int port;
private volatile long closed;
private long tolerance;
Port() {
this(TimeUnit.MINUTES.toNanos(1));
}
Port(final long nanoseconds) {
this.tolerance = nanoseconds;
try {
// When the system is out of ports the following exception will be thrown and caught here
// java.net.SocketException: Too many open files in system
this.serverSocket = new ServerSocket(0);
this.port = this.serverSocket.getLocalPort();
this.closed = 0;
} catch (final IOException e) {
throw new IllegalStateException("Unable to create a server socket with random port", e);
}
}
/**
* If this port has been released more than a minute ago, it is old
* and should be removed from the list
*/
public boolean isOld() {
return closed != 0 && closed < System.nanoTime();
}
public int get() {
return port;
}
public int release() {
if (serverSocket.isClosed()) throw new IllegalStateException("Port has already been consumed");
final int port = serverSocket.getLocalPort();
try {
serverSocket.close();
this.closed = System.nanoTime() + tolerance;
return port;
} catch (IOException e) {
throw new IllegalStateException("Unable to close server socket and free port " + port, e);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Port port1 = (Port) o;
if (port != port1.port) return false;
return true;
}
@Override
public int hashCode() {
return port;
}
}
}