package com.outbrain.gruffalo.netty; import java.io.IOException; import java.net.SocketAddress; import java.util.concurrent.atomic.AtomicInteger; import com.outbrain.swinfra.metrics.api.Histogram; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.outbrain.swinfra.metrics.api.Counter; import com.outbrain.swinfra.metrics.api.Gauge; import com.outbrain.swinfra.metrics.api.MetricFactory; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; class MetricBatcher extends SimpleChannelInboundHandler<String> { private static final Logger log = LoggerFactory.getLogger(MetricBatcher.class); private static final AtomicInteger lastBatchSize = new AtomicInteger(0); private final int batchBufferCapacity; private final Counter connectionCounter; private final Counter metricsCounter; private final Counter unexpectedErrorCounter; private final Counter ioErrorCounter; private final Counter idleChannelsClosed; private final ChannelGroup activeChannels; private final Histogram metricSize; private final int maxChannelIdleTime; private StringBuilder batch; private int currBatchSize; private DateTime lastRead = DateTime.now(); public MetricBatcher(final MetricFactory metricFactory, final int batchBufferCapacity, final ChannelGroup activeChannels, final int maxChannelIdleTime) { Preconditions.checkArgument(maxChannelIdleTime > 0, "maxChannelIdleTime must be greater than 0"); this.maxChannelIdleTime = maxChannelIdleTime; Preconditions.checkNotNull(metricFactory, "metricFactory may not be null"); this.batchBufferCapacity = batchBufferCapacity; this.activeChannels = Preconditions.checkNotNull(activeChannels, "activeChannels must not be null"); prepareNewBatch(); final String component = getClass().getSimpleName(); connectionCounter = metricFactory.createCounter(component, "connections"); metricsCounter = metricFactory.createCounter(component, "metricsReceived"); unexpectedErrorCounter = metricFactory.createCounter(component, "unexpectedErrors"); ioErrorCounter = metricFactory.createCounter(component, "ioErrors"); idleChannelsClosed = metricFactory.createCounter(component, "idleChannelsClosed"); metricSize = metricFactory.createHistogram(component, "metricSize", false); try { metricFactory.registerGauge(component, "batchSize", new Gauge<Integer>() { @Override public Integer getValue() { return lastBatchSize.get(); } }); } catch (IllegalArgumentException e) { // ignore metric already exists } } @Override public void channelRead0(final ChannelHandlerContext ctx, final String msg) throws Exception { lastRead = DateTime.now(); currBatchSize++; if (batch.capacity() < batch.length() + msg.length()) { sendBatch(ctx); } batch.append(msg); metricsCounter.inc(); metricSize.update(msg.length()); } private void sendBatch(final ChannelHandlerContext ctx) { if (0 < batch.length()) { ctx.fireChannelRead(new Batch(batch, currBatchSize)); prepareNewBatch(); } } private void prepareNewBatch() { batch = new StringBuilder(batchBufferCapacity); lastBatchSize.set(currBatchSize); currBatchSize = 0; } @Override public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { if (evt instanceof IdleStateEvent) { final IdleStateEvent e = (IdleStateEvent) evt; if (e.state() == IdleState.READER_IDLE) { sendBatch(ctx); final SocketAddress remoteAddress = ctx.channel().remoteAddress(); if (remoteAddress != null) { if (lastRead.plusSeconds(maxChannelIdleTime).isBefore(System.currentTimeMillis())) { log.warn("Closing suspected leaked connection: {}", remoteAddress); idleChannelsClosed.inc(); ctx.close(); lastRead = DateTime.now(); } } } } } @Override public void channelRegistered(final ChannelHandlerContext ctx) throws Exception { if (ctx.channel().remoteAddress() != null) { connectionCounter.inc(); activeChannels.add(ctx.channel()); } } @Override public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { connectionCounter.dec(); try { sendBatch(ctx); } catch (final RuntimeException e) { log.warn("failed to send last batch when closing channel " + ctx.channel().remoteAddress()); } } @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { if (cause instanceof IOException) { ioErrorCounter.inc(); log.error("IOException while handling metrics. Remote host =" + ctx.channel().remoteAddress(), cause); } else { unexpectedErrorCounter.inc(); log.error("Unexpected exception while handling metrics. Remote host =" + ctx.channel().remoteAddress(), cause); } ctx.close(); } }