package org.dcache.services.billing.text; import com.google.common.base.Throwables; import com.google.common.io.LineProcessor; import java.io.IOException; import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.Phaser; import static com.google.common.base.Preconditions.checkState; /** * Decorates a LineProcessor to parallelize invocation of the decorator processor. * * Invoking {@link #processLine(String)} may or may not block and the * decorator processor's {@link LineProcessor#processLine(String)} method * will be called from one of a number of worker threads. * * Lines starting with a hash ('#') however act as barriers and are always passed * through sequentially. * * The decorator must be closed through {@link #close()} before retrieving the result * with {@link #getResult()}. */ public class ParallelizingLineProcessor<T> implements LineProcessor<T>, AutoCloseable { private final ExecutorService service = Executors.newCachedThreadPool(); private final List<Future<T>> readers = new ArrayList<>(); private final BlockingQueue<String> queue; private final int threads; private final LineProcessor<T> callback; private final Phaser phaser; private boolean areWorkersRunning; public ParallelizingLineProcessor(int threads, LineProcessor<T> callback) { this.threads = threads; this.callback = callback; phaser = new Phaser(threads + 1); queue = new ArrayBlockingQueue<>(threads * 512); for (int i = 0; i < threads; i++) { readers.add(service.submit(new LineReader<>(queue, callback, phaser))); } } @Override public void close() throws IOException { try { if (areWorkersRunning) { stopWorkers(); } phaser.forceTermination(); phaser.arriveAndDeregister(); service.shutdown(); for (Future<T> reader : readers) { reader.get(); } } catch (InterruptedException e) { throw new InterruptedIOException(e.getMessage()); } catch (ExecutionException e) { Throwable cause = e.getCause(); Throwables.throwIfUnchecked(cause); throw new IOException(cause.getMessage(), cause); } } private void startWorkers() { checkState(!areWorkersRunning); phaser.arriveAndAwaitAdvance(); areWorkersRunning = true; } private void stopWorkers() throws InterruptedException { checkState(areWorkersRunning); for (int i = 0; i < threads; i++) { queue.put(""); } phaser.arriveAndAwaitAdvance(); areWorkersRunning = false; } @Override public boolean processLine(String line) throws IOException { if (!line.isEmpty()) { try { if (line.charAt(0) != '#') { if (!areWorkersRunning) { startWorkers(); } queue.put(line); } else { if (areWorkersRunning) { stopWorkers(); } callback.processLine(line); } } catch (InterruptedException e) { throw new InterruptedIOException(e.getMessage()); } } return true; } @Override public T getResult() { return callback.getResult(); } private static class LineReader<T> implements Callable<T> { private final BlockingQueue<String> queue; private final LineProcessor<T> processor; private final Phaser phaser; private LineReader(BlockingQueue<String> queue, LineProcessor<T> processor, Phaser phaser) { this.queue = queue; this.processor = processor; this.phaser = phaser; } @Override public T call() throws InterruptedException { while (phaser.arriveAndAwaitAdvance() >= 0) { String line; while (!(line = queue.take()).isEmpty()) { try { processor.processLine(line); } catch (Throwable t) { Thread thread = Thread.currentThread(); thread.getUncaughtExceptionHandler().uncaughtException(thread, t); } } phaser.arriveAndAwaitAdvance(); } return processor.getResult(); } } }