package com.mongodb.hvdf.interceptors; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mongodb.BasicDBList; import com.mongodb.DBObject; import com.mongodb.hvdf.channels.ChannelInterceptor; import com.mongodb.hvdf.configuration.PluginConfiguration; import com.mongodb.hvdf.configuration.TimePeriod; public class BatchingInterceptor extends ChannelInterceptor{ private static Logger logger = LoggerFactory.getLogger(BatchingInterceptor.class); // Configuration defaults private static final int DEFAULT_BATCH_SIZE = 1000; private static final int DEFAULT_NTHREADS = 2; private static final int DEFAULT_MAX_BATCHES = 100; private static final TimePeriod DEFAULT_MAX_AGE = new TimePeriod(1000); // Configuration values private int maxBatchSize = DEFAULT_BATCH_SIZE; private long maxBatchAge = DEFAULT_MAX_AGE.getAs(TimeUnit.MILLISECONDS); private int maxQueuedBatches = DEFAULT_MAX_BATCHES; private int nThreads = DEFAULT_NTHREADS; // Batch management private final ScheduledExecutorService batchTimer; private final BatchManagementTask stagingBatch; private BlockingQueue<Runnable> batchQueue; private ThreadPoolExecutor executor; public BatchingInterceptor(PluginConfiguration config){ this.batchTimer = Executors.newScheduledThreadPool(1); this.stagingBatch = new BatchManagementTask(); configure(config); } @Override public void pushSample(DBObject sample, boolean isList, BasicDBList resultIds) { // push the incoming sample(s) to the staging batch this.stagingBatch.pushToBatch(sample, isList); // Do NOT call forward interceptor, this is done // within the batch executor } @Override public void shutdown() { // Orderly shutdown, existing batches will flush this.batchTimer.shutdown(); this.executor.shutdown(); } private void configure(PluginConfiguration config) { // Pull any specific config from the overrides this.maxBatchSize = config.get("target_batch_size", Integer.class, DEFAULT_BATCH_SIZE); this.maxBatchAge = (config.get("max_batch_age", TimePeriod.class, DEFAULT_MAX_AGE)).getAs(TimeUnit.MILLISECONDS); this.nThreads = config.get("thread_count", Integer.class, DEFAULT_NTHREADS); this.maxQueuedBatches = config.get("max_queued_batches", Integer.class, DEFAULT_MAX_BATCHES); // Schedule the batching task this.batchTimer.scheduleAtFixedRate(stagingBatch, maxBatchAge, maxBatchAge, TimeUnit.MILLISECONDS); // Create the batch queue and the executor that processes them this.batchQueue = new LinkedBlockingDeque<Runnable>(this.maxQueuedBatches); this.executor = new ThreadPoolExecutor( this.nThreads, this.nThreads, 1, TimeUnit.SECONDS, this.batchQueue, new ThreadPoolExecutor.CallerRunsPolicy()); } // Task class for sending batches to channel private class BatchProcessingTask implements Runnable{ private final BasicDBList batch; public BatchProcessingTask(final BasicDBList batch){ this.batch = batch; } @Override public void run() { try{ next.pushSample(batch, true, new BasicDBList()); } catch(Exception ex){ logger.error("Failed to write batch", ex); } } } // Task class for managing incoming samples and periodically // timing out partial batches private class BatchManagementTask implements Runnable { private BasicDBList currentBatch = new BasicDBList(); @Override public void run() { // Push a null sample to the task to force a flush pushToBatch(null, false); } public synchronized void pushToBatch(DBObject sample, boolean isList){ if(sample == null){ // A null sample indicates a flush is required if(currentBatch.size() > 0){ flushBatch(); } } else { // Add the list or document to the current batch if(isList){ currentBatch.addAll((BasicDBList) sample); } else { currentBatch.add(sample); } // If the batch size exceeds limit, flush it if(currentBatch.size() >= maxBatchSize){ flushBatch(); } } } private void flushBatch() { // Create an executor task to process logger.debug("Queuing batch for processing - size : {}, queuelength : {}", currentBatch.size(), batchQueue.size()); executor.submit(new BatchProcessingTask(currentBatch)); currentBatch = new BasicDBList(); } } }