/* * 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.connection; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.channel.EventLoopGroup; import net.tomp2p.futures.BaseFutureAdapter; import net.tomp2p.futures.FutureChannelCreator; import net.tomp2p.futures.FutureDone; import net.tomp2p.p2p.RequestConfiguration; import net.tomp2p.p2p.RoutingConfiguration; /** * Reserves a block of connections. * * @author Thomas Bocek * */ public class BulkReservation { private static final Logger LOG = LoggerFactory.getLogger(BulkReservation.class); private final int maxPermitsUDP; private final int maxPermitsTCP; private final Semaphore semaphoreUPD; private final Semaphore semaphoreTCP; private final ChannelClientConfiguration channelClientConfiguration; private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(); // single thread private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, queue); private final EventLoopGroup workerGroup; private final PeerBean peerBean; // 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 contained in the provided configuration. * * @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 BulkReservation(final EventLoopGroup workerGroup, final ChannelClientConfiguration channelClientConfiguration, final PeerBean peerBean) { this.workerGroup = workerGroup; this.maxPermitsUDP = channelClientConfiguration.maxPermitsUDP(); this.maxPermitsTCP = channelClientConfiguration.maxPermitsTCP(); this.semaphoreUPD = new Semaphore(maxPermitsUDP); this.semaphoreTCP = new Semaphore(maxPermitsTCP); this.channelClientConfiguration = channelClientConfiguration; this.peerBean = peerBean; } public int availablePermitsUDP() { return semaphoreUPD.availablePermits(); } public int availablePermitsTCP() { return semaphoreTCP.availablePermits(); } /** * @return The pending number of requests that are scheduled but not * executed yet. */ public int pendingRequests() { return queue.size(); } /** * Calculates the number of required connections 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 UDP * @return The future channel creator */ public FutureChannelCreator create(final RoutingConfiguration routingConfiguration, final RequestConfiguration requestP2PConfiguration, final DefaultConnectionConfiguration builder) { if (routingConfiguration == null && requestP2PConfiguration == null) { throw new IllegalArgumentException("Both routing configuration and request configuration must be set."); } int nrConnectionsTCP = 0; int nrConnectionsUDP = 0; if (requestP2PConfiguration != null) { if (builder.isForceUDP()) { nrConnectionsUDP = requestP2PConfiguration.parallel(); } else { nrConnectionsTCP = requestP2PConfiguration.parallel(); } } if (routingConfiguration != null) { if (!builder.isForceTCP()) { nrConnectionsUDP = Math.max(nrConnectionsUDP, routingConfiguration.parallel()); } else { nrConnectionsTCP = Math.max(nrConnectionsTCP, routingConfiguration.parallel()); } } LOG.debug("Reservation UDP={}, TCP={}", nrConnectionsUDP, nrConnectionsTCP); return create(nrConnectionsUDP, nrConnectionsTCP); } public FutureChannelCreator createTCP(final int permitsTCP) { return create(0, permitsTCP); } public FutureChannelCreator createUDP(final int permitsUDP) { return create(permitsUDP, 0); } /** * Creates a channel creator for short-lived connections. Always call * {@link ChannelCreator#shutdown()} to release all resources. This needs to * be done in any case, whether FutureChannelCreator returns failed or * success! * * @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 permitsUDPUnadjusted, final int permitsTCP) { //adjust values //for each TCP connection we need 1 udp for possible RCON, and 3 for HOLEPUNCHING final int permitsUDP = permitsUDPUnadjusted + (permitsTCP * 3); LOG.debug("create permits, udp: {}, tcp:{}", permitsUDP, permitsTCP); if (permitsUDP > maxPermitsUDP) { throw new IllegalArgumentException(String.format("Cannot acquire more UDP connections (%s) than maximally allowed (%s).", permitsUDP, maxPermitsUDP)); } if (permitsTCP > maxPermitsTCP) { throw new IllegalArgumentException(String.format("Cannot acquire more TCP connections (%s) than maximally allowed (%s).", permitsTCP, maxPermitsTCP)); } final FutureChannelCreator futureChannelCreator = new FutureChannelCreator(); read.lock(); try { if (shutdown) { return futureChannelCreator.failed("Shutting down."); } FutureDone<Void> futureChannelCreationShutdown = new FutureDone<Void>(); futureChannelCreationShutdown.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 inconsistencies LOG.debug("release permits, udp: {}, tcp:{}", permitsUDP, permitsTCP); semaphoreUPD.release(permitsUDP); semaphoreTCP.release(permitsTCP); } }); executor.execute(new WaitReservation(futureChannelCreator, futureChannelCreationShutdown, permitsUDP, permitsTCP)); return futureChannelCreator; } finally { read.unlock(); } } /** * Shuts down all the channel creators. * * @return The future when the shutdown is complete */ public FutureDone<Void> shutdown() { write.lock(); try { if (shutdown) { return futureReservationDone.failed("Already shutting down"); } shutdown = true; } finally { write.unlock(); } // Fast shutdown for those that are in the queue is not required. // 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()) { WaitReservation wr = (WaitReservation) r; wr.futureChannelCreator().failed("Shutting down."); } final Collection<ChannelCreator> copyChannelCreators; synchronized (channelCreators) { copyChannelCreators = new ArrayList<ChannelCreator>(channelCreators); } // the channelCreator does not change anymore from here on final int size = copyChannelCreators.size(); if (size == 0) { futureReservationDone.done(); } else { final AtomicInteger completeCounter = new AtomicInteger(0); for (final ChannelCreator channelCreator : copyChannelCreators) { // 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 semaphore.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) { // we can block here as we block in // GlobalEventExecutor.INSTANCE semaphoreUPD.acquireUninterruptibly(maxPermitsUDP); semaphoreTCP.acquireUninterruptibly(maxPermitsTCP); futureReservationDone.done(); } } }); channelCreator.shutdown(); } } // wait for completion return futureReservationDone; } /** * Adds a channel creator to the set and also adds it to the shutdown listener. * * @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 { channelCreators.remove(channelCreator); } }); channelCreators.add(channelCreator); } /** * Tries to reserve a channel creator. If too many channels 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 ChannelCreator#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.failed("shutting down"); return; } try { semaphoreUPD.acquire(permitsUDP); } catch (InterruptedException e) { futureChannelCreator.failed(e); return; } try { semaphoreTCP.acquire(permitsTCP); } catch (InterruptedException e) { semaphoreUPD.release(permitsUDP); futureChannelCreator.failed(e); return; } final InetAddress fromAddress; if(channelClientConfiguration.fromAddress() != null) { fromAddress = channelClientConfiguration.fromAddress(); } else if(peerBean.serverPeerAddress() == null) { fromAddress = Inet4Address.getByAddress(new byte[4]); } else if(peerBean.serverPeerAddress().net4Internal()) { fromAddress = peerBean.serverPeerAddress().ipInternalSocket().ipv4().toInetAddress(); } else { fromAddress = peerBean.serverPeerAddress().ipv4Socket().ipv4().toInetAddress(); } LOG.debug("channel from {} upd:{}, tcp:{}. Remaining UDP: {}, TCP: {}", fromAddress, permitsUDP, permitsTCP, semaphoreUPD.availablePermits(), semaphoreTCP.availablePermits()); channelCreator = new ChannelCreator(workerGroup, futureChannelCreationShutdown, permitsUDP, permitsTCP, channelClientConfiguration, fromAddress); addToSet(channelCreator); } catch (UnknownHostException u) { //never happens as we use wildcard address u.printStackTrace(); throw new RuntimeException(u); } finally { read.unlock(); } futureChannelCreator.reserved(channelCreator); } private FutureChannelCreator futureChannelCreator() { return futureChannelCreator; } } }