/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.destination.netty;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.helios.apmrouter.destination.BaseDestination;
import org.helios.apmrouter.metric.IMetric;
import org.jboss.netty.bootstrap.Bootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.handler.logging.LoggingHandler;
import org.jboss.netty.logging.InternalLogLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.support.MetricType;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* <p>Title: NettyDestination</p>
* <p>Description: Base netty destination</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.destination.NettyDestination</code></p>
*/
public abstract class NettyDestination extends BaseDestination implements ChannelPipelineFactory {
/** The nety worker pool */
protected ExecutorService workerPool;
/** The client bootstrap */
protected Bootstrap bstrap;
/** The client channel factory */
protected ChannelFactory channelFactory;
/** The channel options */
protected Map<String, Object> channelOptions = new HashMap<String, Object>();
/** The endpoint server host name or IP address */
protected String host = null;
/** The port that the endpoint server is listening on */
protected int port = -1;
/** The channel pipeline initial handler bean names */
protected final SortedMap<Integer, String> channelHandlers = new ConcurrentSkipListMap<Integer, String>();
/** The socket address this listener is bound to */
protected InetSocketAddress socketAddress = null;
/** The resolved channel handlers in insertion order */
protected LinkedHashMap<String, ChannelHandler> resolvedHandlers = new LinkedHashMap<String, ChannelHandler>();
/** Indicates if a logging handler is installed in the pipelines created for this listener */
protected final AtomicBoolean loggingHandlerInstalled = new AtomicBoolean(false);
/** The relative location of the logging handler */
protected String loggingHandlerLocation = null;
/** The managed channel group */
protected ChannelGroup channelGroup = null;
/** The main forwarding connection channel */
protected Channel channel = null;
/** The main forwarding connection channel close future */
protected ChannelFuture closeFuture = null;
/** Indicates if the main graphite client channel is currently connected */
protected final AtomicBoolean connected = new AtomicBoolean(false);
/** Indicates if a disconnect is expected. (If a disconnect occures and this is false, a reconnect thread will start) */
protected final AtomicBoolean expectDisconnect = new AtomicBoolean(false);
/** Indicates if the reconnect loop is running */
protected final AtomicBoolean reconnecting = new AtomicBoolean(false);
/** The frequency in ms. of the reconnect loop attempts */
protected long reconnectPeriod = 10000;
/** The number of consecutive failed channel initialization attempts */
protected final AtomicInteger failedConnects = new AtomicInteger(0);
/** The injected task scheduler */
protected ThreadPoolTaskScheduler scheduler = null;
/** The reconnect loop scheduling handle */
protected ScheduledFuture<?> reconnectScheduleHandle = null;
/**
* Creates a new NettyDestination
* @param patterns the patterns this destination accepts
*/
public NettyDestination(String... patterns) {
super(patterns);
}
/**
* Creates a new NettyDestination
* @param patterns the patterns this destination accepts
*/
public NettyDestination(Collection<String> patterns) {
super(patterns);
}
/**
* Creates a new NettyDestination
*/
public NettyDestination() {
}
/**
* Starts this listener
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStart()
*/
@Override
protected void doStart() throws Exception {
info("Resolving Channel Handlers");
resolvedHandlers.clear();
for(Map.Entry<Integer, String> entry: channelHandlers.entrySet()) {
ChannelHandler handler = applicationContext.getBean(entry.getValue(), ChannelHandler.class);
debug("Resolved Channel Handler [", entry.getValue(), "]");
resolvedHandlers.put(beanName, handler);
}
info("Resolved [", resolvedHandlers.size(), "] Channel Handlers");
socketAddress = new InetSocketAddress(host, port);
info("Socket Address:", socketAddress);
channelGroup = new DefaultChannelGroup(beanName);
channelFactory = buildChannelFactory();
bstrap = buildBootstrap();
bstrap.setOptions(channelOptions);
bstrap.setPipelineFactory(this);
doConnect();
}
/**
* <p>Initiates an asynch connect to the graphite server.
* <p>When successful, the following are automatically done by this base class:<ul>
* <li>Sets the {@link #channel} to the newly initialized channel</li>
* <li>Sets the {@link #closeFuture} to the newly initialized channel's close future</li>
* <li>The {@link #connected} flag is set to true</li>
* <li>The {@link #closeFuture} is set for the connected channel</li>
* <li>Resets the {@link #failedConnects} counter to zero</li>
* <li>If the {@link #reconnectScheduleHandle} is not null, meaning a reconnect loop was in progress, it will be cleared
* and the {@link #reconnecting} flag is set to false.</li>
* </ul></p>
* <p>When fails, the following are automatically done by this base class:<ul>
* <li>Starts the reconnect loop using {@link #startReconnectLoop()} which will populate the {@link #reconnectScheduleHandle} field.</li>
* <li>Sets the {@link #reconnecting} flag to true</li>
* <li>Increments the {@link #failedConnects} counter</li>
* <li>Sets the {@link #channel} to null</li>
* <li>Sets the {@link #closeFuture} to null</li>
* </il></p>
*/
protected void doConnect() {
if(connected.get()) return;
initializeChannel().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if(f.isSuccess()) {
channel = f.getChannel();
connected.set(true);
closeFuture = f.getChannel().getCloseFuture();
failedConnects.set(0);
if(reconnectScheduleHandle!=null) {
reconnectScheduleHandle.cancel(true);
reconnectScheduleHandle = null;
reconnecting.set(false);
}
onConnect(f);
} else {
if(onConnectFailed(f)) {
startReconnectLoop();
}
}
}
});
}
/**
* Handles a disconnect
*/
protected void doDisconnect() {
connected.set(false);
if(expectDisconnect.get()) {
channel = null;
closeFuture = null;
error("Unexpected disconnect from [", socketAddress , "]");
startReconnectLoop();
} else {
info("Disconnected from [", socketAddress , "]");
}
}
/**
* Schdules a repeating reconnect task at a rate of {@link #reconnectPeriod}.
* Schedule will be cancelled when this component is stopped or a successful connect occurs.
*/
protected void startReconnectLoop() {
if(!reconnecting.compareAndSet(false, true)) return;
reconnectScheduleHandle = scheduler.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
doConnect();
}
}, reconnectPeriod);
}
/**
* {@inheritDoc}
* @see org.jboss.netty.channel.ChannelPipelineFactory#getPipeline()
*/
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
LinkedHashMap<String, ChannelHandler> tmpHandlers = null;
synchronized(resolvedHandlers) {
tmpHandlers = new LinkedHashMap<String, ChannelHandler>(resolvedHandlers);
}
for(Map.Entry<String, ChannelHandler> entry: tmpHandlers.entrySet()) {
pipeline.addLast(entry.getKey(), entry.getValue());
}
return pipeline;
}
/**
* Callback from the main channel initialization when connect succeeds
* @param connectFuture The channel future returned from a successful channel init.
*/
protected void onConnect(ChannelFuture connectFuture) {
}
/**
* Callback from the main channel initialization when connect succeeds
* @param failedConnectFuture The channel future returned from a failed channel init.
* @return true to start a reconnect loop, false to do nothing
*/
protected boolean onConnectFailed(ChannelFuture failedConnectFuture) {
return true;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStop()
*/
@Override
protected void doStop() {
if(reconnectScheduleHandle!=null) {
reconnectScheduleHandle.cancel(true);
reconnectScheduleHandle = null;
}
channelGroup.close().awaitUninterruptibly();
channelFactory.releaseExternalResources();
channelFactory = null;
resolvedHandlers.clear();
socketAddress = null;
channelGroup = null;
}
/**
* Returns the number of metrics forwarded to Graphite
* @return the number of metrics forwarded to Graphite
*/
@ManagedMetric(category="Graphite", metricType=MetricType.COUNTER, description="the number of metrics forwarded to Graphite")
public long getMetricsForwarded() {
return getMetricValue("MetricsForwarded");
}
/**
* Returns the number of metrics forwarded to Graphite in the last flush
* @return the number of metrics forwarded to Graphite in the last flush
*/
@ManagedMetric(category="Graphite", metricType=MetricType.COUNTER, description="the number of metrics forwarded to Graphite in the last flush")
public long getLastMetricsForwarded() {
return getMetricValue("LastMetricsForwarded");
}
/**
* Returns the number of metrics that failed on sending to Graphite
* @return the number of metrics that failed on sending to Graphite
*/
@ManagedMetric(category="Graphite", metricType=MetricType.COUNTER, description="the number of metrics that failed on sending to Graphite")
public long getMetricsForwardFailures() {
return getMetricValue("MetricsForwardFailures");
}
/**
* Returns the number of metrics that were dropped because Graphite was down
* @return the number of metrics that were dropped because Graphite was down
*/
@ManagedMetric(category="Graphite", metricType=MetricType.COUNTER, description="the number of metrics that were dropped because Graphite was down")
public long getMetricsDropped() {
return getMetricValue("MetricsDropped");
}
/**
* Accept Route additive for BaseDestination extensions
* @param routable The metric to route
*/
@Override
protected void doAcceptRoute(IMetric routable) {
try {
final long start = System.currentTimeMillis();
channel.write(routable).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if(f.isSuccess()) {
incr("MetricsForwarded");
} else {
incr("MetricForwardFailures");
}
}
});
} catch (Exception e) {
incr("MetricsForwardFailures");
}
}
/**
* Returns the endpoint server name or IP address
* @return the endpoint server name or IP address
*/
@ManagedAttribute(description="The endpoint server name or IP address")
public String getHost() {
return host;
}
/**
* Indicates if the channel is connected (or ready)
* @return true if the channel is connected, false otherwise
*/
@ManagedAttribute(description="Indicates if the channel is connected (or ready)")
public boolean isConnected() {
return connected.get();
}
/**
* Sets the endpoint server name or IP address
* @param host the endpoint server name or IP address
*/
@ManagedAttribute(description="The endpoint server name or IP address")
public void setHost(String host) {
if(isStarted()) throw new IllegalStateException("Cannot set the host once the channel is started", new Throwable());
this.host = host;
}
/**
* Sets the endpoint server listening port
* @param port the endpoint server listening port
*/
@ManagedAttribute(description="The endpoint server port")
public void setPort(int port) {
if(isStarted()) throw new IllegalStateException("Cannot set the port once the channel is started", new Throwable());
this.port = port;
}
/**
* Returns the endpoint server listening port
* @return the endpoint server listening port
*/
@ManagedAttribute(description="The endpoint server name or IP address")
public int getPort() {
return port;
}
/**
* Returns the channel handler bean names in the order they are bound into the pipelines created for this listener
* @return an array of channel handler bean names
*/
@ManagedAttribute(description="The channel handler bean names")
public String[] getChannelHandlerNames() {
List<String> names = new ArrayList<String>();
if(isStarted()) {
synchronized(resolvedHandlers) {
names.addAll(resolvedHandlers.keySet());
}
} else {
names.addAll(channelHandlers.values());
}
return names.toArray(new String[names.size()]);
}
/**
* Sets the channel handlers bound into the pipelines created for this listener
* @param channelHandlers A map of channel handler bean names keyed by the ordering int of the handlers in the pipeline
*/
public void setChannelHandlers(Map<Integer, String> channelHandlers) {
if(channelHandlers!=null) {
synchronized(this.channelHandlers) {
this.channelHandlers.clear();
this.channelHandlers.putAll(channelHandlers);
}
}
}
/**
* Returns the channel options applied to channels created by this listener
* @return the channel options applied
*/
public Map<String, Object> getChannelOptions() {
return channelOptions;
}
/**
* Sets the worker pool for the graphite destination
* @param workerPool the netty worker thread pool
*/
public void setWorkerPool(ExecutorService workerPool) {
this.workerPool = workerPool;
}
/**
* Indicates if a logging handler is installed in the pipeline
* @return true if a logging handler is installed, false otherwise
*/
@ManagedAttribute(description="Indicates if a logging handler is installed in the pipeline")
public boolean isLoggingHandlerInstalled() {
return loggingHandlerInstalled.get();
}
/**
* Returns the logging handler location
* @return the logging handler location
*/
@ManagedAttribute(description="The logging handler location")
public String getLoggingHandlerLocation() {
return loggingHandlerLocation;
}
/**
* Adds a logging handler to the pipeline map if not already present and if the listener is started.
* @param after The name of the handler after which the logger should be added. Adds first in the pipeline if this name is null or empty.
* @param level The level at which the logging handler should log, based on the names in {@link InternalLogLevel}.
* @param hex true if and only if the hex dump of the received message is logged
*/
@ManagedOperation(description="Adds a logging handler to the pipeline map if not already present and if the listener is started")
@ManagedOperationParameters({
@ManagedOperationParameter(name="after", description="The name of the handler after which the logger should be added. Adds first in the pipeline if this name is null or empty."),
@ManagedOperationParameter(name="level", description="The level at which the logging handler should log"),
@ManagedOperationParameter(name="hex", description="true if and only if the hex dump of the received message is logged")
})
public void addLoggingHandler(String after, String level, boolean hex) {
if(loggingHandlerInstalled.get()) return;
if(!isStarted()) throw new IllegalStateException("This operation can only be executed once the destination is started", new Throwable());
if(after==null || after.trim().isEmpty()) {
after = "first";
} else {
if(!resolvedHandlers.containsKey(after)) throw new IllegalArgumentException("Invalid handler location [" + after + "]", new Throwable());
}
InternalLogLevel logLevel = InternalLogLevel.valueOf(level.trim().toUpperCase());
String handlerName = getClass().getSimpleName() + "." + beanName + "Logger";
LoggingHandler loggingHandler = new LoggingHandler(handlerName, logLevel, hex);
synchronized(resolvedHandlers) {
LinkedHashMap<String, ChannelHandler> tmp = new LinkedHashMap<String, ChannelHandler>(resolvedHandlers);
resolvedHandlers.clear();
if(after.equals("first")) {
resolvedHandlers.put(handlerName, loggingHandler);
resolvedHandlers.putAll(tmp);
} else {
for(Map.Entry<String, ChannelHandler> entry: tmp.entrySet()) {
String loc = entry.getKey();
resolvedHandlers.put(loc, entry.getValue());
if(after.equals(loc)) {
resolvedHandlers.put(handlerName, loggingHandler);
}
}
}
loggingHandlerInstalled.set(true);
}
}
/**
* Removes the logging handler from the pipeline if it is installed and the listener is started
*/
@ManagedOperation(description="Removes the logging handler from the pipeline if it is installed and the listener is started ")
public void removeLoggingHandler() {
if(!loggingHandlerInstalled.get()) return;
if(!isStarted()) throw new IllegalStateException("This operation can only be executed once the destination is started", new Throwable());
String handlerName = getClass().getSimpleName() + "." + beanName + "Logger";
synchronized(resolvedHandlers) {
resolvedHandlers.remove(handlerName);
loggingHandlerInstalled.set(false);
}
}
/**
* Returns the total number of channels created
* @return the total number of channels created
*/
@ManagedMetric(category="NettyDestinations", description="The total number of channels created", metricType=MetricType.COUNTER)
public long getChannelsCreated() {
return getMetricValue("ChannelsCreated");
}
/**
* Returns the total number of channels closed
* @return the total number of channels closed
*/
@ManagedMetric(category="NettyDestinations", description="The total number of channels closed", metricType=MetricType.COUNTER)
public long getChannelsClosed() {
return getMetricValue("ChannelsClosed");
}
/**
* Returns the number of channels currently open
* @return the number of channels currently open
*/
@ManagedMetric(category="NettyDestinations", description="The total number of channels currently open", metricType=MetricType.COUNTER)
public int getCurrentChannels() {
return channelGroup.size();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponent#getSupportedMetricNames()
*/
@Override
public Set<String> getSupportedMetricNames() {
Set<String> _metrics = new HashSet<String>(super.getSupportedMetricNames());
_metrics.add("ChannelsCreated");
_metrics.add("ChannelsClosed");
_metrics.add("MetricsForwarded");
_metrics.add("LastMetricsForwarded");
_metrics.add("MetricsDropped");
_metrics.add("MetricsForwardFailures");
return _metrics;
}
/**
* Returns the frequency of reconnect attempts in ms.
* @return the frequency of reconnect attempts
*/
@ManagedAttribute(description="The frequency of reconnect attempts in ms")
public long getReconnectPeriod() {
return reconnectPeriod;
}
/**
* Sets the frequency of reconnect attempts in ms
* @param reconnectPeriod the frequency of reconnect attempts
*/
@ManagedAttribute(description="The frequency of reconnect attempts in ms")
public void setReconnectPeriod(long reconnectPeriod) {
this.reconnectPeriod = reconnectPeriod;
}
/**
* Indicates if the client is currently in a reconnect loop
* @return true if the client is currently in a reconnect loop, false otherwise
*/
@ManagedAttribute(description="Indicates if the client is currently in a reconnect loop")
public boolean isReconnecting() {
return reconnecting.get();
}
/**
* Injects the scheduler
* @param scheduler the platform scheduler
*/
@Autowired(required=true)
public void setScheduler(ThreadPoolTaskScheduler scheduler) {
this.scheduler = scheduler;
}
/**
* Initializes the main channel (presumably through the bootstrap)
* @return the ChanelFuture for the channel initialization
*/
protected abstract ChannelFuture initializeChannel();
/**
* Builds the channel factory for this netty client
* @return the built channel factory
*/
protected abstract ChannelFactory buildChannelFactory();
/**
* Builds the bootstrap for this netty client
* @return the built bootstrap
*/
protected abstract Bootstrap buildBootstrap();
}