/*
* Copyright 2003-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.smodel;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.testFramework.ThreadTracker;
import com.intellij.util.ReflectionUtil;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.smodel.TaskScheduler.Task;
import jetbrains.mps.smodel.TaskScheduler.TaskIsOutdated;
import jetbrains.mps.util.NamedThreadFactory;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static jetbrains.mps.smodel.EDTExecutor.MAX_SINGLE_EXECUTION_TIME_MS;
/**
* Manages the tasks queue; allowing concurrently to add new tasks and flushing the old ones.
*
* 1. Tasks might come from various threads, they are added to the *tasks queue* under the #myQueueLock.
* 2. Every time the task is the first one in the queue the #flush is initiated.
* 3. The flush procedure is executed asynchronously on the EDT (via the {@link TransactionGuard#submitTransactionLater(Disposable, Runnable)})
* Property: The order of execution is equal to the order of tasks' scheduling
*
* @author apyshkin
*/
final class EDTExecutorInternal implements Disposable {
private static final Logger LOG = LogManager.getLogger(EDTExecutorInternal.class);
private static final String THREAD_GROUP_NAME = "MPS EDT Executor Thread";
private final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(createDaemonFactory());
private final Lock myQueueLock = new ReentrantLock();
private final Condition myQueueIsEmptyCondition = myQueueLock.newCondition();
private final AtomicInteger myTasksCount = new AtomicInteger();
@NotNull
private static ThreadFactory createDaemonFactory() {
return new NamedThreadFactory(THREAD_GROUP_NAME + "-", true);
}
/**
* elements are added only in the {@link TaskScheduler#scheduleTask(Task)} under the {@link #myQueueLock}.
* elements are removed in the EDT only in the {@link EDTExecutorInternal#tryToRunTopTask()}
*/
private final Queue<Task> myTaskQueue = new ConcurrentLinkedQueue<>();
EDTExecutorInternal() {
tellIdeaToBackOff();
}
/**
* otherwise the test in the idea plugin are failing since the ThreadTracker is very strict
* AP
*/
private void tellIdeaToBackOff() {
ThreadTracker.longRunningThreadCreated(this, THREAD_GROUP_NAME);
}
void scheduleTask(Task task) {
traceTheCaller();
try {
myQueueLock.lock();
boolean wakeUp = taskQueueIsEmpty();
boolean success = myTaskQueue.add(task);
if (!success) {
LOG.error("Failed to add a task into the queue " + task);
}
int size = myTasksCount.incrementAndGet();
LOG.trace("total tasks in the queue " + size);
if (wakeUp) {
signalTasksAppeared();
}
} finally {
myQueueLock.unlock();
}
}
private void traceTheCaller() {
if (LOG.isTraceEnabled()) {
LOG.trace("schedule task: the caller is " + ReflectionUtil.findCallerClass(7));
}
}
private void signalTasksAppeared() {
flushQueueLaterInEDT();
}
/**
* flushing the whole queue in the edt within the transaction.
*/
private void flushQueueLaterInEDT() {
assert !taskQueueIsEmpty() : "private method precondition is not satisfied";
TransactionGuard.getInstance().submitTransactionLater(this, this::flushTasksQueue);
}
private void flushTasksQueue() {
ThreadUtils.assertEDT();
checkQueueIsNotTooLarge();
doFlush();
}
private void doFlush() {
try {
ScheduledFuture<?> timer = createTimeOutFuture();
int queueSize = myTasksCount.get();
while (queueSize > 0) {
LOG.trace(String.format("flush %d tasks: %d ms left", queueSize, timer.getDelay(TimeUnit.MILLISECONDS)));
flushNTasks(timer, queueSize);
if (timer.isDone()) {
return;
}
queueSize = myTasksCount.get();
}
} finally {
try {
myQueueLock.lock();
if (taskQueueIsEmpty()) {
signalNoTasksInTheQueue();
} else {
flushQueueLaterInEDT();
}
} finally {
myQueueLock.unlock();
}
}
}
private void flushNTasks(ScheduledFuture<?> timer, int nTasksToFlush) {
for (int taskCounter = 0; taskCounter < nTasksToFlush; ++taskCounter) {
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("flush tasks: %d ms left", timer.getDelay(TimeUnit.MILLISECONDS)));
}
tryToRunTopTask();
}
}
@NotNull
private ScheduledFuture<?> createTimeOutFuture() {
return EXECUTOR_SERVICE.schedule(() -> {},
MAX_SINGLE_EXECUTION_TIME_MS,
TimeUnit.MILLISECONDS);
}
private void checkQueueIsNotTooLarge() {
int queueSize = myTasksCount.get();
if (queueSize > EDTExecutor.QUEUE_MAX_EXPECTED_VALUE) {
LOG.warn("Tasks queue size is " + queueSize + " which is above the expected");
} else {
LOG.trace("flushing the queue with " + queueSize + " tasks in it");
}
}
/**
* Actual task execution happens here
* It tries to access the corresponding lock (read/write) and removes the task only if succeeds.
* @return true iff the task was a success and it is gone from the queue
*/
private boolean tryToRunTopTask() {
Task task = myTaskQueue.peek();
if (task == null) {
return false;
}
boolean taskPassed = true;
try {
taskPassed = task.tryRun();
} catch (TaskIsOutdated ignored) {
LOG.warn("The scheduled task has expired", ignored);
} catch (Exception e) {
LOG.error("run in EDT failure", e);
} finally {
if (taskPassed) {
LOG.trace("removing the task");
myTaskQueue.remove();
myTasksCount.decrementAndGet();
}
}
return taskPassed;
}
void flushTasks() {
if (ThreadUtils.isInEDT()) {
throw new IllegalStateException("Current Thread is EDT : possible deadlock");
}
waitForQueueToBeEmpty();
}
private boolean taskQueueIsEmpty() {
return myTasksCount.get() == 0;
}
/**
* triggers the {@link #waitForQueueToBeEmpty()} method
*/
private void signalNoTasksInTheQueue() {
myQueueIsEmptyCondition.signalAll();
}
/**
* A standard idiom: waiting for a condition to happen (here: wait until the tasks queue is empty)
* Triggered by {@link EDTExecutorInternal#signalNoTasksInTheQueue()}
*/
private void waitForQueueToBeEmpty() {
try {
myQueueLock.lock();
while (!taskQueueIsEmpty()) {
try {
myQueueIsEmptyCondition.await();
} catch (InterruptedException ie) {
LOG.warn("Interrupted while waiting for flush", ie);
Thread.currentThread().interrupt();
return;
}
}
} finally {
myQueueLock.unlock();
}
}
@Override
public void dispose() {
new ExecutorServiceShutdownHelper(EXECUTOR_SERVICE).shutdownAndAwaitTermination(EDTExecutor.TERMINATION_TIMEOUT_MS);
}
}