// This file is part of OpenTSDB.
// Copyright (C) 2010-2012 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelException;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.handler.codec.embedder.CodecEmbedderException;
import net.opentsdb.stats.StatsCollector;
/**
* Keeps track of all existing connections.
*/
final class ConnectionManager extends SimpleChannelHandler {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionManager.class);
private static final AtomicLong connections_established = new AtomicLong();
private static final AtomicLong connections_rejected = new AtomicLong();
private static final AtomicLong exceptions_unknown = new AtomicLong();
private static final AtomicLong exceptions_closed = new AtomicLong();
private static final AtomicLong exceptions_reset = new AtomicLong();
private static final AtomicLong exceptions_timeout = new AtomicLong();
/** Max connections can be serviced by tsd, if over limit, tsd will refuse
* new connections. */
private final int connections_limit;
/** A counter used for determining how many channels are open. Something odd
* happens with the DefaultChannelGroup in that .size() doesn't return the
* actual number of open connections. TODO - find out why. */
private final AtomicInteger open_connections = new AtomicInteger();
private static final DefaultChannelGroup channels =
new DefaultChannelGroup("all-channels");
static void closeAllConnections() {
channels.close().awaitUninterruptibly();
}
/**
* Default Ctor with no concurrent connection limit.
*/
public ConnectionManager() {
connections_limit = 0;
}
/**
* CTor for setting a limit on concurrent connections.
* @param connections_limit The maximum number of concurrent connections allowed.
* @since 2.3
*/
public ConnectionManager(final int connections_limit) {
LOG.info("TSD concurrent connection limit set to: " + connections_limit);
this.connections_limit = connections_limit;
}
/**
* Collects the stats and metrics tracked by this instance.
* @param collector The collector to use.
*/
public static void collectStats(final StatsCollector collector) {
collector.record("connectionmgr.connections", channels.size(), "type=open");
collector.record("connectionmgr.connections", connections_rejected,
"type=rejected");
collector.record("connectionmgr.connections", connections_established,
"type=total");
collector.record("connectionmgr.exceptions", exceptions_closed,
"type=closed");
collector.record("connectionmgr.exceptions", exceptions_reset,
"type=reset");
collector.record("connectionmgr.exceptions", exceptions_timeout,
"type=timeout");
collector.record("connectionmgr.exceptions", exceptions_unknown,
"type=unknown");
}
@Override
public void channelOpen(final ChannelHandlerContext ctx,
final ChannelStateEvent e) throws IOException {
if (connections_limit > 0) {
final int channel_size = open_connections.incrementAndGet();
if (channel_size > connections_limit) {
throw new ConnectionRefusedException("Channel size (" + channel_size + ") exceeds total "
+ "connection limit (" + connections_limit + ")");
// exceptionCaught will close the connection and increment the counter.
}
}
channels.add(e.getChannel());
connections_established.incrementAndGet();
}
@Override
public void channelClosed(final ChannelHandlerContext ctx,
final ChannelStateEvent e) throws IOException {
open_connections.decrementAndGet();
}
@Override
public void handleUpstream(final ChannelHandlerContext ctx,
final ChannelEvent e) throws Exception {
if (e instanceof ChannelStateEvent) {
LOG.info(e.toString());
}
super.handleUpstream(ctx, e);
}
@Override
public void exceptionCaught(final ChannelHandlerContext ctx,
final ExceptionEvent e) {
final Throwable cause = e.getCause();
final Channel chan = ctx.getChannel();
if (cause instanceof ClosedChannelException) {
exceptions_closed.incrementAndGet();
LOG.warn("Attempt to write to closed channel " + chan);
return;
}
if (cause instanceof IOException) {
final String message = cause.getMessage();
if ("Connection reset by peer".equals(message)) {
exceptions_reset.incrementAndGet();
return;
} else if ("Connection timed out".equals(message)) {
exceptions_timeout.incrementAndGet();
// Do nothing. A client disconnecting isn't really our problem. Oh,
// and I'm not kidding you, there's no better way to detect ECONNRESET
// in Java. Like, people have been bitching about errno for years,
// and Java managed to do something *far* worse. That's quite a feat.
return;
} else if (cause instanceof ConnectionRefusedException) {
connections_rejected.incrementAndGet();
if (LOG.isDebugEnabled()) {
LOG.debug("Refusing connection from " + chan, e.getCause());
}
chan.close();
return;
}
}
if (cause instanceof CodecEmbedderException) {
// payload was not compressed as it was announced to be
LOG.warn("Http codec error : " + cause.getMessage());
e.getChannel().close();
return;
}
exceptions_unknown.incrementAndGet();
LOG.error("Unexpected exception from downstream for " + chan, cause);
e.getChannel().close();
}
/** Simple exception for refusing a connection. */
private static class ConnectionRefusedException extends ChannelException {
/**
* Default ctor with a message.
* @param message A descriptive message for the exception.
*/
public ConnectionRefusedException(final String message) {
super(message);
}
private static final long serialVersionUID = 5348377149312597939L;
}
}