/* * Copyright 2014 TORCH GmbH * * 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 org.graylog2.gelfclient.transport; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import org.graylog2.gelfclient.GelfMessage; import org.graylog2.gelfclient.util.Uninterruptibles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import static java.util.concurrent.TimeUnit.MICROSECONDS; /** * The main event thread used by the {@link org.graylog2.gelfclient.transport.GelfTransport}s. */ public class GelfSenderThread { private static final Logger LOG = LoggerFactory.getLogger(GelfSenderThread.class); private final ReentrantLock lock; private final Condition connectedCond; private final AtomicBoolean keepRunning = new AtomicBoolean(true); private final Thread senderThread; private Channel channel; private final int maxInflightSends; /** * Creates a new sender thread with the given {@link BlockingQueue} as source of messages. * * @param queue the {@link BlockingQueue} used as source of {@link GelfMessage}s * @param maxInflightSends the maximum number of outstanding network writes/flushes before the sender spins */ public GelfSenderThread(final BlockingQueue<GelfMessage> queue, int maxInflightSends) { this.maxInflightSends = maxInflightSends; this.lock = new ReentrantLock(); this.connectedCond = lock.newCondition(); if (maxInflightSends <= 0) { throw new IllegalArgumentException("maxInflightSends must be larger than 0"); } this.senderThread = new Thread(new Runnable() { @Override public void run() { GelfMessage gelfMessage = null; final AtomicInteger inflightSends = new AtomicInteger(0); final ChannelFutureListener inflightListener = new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { inflightSends.decrementAndGet(); } }; while (keepRunning.get()) { // wait until we are connected to the graylog2 server before polling log events from the queue lock.lock(); try { while (channel == null || !channel.isActive()) { try { connectedCond.await(); } catch (InterruptedException e) { if (!keepRunning.get()) { // bail out if we are awoken because the application is stopping break; } } } // we are connected, let's start sending logs try { // if we have a lingering event already, try to send that instead of polling a new one. if (gelfMessage == null) { gelfMessage = queue.poll(100, TimeUnit.MILLISECONDS); } // if we are still connected, convert LoggingEvent to GELF and send it // but if we aren't connected anymore, we'll have already pulled an event from the queue, // which we keep hanging around in this thread and in the next loop iteration will block until we are connected again. if (gelfMessage != null && channel != null && channel.isActive()) { // Do not allow more than "maxInflightSends" concurrent writes in netty, to avoid having netty buffer // excessively when faced with slower consumers while (inflightSends.get() > GelfSenderThread.this.maxInflightSends) { Uninterruptibles.sleepUninterruptibly(1, MICROSECONDS); } inflightSends.incrementAndGet(); // Write the GELF message to the pipeline. The protocol specific channel handler // will take care of encoding. channel.writeAndFlush(gelfMessage).addListener(inflightListener); gelfMessage = null; } } catch (InterruptedException e) { // ignore, when stopping keepRunning will be set to false outside } } finally { lock.unlock(); } } LOG.debug("GelfSenderThread exiting!"); } }); this.senderThread.setDaemon(true); this.senderThread.setName("GelfSenderThread-" + senderThread.getId()); } public void start(Channel channel) { lock.lock(); try { this.channel = channel; this.connectedCond.signalAll(); } finally { lock.unlock(); } senderThread.start(); } public void stop() { keepRunning.set(false); senderThread.interrupt(); } }