net.tomp2p.connection.ChannelCreator.java Source code

Java tutorial

Introduction

Here is the source code for net.tomp2p.connection.ChannelCreator.java

Source

/*
 * 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 io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.ChannelGroupFuture;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import net.tomp2p.futures.FutureDone;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.message.Message;
import net.tomp2p.rpc.RPC.Commands;
import net.tomp2p.utils.Pair;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Creates the channels. This class is created by
 * {@link net.tomp2p.connection.Reservation.WaitReservationPermanent} and should
 * never be called directly. With this class one can create TCP or UDP channels
 * up to a certain extend. Thus it must be know beforehand how many connections
 * will be created.
 * 
 * @author Thomas Bocek
 */
public class ChannelCreator {
    private static final Logger LOG = LoggerFactory.getLogger(ChannelCreator.class);

    private final EventLoopGroup workerGroup;
    private final ChannelGroup recipients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    private final int maxPermitsUDP;
    private final int maxPermitsTCP;

    private final Semaphore semaphoreUPD;
    private final Semaphore semaphoreTCP;

    // we should be fair, otherwise we see connection timeouts due to unfairness
    // if busy
    private final ReadWriteLock readWriteLockUDP = new ReentrantReadWriteLock(true);
    private final Lock readUDP = readWriteLockUDP.readLock();
    private final Lock writeUDP = readWriteLockUDP.writeLock();

    private final ReadWriteLock readWriteLockTCP = new ReentrantReadWriteLock(true);
    private final Lock readTCP = readWriteLockTCP.readLock();
    private final Lock writeTCP = readWriteLockTCP.writeLock();

    private final FutureDone<Void> futureChannelCreationDone;

    private final ChannelClientConfiguration channelClientConfiguration;

    private EventExecutorGroup handlerExecutor;

    private boolean shutdownUDP = false;
    private boolean shutdownTCP = false;

    /**
     * Package private constructor, since this is created by
     * {@link net.tomp2p.connection.Reservation.WaitReservationPermanent} and
     * should never be called directly.
     * 
     * @param workerGroup
     *            The worker group for netty that is shared between TCP and UDP.
     *            This worker group is not shutdown if this instance is shutdown
     * @param futureChannelCreationDone
     *            We need to set this from the outside as we want to attach
     *            listeners to it
     * @param maxPermitsUDP
     *            The number of max. parallel UDP connections.
     * @param maxPermitsTCP
     *            The number of max. parallel TCP connections.
     * @param channelClientConfiguration
     *            The configuration that contains the pipeline filter
     */
    ChannelCreator(final EventLoopGroup workerGroup, final FutureDone<Void> futureChannelCreationDone,
            final int maxPermitsUDP, final int maxPermitsTCP,
            final ChannelClientConfiguration channelClientConfiguration) {
        this.workerGroup = workerGroup;
        this.futureChannelCreationDone = futureChannelCreationDone;
        this.maxPermitsUDP = maxPermitsUDP;
        this.maxPermitsTCP = maxPermitsTCP;
        this.semaphoreUPD = new Semaphore(maxPermitsUDP);
        this.semaphoreTCP = new Semaphore(maxPermitsTCP);
        this.channelClientConfiguration = channelClientConfiguration;
    }

