org.fiolino.common.processing.sink.ParallelizingSink Maven / Gradle / Ivy
Show all versions of commons Show documentation
package org.fiolino.common.processing.sink;
import org.fiolino.common.container.Container;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A sink that splits the targets into parallel threads.
*
* Created by kuli on 31.03.16.
*/
public final class ParallelizingSink extends ChainedSink {
private static final Logger logger = Logger.getLogger(ParallelizingSink.class.getName());
private final Consumer executor;
private final String name;
private final int parallelity;
private Task next;
private volatile int commitCount;
private long timeout = TimeUnit.MINUTES.toSeconds(5);
private volatile Container currentMetadata;
/**
* Creates a parallelizer.
*
* @param target This will be parallelized. It must be either a {@link CloneableSink} or a {@link ThreadsafeSink}.
* @param name The name of the sink, used for the logger.
* @param parallelity How many threads are running of the target sink. 0 means no parallelity at all,
* the target will be returned directly. -1 means the parallelity is based on the number
* of cores, i.e. it's the number of cores minus one.
* @param queueSize The size of the queue for each thread. When the queue is full, then the producer thread first
* tries to find another thread's queue, and waits until the next active queue gets freed by
* at least one.
* @param The type of the processed items.
* @return The {@link ParallelizingSink}, or the target if parallelity is zero.
*/
public static Sink createFor(Sink target, String name,
Consumer executor,
int parallelity, int queueSize) {
validateTarget(target);
int p = getRealParallelity(parallelity);
if (p == 0) {
return target;
}
return new ParallelizingSink<>(target, name, executor, p, queueSize);
}
private static void validateTarget(Sink super T> target) {
if (!(target instanceof CloneableSink) && !(target instanceof ThreadsafeSink)) {
throw new IllegalArgumentException("Target " + target + " must be either cloneable or thread safe!");
}
}
private static int getRealParallelity(int parallelity) {
if (parallelity < 0) {
return Runtime.getRuntime().availableProcessors() - 1;
}
return parallelity;
}
private ParallelizingSink(Sink super T> target, String name,
Consumer executor,
int parallelity, int queueSize) {
super(target);
this.name = name;
assert parallelity >= 1;
if (queueSize <= 0) {
throw new IllegalArgumentException("QueueSize must be > 0: " + queueSize);
}
this.parallelity = parallelity;
this.executor = executor;
Task task = new Task(target, 1, queueSize);
next = task;
for (int i = 1; i < parallelity; i++) { // starting at 1 is intended, since one task is already created
Sink super T> newTarget = targetForCloning(target);
task = createTask(task, newTarget, queueSize);
}
task.setNext(next);
}
@Override
public String toString() {
return name;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
String nameFor(Task t) {
return name + " #" + t.number + "/" + parallelity;
}
private Task createTask(Task task, Sink super T> target, int queueSize) {
Task n = new Task(target, task.number + 1, queueSize);
task.setNext(n);
return n;
}
@Override
public void accept(T value, Container metadata) throws Exception {
currentMetadata = metadata;
try {
next = next.offer(value);
} catch (InterruptedException ex) {
logger.log(Level.WARNING, () -> "Adding " + value + " to full queue was interrupted!");
Thread.currentThread().interrupt();
}
}
@Override
public void commit(Container metadata) throws Exception {
try {
SynchronizationPoint p = new SynchronizationPoint(timeout, metadata);
next.putIntoAll(p);
p.startAndWait(name + " ");
commitCount++;
} catch (InterruptedException ex) {
logger.log(Level.WARNING, () -> "Interrupted while finishing!");
Thread.currentThread().interrupt();
}
next.throwError();
super.commit(metadata);
}
public int[] getWorkCounters() {
return next.getCounters();
}
private static class SynchronizationPoint {
private final CountDownLatch initializer;
private final long timeout;
private final Container metadata;
private volatile CountDownLatch latch;
private int waiters;
SynchronizationPoint(long timeout, Container metadata) {
this.timeout = timeout;
this.metadata = metadata;
initializer = new CountDownLatch(1);
waiters = 1;
}
Container getMetadata() {
return metadata;
}
void register() {
if (waiters == -1) {
throw new IllegalStateException("Was already started.");
}
waiters++;
}
/**
* This is called from the main thread.
*
* @param name The name is only used in case of timeout
* @throws InterruptedException If the synchronization point wasn't added to the queues
*/
void startAndWait(String name) throws InterruptedException {
latch = new CountDownLatch(waiters);
logger.info(() -> "Will wait for " + waiters + " threads");
waiters = -1;
initializer.countDown();
await(name);
}
/**
* This is called from the individual tasks.
*
* @param name Used for logging
* @throws InterruptedException If the synchronization was interrupted
*/
void syncTask(String name) throws InterruptedException {
logger.info("Synchronizing " + name);
initializer.await();
await(name);
}
private void await(String name) throws InterruptedException {
latch.countDown();
if (latch.await(timeout, TimeUnit.SECONDS)) {
return;
}
logger.log(Level.WARNING, () -> "Timeout after " + timeout + " seconds on " + name);
}
}
private final class Task implements Runnable {
private final int number;
private final BlockingDeque