package de.uni_goettingen.sub.commons.ocr.abbyy.server.multiuser;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.ILock;
import de.uni_goettingen.sub.commons.ocr.abbyy.server.AbbyyProcess;
import de.uni_goettingen.sub.commons.ocr.abbyy.server.ItemComparator;
/**
* This executor uses Hazelcast to implement coordination of abbyy processes across several JVMs
* or even different hosts. It includes a kind of waiting queue for abbyy processes. In general,
* processes with a higher priority and a lower timestamp are preferred and the executor will try
* to start them before the other ones.
*
* When there are several users and one of them has a higher priority, he will use all but one
* execution slots (their number is defined by maxParallelThreads). This way, one high-priority
* user cannot block all the others. However, if there are two or more higher-priority users,
* they will consume all the execution time, so that the ones with lower priority
* will have to wait.
*
* Processes with the same priority are sorted by their timestamp. The timestamp is set right
* here in the executor just before the run() method of each process, and not at the time of
* creating the processes. Otherwise, two long-running batches of processes would alternate
* in their execution and never let a third one come through to be executed. In effect, this
* is the behavior of a simple FIFO queue.
*
* @author dennis
*
*/
public class HazelcastExecutor extends ThreadPoolExecutor implements Executor {
private final static Logger logger = LoggerFactory.getLogger(HazelcastExecutor.class);
private int maxProcesses;
// This is where all the started processes of the whole cluster reside. They are all at least
// in the beforeExecute() method. Their number can be up to maxProcesses*nodesInCluster.
private Map<String, AbbyyProcess> queuedProcesses;
// This is used to temporarily sort the processes by their priority to find out which one
// is allowed to leave beforeExecute() next.
private PriorityQueue<AbbyyProcess> queuedProcessesSorted;
// Here are all the processes that made it through beforeExecute() and are actively running.
// Their maximum number cluster-wide is maxProcesses.
private Set<String> runningProcesses;
private Lock clusterLock;
private Condition mightBeAllowedToExecute;
private long waitingTimeInMillis = 1000 * 60 * 10;
private long startedExecutingAt = Long.MAX_VALUE;
// for unit tests
void setLock(Lock newLock) {
clusterLock = newLock;
}
void setCondition(Condition newCondition) {
mightBeAllowedToExecute = newCondition;
}
void setQueuedProcesses(Map<String, AbbyyProcess> newQueued) {
queuedProcesses = newQueued;
}
void setRunningProcesses(Set<String> newRunning) {
runningProcesses = newRunning;
}
void setWaitingTime(long newTime) {
waitingTimeInMillis = newTime;
}
long getStartedLastProcessAt() {
return startedExecutingAt;
}
public HazelcastExecutor(int maxParallelThreads, HazelcastInstance hazelcast) {
super(maxParallelThreads, maxParallelThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
clusterLock = hazelcast.getLock("clusterLock");
mightBeAllowedToExecute = ((ILock)clusterLock).newCondition("clusterCondition");
maxProcesses = maxParallelThreads;
queuedProcessesSorted = new PriorityQueue<AbbyyProcess>(100, new ItemComparator());
queuedProcesses = hazelcast.getMap("queued");
runningProcesses = hazelcast.getSet("running");
}
@Override
protected void beforeExecute(Thread t, Runnable process) {
super.beforeExecute(t, process);
AbbyyProcess abbyyProcess = (AbbyyProcess) process;
abbyyProcess.setStartedAt(System.currentTimeMillis());
clusterLock.lock();
try {
queuedProcesses.put(abbyyProcess.getProcessId(), abbyyProcess);
while (!allowedToExecute(abbyyProcess)) {
mightBeAllowedToExecute.await(waitingTimeInMillis, TimeUnit.MILLISECONDS);
}
queuedProcesses.remove(abbyyProcess.getProcessId());
runningProcesses.add(abbyyProcess.getProcessId());
mightBeAllowedToExecute.signalAll();
} catch (InterruptedException e) {
logger.error("Waiting thread was interrupted: " + abbyyProcess.getName(), e);
} finally {
clusterLock.unlock();
}
startedExecutingAt = System.nanoTime();
}
private boolean allowedToExecute(AbbyyProcess abbyyProcess) {
boolean thereAreFreeSlots = runningProcesses.size() < maxProcesses;
queuedProcessesSorted.clear();
queuedProcessesSorted.addAll(queuedProcesses.values());
AbbyyProcess head = queuedProcessesSorted.poll();
boolean currentIsHead = head.equals(abbyyProcess);
return thereAreFreeSlots && currentIsHead && abbyyProcess.hasEnoughSpaceForExecution();
}
@Override
protected void afterExecute(Runnable process, Throwable e) {
super.afterExecute(process, e);
AbbyyProcess abbyyProcess = (AbbyyProcess) process;
clusterLock.lock();
try {
runningProcesses.remove(abbyyProcess.getProcessId());
mightBeAllowedToExecute.signalAll();
} finally {
clusterLock.unlock();
}
}
}