/*
* 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.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.util.ComputeRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.annotations.Immutable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Since the IDEA platform seems not to have any primitive #tryWrite to invoke
* and we sometimes experience long reads (e.g. Highlighter) we are forced to
* start a separate thread which waits for some time and then interrupts
*
* @author apyshkin
* @since 2017.2
*
* TODO request #tryLock method from the IDEA platform
*/
@Immutable
final class TryRunPlatformWriteHelper {
private static final int WAIT_FOR_WRITE_LOCK_MS = 200;
private final WriteActionTracker myWriteActionTracker;
private final DelayQueue<DelayedInterrupt> myInterruptQueue = new DelayQueue<>();
TryRunPlatformWriteHelper(@NotNull WriteActionTracker writeActionTracker) {
myWriteActionTracker = writeActionTracker;
myInterruptingThread.start();
}
private final Thread myInterruptingThread = new Thread(() -> {
while (true) {
try {
DelayedInterrupt di = myInterruptQueue.take();
di.timeIsUp();
} catch (InterruptedException e) {
Application app = ApplicationManager.getApplication();
if (app == null || app.isDisposeInProgress() || app.isDisposed()) {
return;
}
}
}
}, "MPS interrupting thread");
public void dispose() {
for (int attempt = 3; attempt > 0 && myInterruptingThread.isAlive(); --attempt) {
myInterruptingThread.interrupt();
try {
myInterruptingThread.join(500);
} catch (InterruptedException e) {
break;
}
}
}
private void cancelInterrupt(DelayedInterrupt di) {
myInterruptQueue.remove(di);
}
private DelayedInterrupt interruptLater(Thread toInterrupt, long delay, TimeUnit unit) {
DelayedInterrupt di = new DelayedInterrupt(toInterrupt, delay, unit);
myInterruptQueue.put(di);
return di;
}
void tryCommand(@NotNull Project project, @NotNull Runnable runnable) throws WriteTimeOutException {
ComputeRunnable<WriteTimeOutException> computable = new ComputeRunnable<>(() -> {
TryWriteActionRunnable tryWriteAction = new TryWriteActionRunnable(runnable);
try {
tryWriteAction.tryWrite();
} catch (WriteTimeOutException e) {
return e;
}
return null;
});
executeCommand(project, computable);
if (computable.getResult() != null) {
throw computable.getResult();
}
}
private void executeCommand(@NotNull Project project, ComputeRunnable<?> computable) {
CommandProcessor.getInstance().executeCommand(
project,
computable,
"MPS #tryCommand",
null,
UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION);
}
<T> T tryWrite(Computable<T> computable) throws WriteTimeOutException {
ComputeRunnable<T> toCompute = new ComputeRunnable<>(computable::compute);
new TryWriteActionRunnable(toCompute).tryWrite();
return toCompute.getResult();
}
@Immutable
private final class TryWriteActionRunnable {
private final Runnable myRunnable;
TryWriteActionRunnable(Runnable runnable) {
myRunnable = runnable;
}
void tryWrite() throws WriteTimeOutException {
ThreadUtils.assertEDT();
// workaround for IDEA's locks shortcoming: timeout on write action
Thread.interrupted();
final DelayedInterrupt delayedInterrupt = interruptLater(Thread.currentThread(), WAIT_FOR_WRITE_LOCK_MS, MILLISECONDS);
try {
myWriteActionTracker.writeActionScheduled();
ApplicationManager.getApplication().runWriteAction(() -> {
cancelInterrupt(delayedInterrupt);
myRunnable.run();
});
} catch (RuntimeException re) {
dealWithRuntimeException(delayedInterrupt, re);
} finally {
myWriteActionTracker.writeActionProcessed();
}
}
private void dealWithRuntimeException(DelayedInterrupt delayedInterrupt, RuntimeException re) throws WriteTimeOutException {
cancelInterrupt(delayedInterrupt);
RuntimeException cause = getCause(re);
if (cause.getCause() instanceof InterruptedException) {
if (delayedInterrupt.isInterruptSuccessful()) {
throw new WriteTimeOutException(cause.getCause());
} else {
throw cause;
}
} else {
throw cause;
}
}
@NotNull
private RuntimeException getCause(RuntimeException re) {
while (re.getCause() instanceof RuntimeException) {
re = (RuntimeException) re.getCause();
}
return re;
}
}
private static class DelayedInterrupt implements Delayed {
private final long myAlarmTimeNanos;
private final Thread myToInterrupt;
private boolean myInterruptSuccess;
private DelayedInterrupt(@NotNull Thread toInterrupt, long delay, TimeUnit unit) {
myToInterrupt = toInterrupt;
myAlarmTimeNanos = System.nanoTime() + unit.toNanos(delay);
}
private void timeIsUp() {
myInterruptSuccess = myToInterrupt.isInterrupted();
}
boolean isInterruptSuccessful() {
return myInterruptSuccess;
}
@Override
public long getDelay(@NotNull TimeUnit unit) {
return unit.convert(myAlarmTimeNanos - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(@NotNull Delayed that) {
if (!(that instanceof DelayedInterrupt)) {
throw new ClassCastException();
}
return (int) (myAlarmTimeNanos - ((DelayedInterrupt) that).myAlarmTimeNanos);
}
}
}