/*******************************************************************************
* Copyright (c) 2015, 2017 Obeo and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
* Martin Fleck - bug 512677
*******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.logical.resolver;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin;
/**
* Class providing facilities to schedule computations and hide as much as possible the multi-threaded
* complexity. Each computation is identified by a key. Each computation is supposed to possibly load
* resources, and the scheduler also provides a facility to unload resources that are no longer needed.
*
* @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a>
* @param <T>
* type of keys used to keep track of currently ongoing computations.
*/
public class ResourceComputationScheduler<T> {
/**
* Keeps track of the keys which we are currently computing (or which are queued for computation).
* <p>
* This along with {@link #computedKeys} will prevent multiple "duplicate" computation threads to be
* queued. We assume that this will be sufficient to prevent duplicates, and the computation threads
* themselves won't check whether their target has already been computed before starting.
* </p>
*/
private final Set<T> currentlyComputing;
/**
* We'll keep track of what's already been computed to avoid duplicate jobs.
*/
private volatile Set<T> computedKeys;
/** Thread pool for our resolving threads. */
private ListeningExecutorService computingPool;
/** Thread pool for our unloading threads. */
private ListeningExecutorService unloadingPool;
/**
* An executor service will be used to shut down the {@link #unloadingPool} and the {@link #computingPool}
* .
*/
private ListeningExecutorService terminator;
/** Tracks if shutdown of {@link #unloadingPool} and {@link #computingPool} is currently in progress. */
private final AtomicBoolean shutdownInProgress;
/**
* This will lock will prevent concurrent modifications of this class's fields. Most notably,
* {@link #currentlyComputing} and {@link #computedKeys} must not be accessed concurrently by two threads
* at once.
*/
private final ReentrantLock lock;
/** Condition to await for all current task threads to terminate. */
private final Condition endOfTasks;
/** How long to wait for task completion when shutting down the pools. */
private final int shutdownWaitDuration;
/** Unit of the above duration. */
private final TimeUnit shutdownWaitUnit;
/** Event bus used to send state change events */
private final EventBus eventBus;
/**
* Constructor, configured to wait for tasks completion for 5 seconds (will wait at most 10 seconds).
*/
public ResourceComputationScheduler() {
this(5, TimeUnit.SECONDS);
}
/**
* Constructor.
*
* @param shutdownWaitDuration
* Time to wait for current tasks completion when shutting down the pools (will wait at most
* twice this amount of time).
* @param shutdownWaitUnit
* Unit to use to interpret the other parameter.
*/
public ResourceComputationScheduler(int shutdownWaitDuration, TimeUnit shutdownWaitUnit) {
this(shutdownWaitDuration, shutdownWaitUnit, null);
}
/**
* Constructor.
*
* @param shutdownWaitDuration
* Time to wait for current tasks completion when shutting down the pools (will wait at most
* twice this amount of time).
* @param shutdownWaitUnit
* Unit to use to interpret the other parameter.
* @param eventBus
* The {@link EventBus} used to post events (shutdown events), can be {@code null}
*/
public ResourceComputationScheduler(int shutdownWaitDuration, TimeUnit shutdownWaitUnit,
EventBus eventBus) {
this.lock = new ReentrantLock(true);
this.endOfTasks = lock.newCondition();
this.currentlyComputing = new HashSet<T>();
this.shutdownInProgress = new AtomicBoolean(false);
this.shutdownWaitDuration = shutdownWaitDuration;
this.shutdownWaitUnit = shutdownWaitUnit;
this.eventBus = eventBus;
}
/**
* Creates the thread pools of this resolver and instantiates the computedElements Set. We cannot keep
* pools between resolving calls because in case of cancellation, we have to shutdown the pool to exit
* early.
* <p>
* <b>Pre-conditions:</b>
* <ul>
* <li>{@link #initialize()} has been called</li>
* <li>A lock has been acquired before on this instance's {@link #lock}</li>
* <li>{@link #dispose()} has not been called</li>
* </ul>
* <b>Post-conditions:</b>
* <ul>
* <li>{@link #computingPool} is not null and is ready to be used</li>
* <li>{@link #unloadingPool} is not null and is ready to be used</li>
* <li>{@link #computedKeys} is not null</li>
* </p>
*/
private void setUpComputation() {
final int availableProcessors = Runtime.getRuntime().availableProcessors();
ThreadFactory computingThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("EMFCompare-ResolvingThread-%d") //$NON-NLS-1$
.build();
this.computingPool = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(availableProcessors, computingThreadFactory));
ThreadFactory unloadingThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("EMFCompare-UnloadingThread-%d") //$NON-NLS-1$
.build();
this.unloadingPool = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(availableProcessors, unloadingThreadFactory));
computedKeys = new LinkedHashSet<T>();
}
/**
* Shutdown the pools and delete {@link #computedKeys}.
* <p>
* <b>Post-conditions:</b>
* <ul>
* <li>{@link #computingPool} is null and is ready to be used</li>
* <li>{@link #unloadingPool} is null and is ready to be used</li>
* <li>{@link #computedKeys} is null</li>
* </p>
*/
private void tearDownComputation() {
if (!shutdownInProgress.get()) {
shutdownPools();
}
computedKeys = null;
}
/**
* If {@link #shutdownInProgress shutdown has not been requested before}, it submits a new task to
* {@link #shutdownPools() shut down} {@link #computingPool} and {@link #unloadingPool}. Do nothing if
* current thread already is interrupted. If a shutdown is actually started, events will be posted to the
* scheduler's eventBus if there is one. The events will be:
* <ol>
* <li>STARTED</li>
* <li>SUCCESS if shutdown has succeeded or FAILURE if shutdown has failed</li>
* </ol>
* <b>Note</b> that these events will be sent in the calling Thread.
*/
public void demandShutdown() {
if (!Thread.currentThread().isInterrupted()) {
if (shutdownInProgress.compareAndSet(false, true)) {
if (eventBus != null) {
eventBus.post(ShutdownStatus.STARTED);
}
Runnable runnable = new Runnable() {
public void run() {
shutdownPools();
}
};
ListenableFuture<?> listenableFuture = terminator.submit(runnable);
Futures.addCallback(listenableFuture, new FutureCallback<Object>() {
public void onSuccess(Object result) {
shutdownInProgress.set(false);
if (eventBus != null) {
eventBus.post(ShutdownStatus.SUCCESS);
}
}
public void onFailure(Throwable t) {
shutdownInProgress.set(false);
if (eventBus != null) {
eventBus.post(new ShutdownStatus(t));
}
EMFCompareIDEUIPlugin.getDefault().log(t);
}
});
}
}
}
/**
* Shutdown {@link #computingPool} and {@link #unloadingPool} and set these two fields to null.
*/
private synchronized void shutdownPools() {
try {
// we don't do the closing in one sentence to make sure we call both shutdown methods
if (computingPool != null) {
shutdownAndAwaitTermination(computingPool);
}
if (unloadingPool != null) {
shutdownAndAwaitTermination(unloadingPool);
}
} finally {
computingPool = null;
unloadingPool = null;
}
}
/**
* Initializes this scheduler, which instantiates its {@link #terminator}. Can be called several times
* with no problem.
*/
public synchronized void initialize() {
if (!isInitialized()) {
this.terminator = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("EMFCompare-ThreadPoolShutdowner-%d") //$NON-NLS-1$
.setPriority(Thread.MAX_PRIORITY).build()));
}
}
/**
* Indicates whether this scheduler is initialized, i.e. it can run computations.
*
* @return {@code true} if and only if the scheduler is initialized.
*/
public boolean isInitialized() {
return terminator != null;
}
/**
* Disposes this scheduler, which shuts down its {@link #terminator}.
*/
public synchronized void dispose() {
if (isInitialized()) {
terminator.shutdown();
terminator = null;
}
}
/**
* Executes the given callable as soon as possible. Whatever happens, the given callback is run before
* returning (in a "finally" clause) and then the "notComputing" condition will be signalled and the lock
* released.
* <p>
* <b>Pre-conditions:</b>
* <ul>
* <li>{@link #initialize()} has been called</li>
* <li>{@link #dispose()} has not been called</li>
* </ul>
* </p>
* If the scheduler has an eventBus, it will post the following events:
* <ol>
* <li>SETTING_UP</li>
* <li>SCHEDULED as soon as the set-up is finished</li>
* <li>FAILURE if and only if the given callable throws an exception</li>
* <li>FINISHING as soon as the given callable has finished running (successfully or not)</li>
* <li>FINISHED as soon as the tear-down is finished</li>
* </ol>
* <b>Note</b> that these events will be sent in the Thread that ran the computation, NOT in the calling
* Thread.
*
* @param callable
* will be executed as soon as this instance is no longer computing anything. Must not be
* {@code null}.
* @param postTreatment
* will be called in a finally clause, whatever the outcome of the computation. Can be
* {@code null}.
* @param <U>
* the type of the return value.
* @return The result returned by the given callable execution.
*/
public synchronized <U> U call(Callable<U> callable, Runnable postTreatment) {
checkNotNull(callable);
try {
if (eventBus != null) {
eventBus.post(CallStatus.SETTING_UP);
}
setUpComputation();
if (eventBus != null) {
eventBus.post(CallStatus.SCHEDULED);
}
return callable.call();
} catch (Exception e) {
if (eventBus != null) {
eventBus.post(new CallStatus(e));
}
if (e instanceof InterruptedException) {
throw new OperationCanceledException();
}
if (e instanceof OperationCanceledException) {
throw (OperationCanceledException)e;
}
throw new RuntimeException(e);
} finally {
if (eventBus != null) {
eventBus.post(CallStatus.FINISHING);
}
try {
tearDownComputation();
if (postTreatment != null) {
postTreatment.run();
}
} finally {
if (eventBus != null) {
eventBus.post(CallStatus.FINISHED);
}
}
}
}
/**
* Schedules all the given computations, which will only be run if no computation for the same key is in
* the {@link #computedKeys} variable. It is up to the caller to make sure that the semantics of
* computations previously run is the same as thos they are submitting, otherwise computations completely
* unrelated to what is being submitted may have marked a key as already computed. Returns after all the
* currently running plus submitted computations have finished.
*
* @param computations
* An iterable over the computations to schedule. {@code null} entries are silently ignored.
*/
public void computeAll(Iterable<? extends IComputation<T>> computations) {
checkNotNull(computations);
lock.lock();
for (IComputation<T> comp : computations) {
if (comp != null) {
scheduleComputation(comp);
}
}
waitForEndOfTasks();
}
/**
* Schedules a given computation to be performed as soon as possible, if its key is not present in the
* {@link #computedKeys} or in the {@link #currentlyComputing} keys, in which case the computation is
* ignored. It is up to the caller to make sure that they submit homogeneous computations, in order for
* the filtering of computations by key to be meaningful.
* <p>
* <b>WARNING!</b> In a multi-threaded execution, this method may return before the computation is run. It
* is up to callers to make sure they only invoke that inside of a more general call to
* {@link #call(Callable, Runnable)}, {@link #computeAll(Iterable)}, or {@link #runAll(Iterable)}
* </p>
*
* @param computation
* The computation to run. Cannot be {@code null}.
* @return {@code true} if and only if the given key is not already among either the computed elements or
* the currently computing elements.
*/
public boolean scheduleComputation(final IComputation<T> computation) {
checkNotNull(computation);
lock.lock();
try {
if (computedKeys.add(computation.getKey()) && currentlyComputing.add(computation.getKey())) {
ListenableFuture<?> future = computingPool.submit(new Runnable() {
public void run() {
computation.run();
}
});
// even if post-treatment is null, we need this callback for proper finalization
Futures.addCallback(future, new ComputingFutureCallback<T>(this, computation.getKey(),
computation.getPostTreatment()));
return true;
}
return false;
} finally {
lock.unlock();
}
}
/**
* Executes all the runnables in the given iterable, and returns when all computations possibly running or
* launched by the given runnables are finished. This must be used when some treatment will possibly
* schedule computations but the exact list of computations cannot be computed beforehand.
*
* @param runnables
* An iterable over the runnables to execute, must not be {@code null}. {@code null} entries
* are silently ignored.
*/
public void runAll(Iterable<? extends Runnable> runnables) {
checkNotNull(runnables);
lock.lock();
for (Runnable runnable : runnables) {
if (runnable != null) {
runnable.run();
}
}
waitForEndOfTasks();
}
/**
* Schedule a job that is suppoed to unload resource(s) that are no longer needed. This implementation
* uses a dedicated thread pool to perform these unloads.
*
* @param runnable
* Runnable to run, must not be {@code null}
* @param callback
* Callback to call upon completion, can be {@code null}
*/
public void scheduleUnload(Runnable runnable, FutureCallback<Object> callback) {
ListenableFuture<?> future = unloadingPool.submit(runnable);
if (callback != null) {
Futures.addCallback(future, callback);
}
}
/**
* Provides the set of keys of all the computations that have been run or are still running since its set
* of keys {@link #computedKeys} was last set.
*
* @return The set of keys of all the computations that have been run or are still running since its set
* of keys {@link #computedKeys} was last set.
*/
public ImmutableSet<T> getComputedElements() {
if (computedKeys == null) {
return ImmutableSet.of();
}
return ImmutableSet.copyOf(computedKeys);
}
/**
* Evaluates whether a computation with the given key has been run or is still running since its set of
* keys {@link #computedKeys} was last set.
*
* @param key
* The key of the computation to check.
* @return true, if a computation of the given key has been run or is still running.
*/
public boolean isScheduled(T key) {
return computedKeys != null && computedKeys.contains(key);
}
/**
* Clears the set of computed keys.
*/
public void clearComputedElements() {
computedKeys.clear();
}
/**
* Sets the computed keys with all the values in the given iterable.
*
* @param elements
* An iterable over the elements to set as computed, must not be {@code null} but can be empty.
*/
public void setComputedElements(Iterable<T> elements) {
computedKeys = Sets.newLinkedHashSet(elements);
}
/**
* Shuts down an {@link ExecutorService} in two phases, first by calling {@link ExecutorService#shutdown()
* shutdown} to reject incoming tasks, and then calling {@link ExecutorService#shutdownNow() shutdownNow},
* if necessary, to cancel any lingering tasks. Returns true if the pool has been properly shutdown, false
* otherwise.
* <p>
* Copy/pasted from {@link ExecutorService} javadoc.
*
* @param pool
* the pool to shutdown, must not be {@code null}
* @return true if the pool has been properly shutdown, false otherwise.
*/
private boolean shutdownAndAwaitTermination(ExecutorService pool) {
boolean ret = true;
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(shutdownWaitDuration, shutdownWaitUnit)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being canceled
if (!pool.awaitTermination(shutdownWaitDuration, shutdownWaitUnit)) {
ret = false;
}
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
ret = false;
}
return ret;
}
/**
* Wait until all tasks have finished executing, or the current thread is interrupted, in which case an
* OperationCanceledException is thrown.
*/
private void waitForEndOfTasks() {
try {
// TODO Is this test really necessary?
// TODO Is this test dangerous (infinite wait)
while (!currentlyComputing.isEmpty()) {
endOfTasks.await();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// FIXME?
throw new OperationCanceledException();
} finally {
lock.unlock();
}
}
/**
* This will remove the given uri from the {@link #currentlyComputing} set and signal to
* {@link #endOfTasks} if the set is empty afterward. This method must be call by every callback of
* resolving tasks.
*
* @param key
* the key to remove.
*/
private void finalizeTask(T key) {
lock.lock();
try {
currentlyComputing.remove(key);
if (currentlyComputing.isEmpty()) {
endOfTasks.signal();
}
} finally {
lock.unlock();
}
}
// TODO Change javadoc
/**
* The callback for tasks. It will report progress, log errors and finalize the resolving and as such,
* possibly signaling the end of the resolution.
*
* @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a>
*/
private static final class ComputingFutureCallback<T> implements FutureCallback<Object> {
/** The scheduler. */
private final ResourceComputationScheduler<T> scheduler;
/** The key. */
private final T key;
/** The wrapped callback. */
private final FutureCallback<Object> wrappedCallback;
/**
* Constructor.
*
* @param scheduler
* The scheduler
* @param key
* The key
* @param callback
* The callback, can be {@code null}
*/
private ComputingFutureCallback(ResourceComputationScheduler<T> scheduler, T key,
FutureCallback<Object> callback) {
this.scheduler = checkNotNull(scheduler);
this.key = checkNotNull(key);
this.wrappedCallback = callback;
}
/**
* {@inheritDoc}
*/
public void onSuccess(Object result) {
try {
if (wrappedCallback != null) {
wrappedCallback.onSuccess(result);
}
} finally {
scheduler.finalizeTask(key);
}
}
/**
* {@inheritDoc}
*/
public void onFailure(Throwable t) {
try {
if (wrappedCallback != null) {
wrappedCallback.onFailure(t);
}
} finally {
scheduler.finalizeTask(key);
}
}
}
public static enum ComputationState {
/** Computation is setting-up (preparing pools and so on). */
SETTING_UP,
/** Computation is set-up and scheduled. */
SCHEDULED,
/** Computation is over, tear-down and post-treatments are starting. */
FINISHING,
/** Computation is over and has failed. */
FAILED,
/** Computation is over and tear-down + post-treatments are finished. */
FINISHED;
}
public static class CallStatus {
public static final CallStatus SETTING_UP = new CallStatus(ComputationState.SETTING_UP);
public static final CallStatus SCHEDULED = new CallStatus(ComputationState.SCHEDULED);
public static final CallStatus FINISHING = new CallStatus(ComputationState.FINISHING);
public static final CallStatus FINISHED = new CallStatus(ComputationState.FINISHED);
private final Throwable cause;
private final ComputationState state;
private CallStatus(ComputationState state) {
this.state = state;
this.cause = null;
}
private CallStatus(Throwable cause) {
this.state = ComputationState.FAILED;
this.cause = cause;
}
public Throwable getCause() {
return cause;
}
public ComputationState getState() {
return state;
}
}
public static enum ShutdownState {
STARTED, FINISH_FAILED, FINISH_SUCCESS;
}
public static class ShutdownStatus {
public static final ShutdownStatus STARTED = new ShutdownStatus(ShutdownState.STARTED);
public static final ShutdownStatus SUCCESS = new ShutdownStatus(ShutdownState.FINISH_SUCCESS);
private final Throwable cause;
private final ShutdownState state;
private ShutdownStatus(ShutdownState state) {
this.state = state;
this.cause = null;
}
private ShutdownStatus(Throwable cause) {
this.state = ShutdownState.FINISH_FAILED;
this.cause = cause;
}
public Throwable getCause() {
return cause;
}
public ShutdownState getState() {
return state;
}
}
}