/**
* AnalyzerBeans
* Copyright (C) 2014 Neopost - Customer Information Management
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program 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 distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.eobjects.analyzer.util.batch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eobjects.analyzer.beans.api.Transformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A batch transformation buffer utility for archieving minor batch operations
* while preserving the one-by-one transformation interface of
* {@link Transformer}.
*
* @param <I>
* the input type
* @param <O>
* the output type
*/
public class BatchTransformationBuffer<I, O> {
private static final Logger logger = LoggerFactory.getLogger(BatchTransformationBuffer.class);
// default 1 second interval of flushing
public static final int DEFAULT_FLUSH_INTERVAL = 1000;
// default max 20 items in buffer
public static final int DEFAULT_MAX_BATCH_SIZE = 20;
private static final long[] AWAIT_TIMES = { 20, 50, 100, 100, 200 };
private final BatchTransformation<I, O> _transformation;
private final BlockingQueue<BatchEntry<I, O>> _queue;
private final AtomicInteger _batchNo;
private final int _maxBatchSize;
private final ScheduledExecutorService _threadPool;
private final int _flushInterval;
public BatchTransformationBuffer(BatchTransformation<I, O> transformation) {
this(transformation, DEFAULT_MAX_BATCH_SIZE, DEFAULT_FLUSH_INTERVAL);
}
public BatchTransformationBuffer(BatchTransformation<I, O> transformation, int maxBatchSize, int flushIntervalMillis) {
_transformation = transformation;
_flushInterval = flushIntervalMillis;
_maxBatchSize = maxBatchSize;
_queue = new ArrayBlockingQueue<BatchEntry<I, O>>(maxBatchSize);
_batchNo = new AtomicInteger();
_threadPool = Executors.newScheduledThreadPool(1);
}
public void start() {
logger.info("start()");
_threadPool.scheduleAtFixedRate(createFlushCommand(), _flushInterval, _flushInterval, TimeUnit.MILLISECONDS);
}
private Runnable createFlushCommand() {
return new Runnable() {
@Override
public void run() {
flushBuffer(true);
}
};
}
public int getBatchCount() {
return _batchNo.get();
}
public void flushBuffer() {
flushBuffer(false);
}
private void flushBuffer(boolean scheduled) {
if (_queue.isEmpty()) {
// do nothing when queue is empty
return;
}
if (!scheduled) {
if (_queue.size() < _maxBatchSize) {
logger.debug("Batch ignored, flush operation not scheduled and queue is not full");
return;
}
}
final List<BatchEntry<?, O>> entries = new ArrayList<BatchEntry<?, O>>(_maxBatchSize);
final int batchSize = _queue.drainTo(entries);
if (batchSize == 0) {
logger.debug("Batch ignored, no elements left in queue");
// it may happen that multiple threads try to flush at the same
// time - in this case we want to stop them here.
return;
}
final int batchNumber = _batchNo.incrementAndGet();
logger.info("Batch #{} - Preparing {} entries, scheduled={}", batchNumber, batchSize, scheduled);
final Object[] input = new Object[batchSize];
for (int i = 0; i < batchSize; i++) {
input[i] = entries.get(i).getInput();
}
final BatchSource<I> source = new ArrayBatchSource<I>(input);
final BatchEntryBatchSink<O> sink = new BatchEntryBatchSink<O>(entries);
_transformation.map(source, sink);
logger.info("Batch #{} - Finished", batchNumber, batchSize);
if (scheduled) {
if (!_queue.isEmpty()) {
flushBuffer(true);
}
}
}
public void shutdown() {
logger.info("shutdown()");
_threadPool.shutdown();
}
public O transform(I input) {
final BatchEntry<I, O> entry = new BatchEntry<I, O>(input);
while (!_queue.offer(entry)) {
flushBuffer();
}
int attemptIndex = 0;
while (true) {
final long waitTime = (attemptIndex < AWAIT_TIMES.length ? AWAIT_TIMES[attemptIndex]
: AWAIT_TIMES[AWAIT_TIMES.length - 1]);
try {
final boolean finished = entry.await(waitTime);
if (finished) {
return entry.getOuput();
}
flushBuffer();
attemptIndex++;
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new IllegalStateException(e);
}
}
}
}