    /**
     * Creates a "channel" to the given address. This won't send any message
     * unlike TCP.
     * 
     * @param broadcast
     *            Sets this channel to be able to broadcast
     * @param channelHandlers
     *            The handlers to set
     * @param futureResponse
     *            The futureResponse
     * @return The channel future object or null if we are shut down
     */
    public ChannelFuture createUDP(final boolean broadcast,
            final Map<String, Pair<EventExecutorGroup, ChannelHandler>> channelHandlers,
            FutureResponse futureResponse) {
        readUDP.lock();
        try {
            if (shutdownUDP) {
                return null;
            }
            if (!semaphoreUPD.tryAcquire()) {
                LOG.error("Tried to acquire more resources (UDP) than announced! Announced {}", maxPermitsUDP);
                throw new RuntimeException("Tried to acquire more resources (UDP) than announced!");
            }
            final Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioDatagramChannel.class);
            b.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(ConnectionBean.UDP_LIMIT));
            if (broadcast) {
                b.option(ChannelOption.SO_BROADCAST, true);
            }
            Map<String, Pair<EventExecutorGroup, ChannelHandler>> channelHandlers2 = channelClientConfiguration
                    .pipelineFilter().filter(channelHandlers, false, true);
            addHandlers(b, channelHandlers2);
            // Here we need to bind, as opposed to the TCP, were we connect if
            // we do a connect, we cannot receive
            // broadcast messages
            final ChannelFuture channelFuture;
            channelFuture = b.bind(new InetSocketAddress(channelClientConfiguration.senderUDP(), 0));
            recipients.add(channelFuture.channel());
            setupCloseListener(channelFuture, semaphoreUPD, futureResponse);
            return channelFuture;
        } finally {
            readUDP.unlock();
        }
    }

    /**
     * Creates a channel to the given address. This will setup the TCP
     * connection
     * 
     * @param socketAddress
     *            The address to send future messages
     * @param connectionTimeoutMillis
     *            The timeout for establishing a TCP connection
     * @param channelHandlers
     *            The handlers to set
     * @param futureResponse
     *            the futureResponse
     * @return The channel future object or null if we are shut down.
     */
    public ChannelFuture createTCP(final SocketAddress socketAddress, final int connectionTimeoutMillis,
            final Map<String, Pair<EventExecutorGroup, ChannelHandler>> channelHandlers,
            final FutureResponse futureResponse) {
        readTCP.lock();
        try {
            if (shutdownTCP) {
                return null;
            }
            if (!semaphoreTCP.tryAcquire()) {
                LOG.error("Tried to acquire more resources (TCP) than announced!");
                throw new RuntimeException("Tried to acquire more resources (TCP) than announced!");
            }
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis);
            b.option(ChannelOption.TCP_NODELAY, true);
            b.option(ChannelOption.SO_LINGER, 0);
            b.option(ChannelOption.SO_REUSEADDR, true);
            Map<String, Pair<EventExecutorGroup, ChannelHandler>> channelHandlers2 = channelClientConfiguration
                    .pipelineFilter().filter(channelHandlers, true, true);
            addHandlers(b, channelHandlers2);

            ChannelFuture channelFuture = b.connect(socketAddress,
                    new InetSocketAddress(channelClientConfiguration.senderTCP(), 0));

            recipients.add(channelFuture.channel());
            setupCloseListener(channelFuture, semaphoreTCP, futureResponse);
            return channelFuture;
        } finally {
            readTCP.unlock();
        }
    }

    /**
     * Since we want to add multiple handlers, we need to do this with the
     * pipeline.
     * 
     * @param bootstrap
     *            The bootstrap
     * @param channelHandlers
     *            The handlers to be added.
     */
    private void addHandlers(final Bootstrap bootstrap,
            final Map<String, Pair<EventExecutorGroup, ChannelHandler>> channelHandlers) {
        bootstrap.handler(new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(final Channel ch) throws Exception {
                ch.config().setAllocator(channelClientConfiguration.byteBufAllocator());
                for (Map.Entry<String, Pair<EventExecutorGroup, ChannelHandler>> entry : channelHandlers
                        .entrySet()) {
                    if (entry.getKey().equals("handler")) {
                        handlerExecutor = entry.getValue().element0();
                    }
                    if (entry.getValue().element0() != null) {
                        ch.pipeline().addLast(entry.getValue().element0(), entry.getKey(),
                                entry.getValue().element1());
                    } else {
                        ch.pipeline().addLast(entry.getKey(), entry.getValue().element1());
                    }
                }
            }
        });
    }

    /**
     * When a channel is closed, the semaphore is released an other channel can
     * be created. Also the lock for the channel creating is being released.
     * This means that the channelCreator can be shutdown.
     * 
     * @param channelFuture
     *            The channel future
     * @param semaphore
     *            The semaphore to decrease
     * @param futureResponse
     *            The future response
     * 
     * @return The same future that was passed as an argument
     */
    private ChannelFuture setupCloseListener(final ChannelFuture channelFuture, final Semaphore semaphore,
            final FutureResponse futureResponse) {
        channelFuture.channel().closeFuture().addListener(new GenericFutureListener<ChannelFuture>() {
            @Override
            public void operationComplete(final ChannelFuture future) throws Exception {
                // it is important that the release of the semaphore and the set
                // of the future happen sequentially. If this is run in this
                // thread it will be a netty thread, and this is not what the
                // user may have wanted. The future response should be executed
                // in the thread of the handler.
                Runnable runner = new Runnable() {
                    @Override
                    public void run() {
                        semaphore.release();

                        Message request = futureResponse.request();
                        if (request != null && futureResponse.responseMessage() == null
                                && request.recipient().isSlow() && request.command() != Commands.PING.getNr()
                                && request.command() != Commands.NEIGHBOR.getNr()) {
                            // If the request goes to a slow peer, the channel
                            // can be closed until the response arrives
                            LOG.debug("Ignoring channel close event because recipient is slow peer");
                        } else {
                            futureResponse.responseNow();
                        }
                    }
                };
                if (handlerExecutor == null) {
                    runner.run();
                } else {
                    handlerExecutor.submit(runner);
                }
            }
        });
        return channelFuture;
    }

    /**
     * Setup the close listener for a channel that was already created
     * 
     * @param channelFuture
     *            The channel future
     * @param futureResponse
     *            The future response
     * @return The same future that was passed as an argument
     */
    public ChannelFuture setupCloseListener(final ChannelFuture channelFuture,
            final FutureResponse futureResponse) {
        channelFuture.channel().closeFuture().addListener(new GenericFutureListener<ChannelFuture>() {
            @Override
            public void operationComplete(final ChannelFuture future) throws Exception {
                futureResponse.responseNow();
            }
        });
        return channelFuture;
    }

    public boolean isShutdown() {
        return shutdownTCP || shutdownUDP;
    }

    /**
     * Shutdown this channel creator. This means that no TCP or UDP connection
     * can be established.
     * 
     * @return The shutdown future.
     */
    public FutureDone<Void> shutdown() {
        // set shutdown flag for UDP and TCP, if we acquire a write lock, all
        // read locks are blocked as well
        writeUDP.lock();
        writeTCP.lock();
        try {
            if (shutdownTCP || shutdownUDP) {
                shutdownFuture().failed("already shutting down");
                return shutdownFuture();
            }
            shutdownUDP = true;
            shutdownTCP = true;
        } finally {
            writeTCP.unlock();
            writeUDP.unlock();
        }

        recipients.close().addListener(new GenericFutureListener<ChannelGroupFuture>() {
            @Override
            public void operationComplete(final ChannelGroupFuture future) throws Exception {
                // we can block here as we block in GlobalEventExecutor.INSTANCE
                semaphoreUPD.acquireUninterruptibly(maxPermitsUDP);
                semaphoreTCP.acquireUninterruptibly(maxPermitsTCP);
                shutdownFuture().done();
            }
        });

        return shutdownFuture();
    }

    /**
     * @return The shutdown future that is used when calling {@link #shutdown()}
     */
    public FutureDone<Void> shutdownFuture() {
        return futureChannelCreationDone;
    }

    public int availableUDPPermits() {
        return semaphoreUPD.availablePermits();
    }

    public int availableTCPPermits() {
        return semaphoreTCP.availablePermits();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("sem-udp:");
        sb.append(semaphoreUPD.availablePermits());
        sb.append(",sem-tcp:");
        sb.append(semaphoreTCP.availablePermits());
        sb.append(",addrUDP:");
        sb.append(semaphoreUPD);
        return sb.toString();
    }
}