All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.logging.messaging.MessageBatcher Maven / Gradle / Ivy

/*
 * Copyright 2012 Netflix, Inc.
 *
 *    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 com.netflix.logging.messaging;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.netflix.blitz4j.BlitzConfig;
import com.netflix.blitz4j.LoggingConfiguration;
import com.netflix.config.DynamicBooleanProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.annotations.Monitor;
import com.netflix.servo.monitor.Counter;
import com.netflix.servo.monitor.Monitors;
import com.netflix.servo.monitor.Stopwatch;
import com.netflix.servo.monitor.Timer;

/**
 * A general purpose batcher that combines messages into batches. Callers of
 * process don't block. Configurable parameters control the number of messages
 * that may be queued awaiting processing, the maximum size of a batch, the
 * maximum time a message waits to be combined with others in a batch and the
 * size of the pool of threads that process batches.
 * 

* The implementation aims to avoid congestion, by working more efficiently as * load increases. As messages arrive faster, the collector executes less code * and batch sizes increase (up to the configured maximum). It should be more * efficient to process a batch than to process the messages individually. *

* The implementation works by adding the arriving messages to a queue. The collector * thread takes messages from the queue and collects them into batches. When a * batch is big enough or old enough, the collector passes it to the processor, * which passes the batch to the target stream. *

* The processor maintains a thread pool. If there's more work than threads, the * collector participates in processing by default, and consequently stops * collecting more batches. * * @author Karthik Ranganathan */ public class MessageBatcher { private static final BlitzConfig CONFIGURATION = LoggingConfiguration.getInstance().getConfiguration(); private static final String DOT = "."; private static final String BATCHER_PREFIX = "batcher."; private static final String COLLECTOR_SUFFIX = ".collector"; private boolean shouldCollectorShutdown; List batch; protected String name; protected BlockingQueue queue; protected int maxMessages; protected static long maxDelay; // in nsec protected Collector collector; protected ThreadPoolExecutor processor; protected MessageProcessor target = null; /** * The number of batches that are currently being processed by the target * stream. */ protected final AtomicInteger concurrentBatches = new AtomicInteger(0); protected Timer queueSizeTracer; protected Timer batchSyncPutTracer; protected Timer threadSubmitTracer; protected Timer processTimeTracer; protected Timer avgBatchSizeTracer; protected Counter queueOverflowCounter; private volatile boolean isShutDown; private AtomicLong numberAdded = new AtomicLong(); private AtomicLong numberDropped = new AtomicLong(); private boolean blockingProperty; private boolean isCollectorPaused; private Counter processCount; public static final String POOL_MAX_THREADS = "maxThreads"; public static final String POOL_MIN_THREADS = "minThreads"; public static final String POOL_KEEP_ALIVE_TIME = "keepAliveTime"; public MessageBatcher(String name, MessageProcessor target) { this.name = BATCHER_PREFIX + name; this.target = target; queue = new ArrayBlockingQueue(CONFIGURATION.getBatcherQueueMaxMessages(this.name)); setBatchMaxMessages(CONFIGURATION.getBatchSize(this.name)); batch = new ArrayList(maxMessages); setBatchMaxDelay(CONFIGURATION .getBatcherMaxDelay(this.name)); collector = new Collector(this, this.name + COLLECTOR_SUFFIX); // Immediate Executor creates a factory that uses daemon threads createProcessor(this.name); queueSizeTracer = Monitors.newTimer("queue_size"); batchSyncPutTracer = Monitors.newTimer("waitTimeforBuffer"); avgBatchSizeTracer = Monitors.newTimer("batch_size"); processCount = Monitors.newCounter("messages_processed"); threadSubmitTracer = Monitors.newTimer("thread_invocation_time"); processTimeTracer = Monitors.newTimer("message_processTime"); queueOverflowCounter = Monitors.newCounter("queue_overflow"); blockingProperty = CONFIGURATION .shouldWaitWhenBatcherQueueNotEmpty(this.name); collector.setDaemon(true); collector.start(); try { Monitors.registerObject(this.name, this); } catch (Throwable e) { if (CONFIGURATION .shouldPrintLoggingErrors()) { e.printStackTrace(); } } } /** Set the stream that will process each batch of messages. */ public synchronized void setTarget(MessageProcessor target) { this.target = target; } /** * Set the maximum number of messages in a batch. Setting this to 1 will * prevent batching; that is, messages will be passed to * target.processMessage one at a time. */ public synchronized void setBatchMaxMessages(int maxMessages) { this.maxMessages = maxMessages; } /** * Set the maximum time a message spends waiting to complete a full batch, * in seconds. This doesn't limit the time spent in the queue. */ public synchronized void setBatchMaxDelay(double maxDelaySec) { maxDelay = (long) (maxDelaySec * 1000000000); } /** * Set the max threads for the processors * @param maxThreads - max threads that can be launched for processing */ public void setProcessorMaxThreads(int maxThreads) { if (processor.getCorePoolSize() > maxThreads) { processor.setCorePoolSize(maxThreads); } processor.setMaximumPoolSize(maxThreads); } /** * Checks to see if there is space available in the queue * * @return - true, if available false otherwise */ public boolean isSpaceAvailable() { return (queue.remainingCapacity() > 0); } /** * Processes the message sent to the batcher. This method just writes the * message to the queue and returns immediately. If the queue is full, the * messages are dropped immediately and corresponding counter is * incremented. * * @param message * - The message to be processed * @return boolean - true if the message is queued for processing,false(this * could happen if the queue is full) otherwise */ public boolean process(T message) { // If this batcher has been shutdown, do not accept any more messages if (isShutDown) { return false; } try { queueSizeTracer.record(queue.size()); } catch (Throwable ignored) { } if (!queue.offer(message)) { numberDropped.incrementAndGet(); queueOverflowCounter.increment(); return false; } numberAdded.incrementAndGet(); return true; } /** * Processes the message sent to the batcher. This method tries to write to * the queue. If the queue is full, the send blocks and waits for the * available space. * * @param message * - The message to be processed */ public void processSync(T message) { // If this batcher has been shutdown, do not accept any more messages if (isShutDown) { return; } try { queueSizeTracer.record(queue.size()); } catch (Throwable ignored) { } try { Stopwatch s = batchSyncPutTracer.start(); queue.put(message); s.stop(); } catch (InterruptedException e) { return; } numberAdded.incrementAndGet(); } /** * Processes the messages sent to the batcher. This method just writes the * message to the queue and returns immediately. If the queue is full, the * messages are dropped immediately and corresponding counter is * incremented. * * @param objects * - The messages to be processed */ public void process(List objects) { for (T message : objects) { // If this batcher has been shutdown, do not accept any more // messages if (isShutDown) { return; } process(message); } } /** * Processes the messages sent to the batcher. The messages are first queued * and then will be processed by the * {@link com.netflix.logging.messaging.MessageProcessor} * * @param objects * - The messages to be processed * @param sync * - if true, waits for the queue to make space, if false returns * immediately after dropping the message */ public void process(List objects, boolean sync) { for (T message : objects) { // If this batcher has been shutdown, do not accept any more // messages if (isShutDown) { return; } if (sync) { processSync(message); } else { process(message); } } } /** * Pause the collector. The collector stops picking up messages from the * queue. */ public void pause() { if (!isShutDown) { this.isCollectorPaused = true; } } public boolean isPaused() { return this.isCollectorPaused; } /** * Resume the collector. The collector resumes picking up messages from the * queue and calling the processors. */ public void resume() { if (!isShutDown) { this.isCollectorPaused = false; } } /** * Stops the batcher. The Batcher has to wait for the other processes like * the Collector and the Executor to complete. It waits until it is notified * that the other processes have completed gracefully. The collector waits * until there are no more messages in the queue(tries 3 times waiting for * 0.5 seconds each) and then shuts down gracefully. * * */ public void stop() { /* * Sets the shutdown flag. Future sends to the batcher are not accepted. * The processors wait for the current messages in the queue and with * the processor or collector to complete */ isShutDown = true; int waitTimeinMillis = CONFIGURATION.getBatcherWaitTimeBeforeShutdown(this.name); long timeToWait = waitTimeinMillis + System.currentTimeMillis(); while ((queue.size() > 0 || batch.size() > 0) && (System.currentTimeMillis() < timeToWait)) { try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } try { shouldCollectorShutdown = true; processor.shutdownNow(); /* * processor.awaitTermination(10000, TimeUnit.SECONDS); if * (!processor.isShutdown()) { processor.shutdownNow(); } */ } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * The class that processes the messages in a batch by calling the * implementor of the MessageProcessor interface. * * */ private static class ProcessMessages implements Runnable { public ProcessMessages(MessageBatcher stream, List batch) { this.stream = stream; this.batch = batch; this.processMessagesTracer = stream.processTimeTracer; this.avgConcurrentBatches = Monitors.newTimer(stream.name + ".concurrentBatches"); } private final MessageBatcher stream; private List batch; private Timer processMessagesTracer; private Timer avgConcurrentBatches; /** Process the batch. */ public void run() { try { if (batch == null) { return; } int inProcess = stream.concurrentBatches.incrementAndGet(); try { avgConcurrentBatches.record(inProcess); Stopwatch s = processMessagesTracer.start(); stream.target.process(batch); s.stop(); } finally { stream.concurrentBatches.decrementAndGet(); } } catch (Throwable e) { e.printStackTrace(); } } } private class Collector extends Thread { private static final int SLEEP_TIME_MS = 1; private Timer processTimeTracer; private Counter rejectedCounter = Monitors.newCounter(processCount + ".rejected"); private static final int RETRY_EXECUTION_TIMEOUT_MS = 1; public Collector(MessageBatcher stream, String name) { super(name); processTimeTracer = Monitors.newTimer(name + ".processTime"); this.stream = stream; queueSizeTracer = Monitors.newTimer(name + ".queue_size_at_drain"); } private final MessageBatcher stream; private final Timer queueSizeTracer; /** Process messages from the queue, after grouping them into batches. */ public void run() { int batchSize = 0; while (!shouldCollectorShutdown) { if (isCollectorPaused) { try { Thread.sleep(SLEEP_TIME_MS); } catch (InterruptedException ignore) { } continue; } try { if (batch.size() < stream.maxMessages) { long now = System.nanoTime(); final long firstTime = now; do { if (stream.queue.drainTo(batch, stream.maxMessages - batch.size()) <= 0) { long maxWait = firstTime + stream.maxDelay - now; if (maxWait <= 0) { // timed out break; } // Wait for a message to arrive: Object nextMessage = null; try { nextMessage = stream.queue.poll(maxWait, TimeUnit.NANOSECONDS); } catch (InterruptedException ignore) { } if (nextMessage == null) { // timed out break; } batch.add(nextMessage); now = System.nanoTime(); } } while (batch.size() < stream.maxMessages); } batchSize = batch.size(); if (batchSize > 0) { try { queueSizeTracer.record(stream.queue.size()); } catch (Exception ignored) { } avgBatchSizeTracer.record(batchSize); Stopwatch s = processTimeTracer.start(); boolean retryExecution = false; do { try { stream.processor.execute(new ProcessMessages( stream, batch)); retryExecution = false; } catch (RejectedExecutionException re) { rejectedCounter.increment(); retryExecution = true; Thread.sleep(RETRY_EXECUTION_TIMEOUT_MS); } } while (retryExecution); processCount.increment(batchSize); s.stop(); batch = new ArrayList(stream.maxMessages); } } catch (Throwable e) { if (CONFIGURATION.shouldPrintLoggingErrors()) { e.printStackTrace(); } } } // - while (!shutdownCollector) } // - run() } /** * The size of the the queue in which the messages are batches * * @return- size of the queue */ @Monitor(name = "batcherQueueSize", type = DataSourceType.GAUGE) public int getSize() { if (queue != null) { return queue.size(); } else { return 0; } } /** * Resets the statistics that keeps the count of number of messages added to * this batcher. */ public void resetNumberAdded() { numberAdded.set(0); } /** * Resets the statistics that keeps the count of number of messages dropped * by this batcher. */ public void resetNumberDropped() { numberDropped.set(0); } /** * Gets the statistics count of number of messages added to this batcher. */ @Monitor(name = "numberAdded", type = DataSourceType.GAUGE) public long getNumberAdded() { return numberAdded.get(); } /** * Gets the statistics count of number of messages dropped by this batcher. */ @Monitor(name = "numberDropped", type = DataSourceType.GAUGE) public long getNumberDropped() { return numberDropped.get(); } /** * Gets the information whether the batcher is blocking or not blocking. By * default, the batcher is non-blocking and the messages are just dropped if * the queue is full. * * If the batcher is made blocking, the sends block and wait indefinitely * until space is made in the batcher. * * @return - true if blocking, false otherwise */ @Monitor(name = "blocking", type = DataSourceType.INFORMATIONAL) public boolean isBlocking() { return blockingProperty; } private void createProcessor(String name) { int minThreads = CONFIGURATION .getBatcherMinThreads(this.name); int maxThreads = CONFIGURATION .getBatcherMaxThreads(this.name); int keepAliveTime = CONFIGURATION.getBatcherThreadKeepAliveTime(this.name); ThreadFactory threadFactory = new ThreadFactoryBuilder() .setDaemon(true).setNameFormat(this.name + "-process").build(); this.processor = new ThreadPoolExecutor(minThreads, maxThreads, keepAliveTime, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory); boolean shouldRejectWhenFull = CONFIGURATION .shouldRejectWhenAllBatcherThreadsUsed(this.name); if (!shouldRejectWhenFull) { this.processor .setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { super.rejectedExecution(r, e); } }); } } }