/*
* Copyright 2013 Thomas Bocek
*
* 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 net.tomp2p.connection2;
import io.netty.channel.EventLoopGroup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import net.tomp2p.futures.BaseFutureAdapter;
import net.tomp2p.futures.FutureChannelCreator;
import net.tomp2p.futures.FutureDone;
import net.tomp2p.p2p.RequestP2PConfiguration;
import net.tomp2p.p2p.RoutingConfiguration;
import net.tomp2p.p2p.builder.DHTBuilder;
/**
* Reserves a block of connections.
*
* @author Thomas Bocek
*
*/
public class Reservation {
private final int maxPermitsUDP;
private final int maxPermitsTCP;
private final int maxPermitsPermanentTCP;
private final Semaphore semaphoreUPD;
private final Semaphore semaphoreTCP;
private final Semaphore semaphorePermanentTCP;
private final ChannelClientConfiguration channelClientConfiguration;
private final ExecutorService executor;
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
private final EventLoopGroup workerGroup;
// we should be fair, otherwise we see connection timeouts due to unfairness if busy
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();
private boolean shutdown = false;
private final Collection<ChannelCreator> channelCreators = Collections
.synchronizedList(new ArrayList<ChannelCreator>());
private final FutureDone<Void> futureReservationDone = new FutureDone<Void>();
/**
* Creates a new reservation class with the 3 permits.
*
* @param workerGroup
* The worker group for both UDP and TCP channels. This will not be shutdown in this class, you need to
* shutdown it outside.
* @param channelClientConfiguration
* Sets maxPermitsUDP: the number of maximum short-lived UDP connections, maxPermitsTCP: the number of
* maximum short-lived TCP connections, maxPermitsPermanentTCP: the number of maximum permanent TCP
* connections
*/
public Reservation(final EventLoopGroup workerGroup,
final ChannelClientConfiguration channelClientConfiguration) {
// single thread
this.executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, queue);
this.workerGroup = workerGroup;
this.maxPermitsUDP = channelClientConfiguration.maxPermitsUDP();
this.maxPermitsTCP = channelClientConfiguration.maxPermitsTCP();
this.maxPermitsPermanentTCP = channelClientConfiguration.maxPermitsPermanentTCP();
this.semaphoreUPD = new Semaphore(maxPermitsUDP);
this.semaphoreTCP = new Semaphore(maxPermitsTCP);
this.semaphorePermanentTCP = new Semaphore(maxPermitsPermanentTCP);
this.channelClientConfiguration = channelClientConfiguration;
}
/**
* @return The pending number of requests that are scheduled but not executed yet.
*/
public int pendingRequests() {
return queue.size();
}
/**
* This will calculate the number of required connection for routing and request messages.
*
* @param routingConfiguration
* Contains the number of routing requests in parallel
* @param requestP2PConfiguration
* Contains the number of requests for P2P operations in parallel
* @param builder
* The builder that tells us if we should use TCP or UPD
* @return The future channel creator
*/
public FutureChannelCreator create(final RoutingConfiguration routingConfiguration,
final RequestP2PConfiguration requestP2PConfiguration, final DHTBuilder<?> builder) {
if (routingConfiguration == null && requestP2PConfiguration == null) {
throw new IllegalArgumentException(
"Both routingConfiguration and requestP2PConfiguration cannot be null");
}
int nrConnectionsTCP = 0;
int nrConnectionsUDP = 0;
if (requestP2PConfiguration != null) {
if (builder.isForceUDP()) {
nrConnectionsUDP = requestP2PConfiguration.getParallel();
} else {
nrConnectionsTCP = requestP2PConfiguration.getParallel();
}
}
if (routingConfiguration != null) {
if (!builder.isForceTCP()) {
nrConnectionsUDP = Math.max(nrConnectionsUDP, routingConfiguration.getParallel());
} else {
nrConnectionsTCP = Math.max(nrConnectionsTCP, routingConfiguration.getParallel());
}
}
return create(nrConnectionsUDP, nrConnectionsTCP);
}
/**
* Create a connection creator for short-lived connections.
*
* @param permitsUDP
* The number of short-lived UDP connections
* @param permitsTCP
* The number of short-lived TCP connections
* @return The future channel creator
*/
public FutureChannelCreator create(final int permitsUDP, final int permitsTCP) {
if (permitsUDP > maxPermitsUDP) {
throw new IllegalArgumentException("cannot aquire more UDP connections (" + permitsUDP
+ ") than maximum " + maxPermitsUDP);
}
if (permitsTCP > maxPermitsTCP) {
throw new IllegalArgumentException("cannot aquire more TCP connections (" + permitsTCP
+ ") than maximum " + maxPermitsTCP);
}
FutureChannelCreator futureChannelCreator = new FutureChannelCreator();
FutureDone<Void> futureChannelCreationDone = new FutureDone<Void>();
futureChannelCreationDone.addListener(new BaseFutureAdapter<FutureDone<Void>>() {
@Override
public void operationComplete(final FutureDone<Void> future) throws Exception {
// release the permits in all cases, otherwise we may see inconsitencies
semaphoreUPD.release(permitsUDP);
semaphoreTCP.release(permitsTCP);
}
}, false); // false is important, to be always the first listener
executor.execute(new WaitReservation(futureChannelCreator, futureChannelCreationDone, permitsUDP,
permitsTCP));
return futureChannelCreator;
}
/**
* Create a connection creator for permanent connections.
*
* @param permitsPermanentTCP
* The number of long-lived TCP connections
* @return The future channel creator
*/
public FutureChannelCreator createPermanent(final int permitsPermanentTCP) {
if (permitsPermanentTCP > maxPermitsPermanentTCP) {
throw new IllegalArgumentException("cannot aquire more TCP connections (" + permitsPermanentTCP
+ ") than maximum " + maxPermitsPermanentTCP);
}
FutureChannelCreator futureChannelCreator = new FutureChannelCreator();
FutureDone<Void> futureChannelCreationDone = new FutureDone<Void>();
futureChannelCreationDone.addListener(new BaseFutureAdapter<FutureDone<Void>>() {
@Override
public void operationComplete(final FutureDone<Void> future) throws Exception {
// release the permits in all cases, otherwise we may see inconsitencies
semaphorePermanentTCP.release(permitsPermanentTCP);
}
}, false); // false is important, to be always the first listener
executor.execute(new WaitReservationPermanent(futureChannelCreator, futureChannelCreationDone,
permitsPermanentTCP));
return futureChannelCreator;
}
/**
* Shutdown all the channel creators out there.
*
* @return The future when the shutdown is complete
*/
public FutureDone<Void> shutdown() {
write.lock();
try {
if (shutdown) {
shutdownFuture().setFailed("already shutting down");
return shutdownFuture();
}
shutdown = true;
} finally {
write.unlock();
}
// fast shutdown for those that are in the queue is not required. We could let the executor finish since the
// shutdown flag is set and the future will be set as well to "shutting down":
// for (Runnable r : executor.shutdownNow()) {
// if(r instanceof WaitReservation) {
// WaitReservation wr = (WaitReservation) r;
// wr.futureChannelCreator().setFailed("shutting down");
// } else {
// WaitReservationPermanent wr = (WaitReservationPermanent) r;
// wr.futureChannelCreator().setFailed("shutting down");
// }
// }
// the channelCreator does not change anymore from here on
final int size = channelCreators.size();
if (size == 0) {
complete();
} else {
final AtomicInteger completeCounter = new AtomicInteger(0);
for (final ChannelCreator channelCreator : channelCreators) {
// this is very important that we set first the listener and then call shutdown. Otherwise, the order of
// the listener calls is not guaranteed and we may call this listener before the semphore.release,
// causing an exception.
channelCreator.shutdownFuture().addListener(new BaseFutureAdapter<FutureDone<Void>>() {
@Override
public void operationComplete(final FutureDone<Void> future) throws Exception {
if (completeCounter.incrementAndGet() == size) {
complete();
}
}
});
channelCreator.shutdown();
}
}
return shutdownFuture();
}
/**
* Drain all semaphores and set the future to done.
*/
private void complete() {
if (!semaphoreUPD.tryAcquire(maxPermitsUDP)) {
throw new RuntimeException("Cannot shutdown, as connections (UDP) are still alive: "
+ semaphoreUPD);
}
if (!semaphoreTCP.tryAcquire(maxPermitsTCP)) {
throw new RuntimeException("Cannot shutdown, as connections (TCP) are still alive: "
+ semaphoreTCP);
}
if (!semaphorePermanentTCP.tryAcquire(maxPermitsPermanentTCP)) {
throw new RuntimeException("Cannot shutdown, as connections (pTCP) are still alive: "
+ semaphorePermanentTCP);
}
futureReservationDone.setDone();
}
/**
* @return The shutdown future that is used when calling {@link #shutdown()}
*/
public FutureDone<Void> shutdownFuture() {
return futureReservationDone;
}
/**
* Adds a channel creator to the set and also adds it the the shutdownlistener.
*
* @param channelCreator
* The channel creator
*/
private void addToSet(final ChannelCreator channelCreator) {
channelCreator.shutdownFuture().addListener(new BaseFutureAdapter<FutureDone<Void>>() {
@Override
public void operationComplete(final FutureDone<Void> future) throws Exception {
read.lock();
try {
if (shutdown) {
return;
}
channelCreators.remove(channelCreator);
} finally {
read.unlock();
}
}
});
channelCreators.add(channelCreator);
}
/**
* Tries to reserve a channel creator. If too many channel already created, wait until channels are closed. This
* waiter is for the short-lived connections.
*
* @author Thomas Bocek
*
*/
private class WaitReservation implements Runnable {
private final FutureChannelCreator futureChannelCreator;
private final FutureDone<Void> futureChannelCreationShutdown;
private final int permitsUDP;
private final int permitsTCP;
/**
* Creates a reservation that returns a {@link ChannelCreator} in a future once we have the semaphore.
*
* @param futureChannelCreator
* The status of the creating
* @param futureChannelCreationShutdown
* The {@link ChannelCreator} shutdown feature needs to be passed since we need it for
* {@link Reservation#shutdown()}.
* @param permitsUDP
* The number of permits for UDP
* @param permitsTCP
* The number of permits for TCP
*/
public WaitReservation(final FutureChannelCreator futureChannelCreator,
final FutureDone<Void> futureChannelCreationShutdown, final int permitsUDP,
final int permitsTCP) {
this.futureChannelCreator = futureChannelCreator;
this.futureChannelCreationShutdown = futureChannelCreationShutdown;
this.permitsUDP = permitsUDP;
this.permitsTCP = permitsTCP;
}
@Override
public void run() {
final ChannelCreator channelCreator;
read.lock();
try {
if (shutdown) {
futureChannelCreator.setFailed("shutting down");
return;
}
try {
semaphoreUPD.acquire(permitsUDP);
} catch (InterruptedException e) {
futureChannelCreator.setFailed(e);
return;
}
try {
semaphoreTCP.acquire(permitsTCP);
} catch (InterruptedException e) {
semaphoreUPD.release(permitsUDP);
futureChannelCreator.setFailed(e);
return;
}
channelCreator = new ChannelCreator(workerGroup, futureChannelCreationShutdown, permitsUDP,
permitsTCP, channelClientConfiguration);
addToSet(channelCreator);
} finally {
read.unlock();
}
futureChannelCreator.reserved(channelCreator);
}
}
/**
* Tries to reserve a channel creator. If too many channel already created, wait until channels are closed. This
* waiter is for the long-lived connections.
*
* @author Thomas Bocek
*
*/
private final class WaitReservationPermanent implements Runnable {
private final FutureChannelCreator futureChannelCreator;
private final FutureDone<Void> futureChannelCreationShutdown;
private final int permitsPermanentTCP;
/**
* Creates a reservation that returns a {@link ChannelCreator} in a future once we have the semaphore.
*
* @param futureChannelCreator
* The status of the creating
* @param futureChannelCreationShutdown
* The {@link ChannelCreator} shutdown feature needs to be passed since we need it for
* {@link Reservation#shutdown()}.
* @param permitsPermanentTCP
* The number of permits
*/
private WaitReservationPermanent(final FutureChannelCreator futureChannelCreator,
final FutureDone<Void> futureChannelCreationShutdown, final int permitsPermanentTCP) {
this.futureChannelCreator = futureChannelCreator;
this.futureChannelCreationShutdown = futureChannelCreationShutdown;
this.permitsPermanentTCP = permitsPermanentTCP;
}
@Override
public void run() {
ChannelCreator channelCreator;
read.lock();
try {
if (shutdown) {
futureChannelCreator.setFailed("shutting down");
return;
}
try {
semaphorePermanentTCP.acquire(permitsPermanentTCP);
} catch (InterruptedException e) {
futureChannelCreator.setFailed(e);
return;
}
channelCreator = new ChannelCreator(workerGroup, futureChannelCreationShutdown, 0,
permitsPermanentTCP, channelClientConfiguration);
addToSet(channelCreator);
} finally {
read.unlock();
}
futureChannelCreator.reserved(channelCreator);
}
}
}