mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
remove app read lock privileges
because they're complicated and produce deadlocks during modal indexing when FileContentQueue needs encoding which needs a read action (e.g. JSP) GitOrigin-RevId: b85fbeea47f724fdc8cd8333d9a87e33ac7ceeaf
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f21bbb94a3
commit
f0d1d64f66
@@ -248,6 +248,8 @@ public abstract class DumbService {
|
||||
* (which could start "dumb mode") some reference resolve is required (which again requires "smart mode").<p/>
|
||||
* <p>
|
||||
* Should be invoked on dispatch thread.
|
||||
* It's the caller's responsibility to invoke this method only when the model is in internally consistent state,
|
||||
* so that background threads with read actions don't see half-baked PSI/VFS/etc.
|
||||
*/
|
||||
public abstract void completeJustSubmittedTasks();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
package com.intellij.util.indexing;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.intellij.diagnostic.PerformanceWatcher;
|
||||
import com.intellij.ide.IdeBundle;
|
||||
import com.intellij.openapi.Disposable;
|
||||
@@ -129,7 +130,7 @@ public final class FileBasedIndexProjectHandler implements IndexableFileSet, Dis
|
||||
LOG.info("Reindexing refreshed files: " + files.size() + " to update, calculated in " + calcDuration + "ms");
|
||||
if (!files.isEmpty()) {
|
||||
PerformanceWatcher.Snapshot snapshot = PerformanceWatcher.takeSnapshot();
|
||||
reindexRefreshedFiles(indicator, files, project, index);
|
||||
reindexRefreshedFiles(indicator, files, project);
|
||||
snapshot.logResponsivenessSinceCreation("Reindexing refreshed files");
|
||||
}
|
||||
}
|
||||
@@ -177,10 +178,10 @@ public final class FileBasedIndexProjectHandler implements IndexableFileSet, Dis
|
||||
});
|
||||
}
|
||||
|
||||
private static void reindexRefreshedFiles(ProgressIndicator indicator,
|
||||
Collection<VirtualFile> files,
|
||||
final Project project,
|
||||
final FileBasedIndexImpl index) {
|
||||
@VisibleForTesting
|
||||
@ApiStatus.Internal
|
||||
public static void reindexRefreshedFiles(ProgressIndicator indicator, Collection<VirtualFile> files, Project project) {
|
||||
FileBasedIndexImpl index = (FileBasedIndexImpl)FileBasedIndex.getInstance();
|
||||
CacheUpdateRunner.processFiles(indicator, files, project, content -> index.processRefreshedFile(project, content));
|
||||
}
|
||||
}
|
||||
|
||||
+9
-16
@@ -226,7 +226,6 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App
|
||||
@NotNull
|
||||
@Override
|
||||
public <T> Future<T> executeOnPooledThread(@SuppressWarnings("BoundedWildcard") @NotNull Callable<T> action) {
|
||||
ReadMostlyRWLock.SuspensionId suspensionId = myLock.currentReadPrivilege();
|
||||
return ourThreadExecutorsService.submit(new Callable<T>() {
|
||||
@Override
|
||||
public T call() {
|
||||
@@ -234,16 +233,7 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is very special magic only needed by threads that need read actions and can be executed
|
||||
// during "executeSuspendingWriteAction" (e.g. dumb mode, indexing). Threads created via "executeOnPooledThread"
|
||||
// in these circumstances may run read actions immediately, instead of waiting until the write action is resumed and finished.
|
||||
|
||||
// For everyone else, "executeOnPooledThread" should be equivalent to "AppExecutorUtil" AKA "PooledThreadExecutor" pool
|
||||
try (AccessToken ignored = myLock.applyReadPrivilege(suspensionId)) {
|
||||
if (isDisposed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return action.call();
|
||||
}
|
||||
catch (ProcessCanceledException e) {
|
||||
@@ -1175,6 +1165,13 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App
|
||||
return myLock.isWriteLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* If called inside a write action, executes the given code under a modal progress with write lock released (e.g. to allow for read-action parallelization).
|
||||
* It's the caller's responsibility to invoke this method only when the model is in internally consistent state,
|
||||
* so that background threads with read actions don't see half-baked PSI/VFS/etc. The runnable may perform write actions itself,
|
||||
* callers should be ready for those.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void executeSuspendingWriteAction(@Nullable Project project, @NotNull String title, @NotNull Runnable runnable) {
|
||||
assertIsDispatchThread();
|
||||
if (!myLock.isWriteLocked()) {
|
||||
@@ -1185,11 +1182,7 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App
|
||||
int prevBase = myWriteStackBase;
|
||||
myWriteStackBase = myWriteActionsStack.size();
|
||||
try (AccessToken ignored = myLock.writeSuspend()) {
|
||||
runModalProgress(project, title, () -> {
|
||||
try (AccessToken ignored1 = myLock.grantReadPrivilege()) {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
runModalProgress(project, title, runnable);
|
||||
} finally {
|
||||
myWriteStackBase = prevBase;
|
||||
}
|
||||
|
||||
+9
-73
@@ -15,22 +15,16 @@
|
||||
*/
|
||||
package com.intellij.openapi.application.impl;
|
||||
|
||||
import com.intellij.diagnostic.ThreadDumper;
|
||||
import com.intellij.openapi.application.AccessToken;
|
||||
import com.intellij.openapi.application.ex.ApplicationUtil;
|
||||
import com.intellij.openapi.diagnostic.Attachment;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.progress.impl.CoreProgressManager;
|
||||
import com.intellij.util.containers.ConcurrentList;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
@@ -47,16 +41,13 @@ import java.util.concurrent.locks.LockSupport;
|
||||
* Write lock: sets global {@link #writeRequested} bit and waits for all readers (in global {@link #readers} list) to release their locks by checking {@link Reader#readRequested} for all readers.
|
||||
*/
|
||||
class ReadMostlyRWLock {
|
||||
private static final Logger LOG = Logger.getInstance(ReadMostlyRWLock.class);
|
||||
private final Thread writeThread;
|
||||
volatile boolean writeRequested; // this writer is requesting or obtained the write access
|
||||
private volatile boolean writeAcquired; // this writer obtained the write lock
|
||||
// All reader threads are registered here. Dead readers are garbage collected in writeUnlock().
|
||||
private final ConcurrentList<Reader> readers = ContainerUtil.createConcurrentList();
|
||||
|
||||
private final Map<Thread, SuspensionId> privilegedReaders = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile SuspensionId currentSuspension;
|
||||
private volatile boolean writeSuspended;
|
||||
|
||||
ReadMostlyRWLock(@NotNull Thread writeThread) {
|
||||
this.writeThread = writeThread;
|
||||
@@ -112,11 +103,11 @@ class ReadMostlyRWLock {
|
||||
Reader status = R.get();
|
||||
throwIfImpatient(status);
|
||||
|
||||
if (tryReadLock(status, true)) {
|
||||
if (tryReadLock(status)) {
|
||||
return;
|
||||
}
|
||||
for (int iter = 0; ; iter++) {
|
||||
if (tryReadLock(status, true)) {
|
||||
if (tryReadLock(status)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -183,15 +174,12 @@ class ReadMostlyRWLock {
|
||||
boolean tryReadLock() {
|
||||
checkReadThreadAccess();
|
||||
Reader status = R.get();
|
||||
return tryReadLock(status, true);
|
||||
return tryReadLock(status);
|
||||
}
|
||||
|
||||
private boolean tryReadLock(Reader status, boolean checkPrivileges) {
|
||||
private boolean tryReadLock(Reader status) {
|
||||
throwIfImpatient(status);
|
||||
if (!writeRequested) {
|
||||
if (checkPrivileges && currentSuspension != null && !privilegedReaders.containsKey(Thread.currentThread())) {
|
||||
return false;
|
||||
}
|
||||
status.readRequested = true;
|
||||
if (!writeRequested) {
|
||||
return true;
|
||||
@@ -225,63 +213,14 @@ class ReadMostlyRWLock {
|
||||
}
|
||||
|
||||
AccessToken writeSuspend() {
|
||||
SuspensionId prevSuspension = currentSuspension;
|
||||
if (prevSuspension == null) {
|
||||
currentSuspension = new SuspensionId();
|
||||
}
|
||||
boolean prev = writeSuspended;
|
||||
writeSuspended = true;
|
||||
writeUnlock();
|
||||
return new AccessToken() {
|
||||
@Override
|
||||
public void finish() {
|
||||
writeLock();
|
||||
currentSuspension = prevSuspension;
|
||||
if (prevSuspension == null) {
|
||||
ensureNoPrivilegedReaders();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ensureNoPrivilegedReaders() {
|
||||
if (!privilegedReaders.isEmpty()) {
|
||||
List<String> offenderNames = ContainerUtil.map(privilegedReaders.keySet(), Thread::getName);
|
||||
privilegedReaders.clear();
|
||||
LOG.error("Pooled threads created during write action suspension should have been terminated: " + offenderNames,
|
||||
new Attachment("threadDump.txt", ThreadDumper.dumpThreadsToString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
SuspensionId currentReadPrivilege() {
|
||||
return privilegedReaders.get(Thread.currentThread());
|
||||
}
|
||||
|
||||
@NotNull AccessToken applyReadPrivilege(@Nullable SuspensionId context) {
|
||||
Reader status = R.get();
|
||||
int iter = 0;
|
||||
while (context != null && context == currentSuspension) {
|
||||
if (tryReadLock(status, false)) {
|
||||
try {
|
||||
return context == currentSuspension ? grantReadPrivilege() : AccessToken.EMPTY_ACCESS_TOKEN;
|
||||
}
|
||||
finally {
|
||||
readUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
waitABit(status, iter++);
|
||||
}
|
||||
return AccessToken.EMPTY_ACCESS_TOKEN;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
AccessToken grantReadPrivilege() {
|
||||
Thread thread = Thread.currentThread();
|
||||
privilegedReaders.put(thread, currentSuspension);
|
||||
return new AccessToken() {
|
||||
@Override
|
||||
public void finish() {
|
||||
privilegedReaders.remove(thread);
|
||||
writeSuspended = prev;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -328,8 +267,6 @@ class ReadMostlyRWLock {
|
||||
return writeAcquired;
|
||||
}
|
||||
|
||||
static class SuspensionId {}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReadMostlyRWLock{" +
|
||||
@@ -337,8 +274,7 @@ class ReadMostlyRWLock {
|
||||
", writeRequested=" + writeRequested +
|
||||
", writeAcquired=" + writeAcquired +
|
||||
", readers=" + readers +
|
||||
", privilegedReaders=" + privilegedReaders +
|
||||
", currentSuspension=" + currentSuspension +
|
||||
", writeSuspended=" + writeSuspended +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.intellij.openapi.progress.ProcessCanceledException;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
|
||||
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
|
||||
import com.intellij.openapi.progress.util.ProgressWrapper;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.util.Key;
|
||||
@@ -45,6 +46,8 @@ public class CacheUpdateRunner {
|
||||
final double total = files.size();
|
||||
queue.startLoading();
|
||||
|
||||
indicator.setIndeterminate(false);
|
||||
|
||||
ProgressUpdater progressUpdater = new ProgressUpdater() {
|
||||
final Set<VirtualFile> myFilesBeingProcessed = new THashSet<>();
|
||||
final AtomicInteger myNumberOfFilesProcessed = new AtomicInteger();
|
||||
@@ -163,7 +166,7 @@ public class CacheUpdateRunner {
|
||||
assert !ApplicationManager.getApplication().isWriteAccessAllowed();
|
||||
try {
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
ProgressIndicatorUtils.awaitWithCheckCanceled(future);
|
||||
}
|
||||
|
||||
boolean allFinished = true;
|
||||
@@ -175,7 +178,8 @@ public class CacheUpdateRunner {
|
||||
}
|
||||
return allFinished;
|
||||
}
|
||||
catch (InterruptedException ignored) {
|
||||
catch (ProcessCanceledException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
LOG.error(throwable);
|
||||
|
||||
-97
@@ -585,39 +585,6 @@ public class ApplicationImplTest extends LightPlatformTestCase {
|
||||
if (e.get() != null) throw e.get();
|
||||
}
|
||||
|
||||
public void testSuspendWriteActionDelaysForeignReadActions() throws Throwable {
|
||||
Semaphore mayStartForeignRead = new Semaphore();
|
||||
mayStartForeignRead.down();
|
||||
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
|
||||
ApplicationImpl app = (ApplicationImpl)ApplicationManager.getApplication();
|
||||
List<String> log = Collections.synchronizedList(new ArrayList<>());
|
||||
futures.add(app.executeOnPooledThread(() -> {
|
||||
assertTrue(mayStartForeignRead.waitFor(1000));
|
||||
ReadAction.run(() -> log.add("foreign read"));
|
||||
}));
|
||||
|
||||
safeWrite(() -> {
|
||||
log.add("write started");
|
||||
app.executeSuspendingWriteAction(getProject(), "", () -> {
|
||||
app.invokeAndWait(() ->
|
||||
futures.add(app.executeOnPooledThread(() -> ReadAction.run(() -> log.add("foreign read")))));
|
||||
|
||||
mayStartForeignRead.up();
|
||||
TimeoutUtil.sleep(50);
|
||||
|
||||
ReadAction.run(() -> log.add("progress read"));
|
||||
app.invokeAndWait(() -> WriteAction.run(() -> log.add("nested write")));
|
||||
waitForFuture(app.executeOnPooledThread(() -> ReadAction.run(() -> log.add("forked read"))));
|
||||
});
|
||||
log.add("write finished");
|
||||
});
|
||||
|
||||
futures.forEach(ApplicationImplTest::waitForFuture);
|
||||
assertOrderedEquals(log, "write started", "progress read", "nested write", "forked read", "write finished", "foreign read", "foreign read");
|
||||
}
|
||||
|
||||
private static void waitForFuture(Future<?> future) {
|
||||
try {
|
||||
future.get(10_000, TimeUnit.MILLISECONDS);
|
||||
@@ -645,70 +612,6 @@ public class ApplicationImplTest extends LightPlatformTestCase {
|
||||
safeWrite(runnable);
|
||||
}
|
||||
|
||||
public void testPooledThreadsThatHappenInSuspendedWriteActionStayInSuspendedWriteAction() throws Throwable {
|
||||
LoggedErrorProcessor.getInstance().disableStderrDumping(getTestRootDisposable());
|
||||
|
||||
Ref<Future<?>> future = Ref.create();
|
||||
ApplicationImpl app = (ApplicationImpl)ApplicationManager.getApplication();
|
||||
safeWrite(() -> {
|
||||
try {
|
||||
Semaphore started = new Semaphore();
|
||||
started.down();
|
||||
app.executeSuspendingWriteAction(getProject(), "", () -> {
|
||||
future.set(app.executeOnPooledThread(() -> {
|
||||
started.up();
|
||||
TimeoutUtil.sleep(1000);
|
||||
}));
|
||||
assertTrue(started.waitFor(1000));
|
||||
});
|
||||
fail("should not allow pooled thread to stay there");
|
||||
}
|
||||
catch (AssertionError e) {
|
||||
assertTrue(ExceptionUtil.getThrowableText(e), isEscapingThreadAssertion(e));
|
||||
}
|
||||
});
|
||||
waitForFuture(future.get());
|
||||
}
|
||||
|
||||
public void testPooledThreadsStartedAfterQuickSuspendedWriteActionDontGetReadPrivileges() throws Throwable {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
safeWrite(this::checkPooledThreadsDontGetWrongPrivileges);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPooledThreadsDontGetWrongPrivileges() {
|
||||
ApplicationImpl app = (ApplicationImpl)ApplicationManager.getApplication();
|
||||
Ref<Future<?>> future = Ref.create();
|
||||
|
||||
Disposable disableStderrDumping = Disposer.newDisposable();
|
||||
LoggedErrorProcessor.getInstance().disableStderrDumping(disableStderrDumping);
|
||||
|
||||
Semaphore mayFinish = new Semaphore();
|
||||
mayFinish.down();
|
||||
try {
|
||||
app.executeSuspendingWriteAction(getProject(), "", () ->
|
||||
future.set(app.executeOnPooledThread(
|
||||
() -> assertTrue(mayFinish.waitFor(5_000)))));
|
||||
}
|
||||
catch (AssertionError e) {
|
||||
if (!isEscapingThreadAssertion(e)) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Disposer.dispose(disableStderrDumping);
|
||||
}
|
||||
|
||||
app.executeSuspendingWriteAction(getProject(), "", () -> {});
|
||||
mayFinish.up();
|
||||
waitForFuture(future.get());
|
||||
}
|
||||
|
||||
private static boolean isEscapingThreadAssertion(AssertionError e) {
|
||||
return e.getMessage().contains("should have been terminated");
|
||||
}
|
||||
|
||||
public void testReadActionInImpatientModeShouldThrowWhenThereIsAPendingWrite() throws Throwable {
|
||||
AtomicBoolean stopRead = new AtomicBoolean();
|
||||
AtomicBoolean readAcquired = new AtomicBoolean();
|
||||
|
||||
+53
@@ -16,12 +16,23 @@
|
||||
package com.intellij.openapi.project
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileImpl
|
||||
import com.intellij.psi.impl.PsiManagerImpl
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl
|
||||
import com.intellij.util.TimeoutUtil
|
||||
import com.intellij.util.concurrency.Semaphore
|
||||
import com.intellij.util.indexing.FileBasedIndexProjectHandler
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.jetbrains.annotations.NotNull
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* @author peter
|
||||
*/
|
||||
@@ -64,4 +75,46 @@ class DumbServiceImplTest extends BasePlatformTestCase {
|
||||
private DumbServiceImpl getDumbService() {
|
||||
(DumbServiceImpl)DumbService.getInstance(project)
|
||||
}
|
||||
|
||||
void "test no deadlocks when indexing JSP modally"() {
|
||||
def tempFixture = new TempDirTestFixtureImpl()
|
||||
disposeOnTearDown { tempFixture.tearDown() }
|
||||
|
||||
// create externally and carefully refresh, avoiding eager content loading and charset detection
|
||||
def dir = new File(tempFixture.tempDirPath + '/jsps')
|
||||
dir.mkdirs()
|
||||
new File(dir, 'a.jsp').createNewFile()
|
||||
|
||||
def vDir = LocalFileSystem.instance.refreshAndFindFileByIoFile(dir)
|
||||
assert vDir != null
|
||||
assert vDir.children.length == 1
|
||||
def child = vDir.children[0]
|
||||
assert child.fileType.name == 'JSP'
|
||||
assert !((VirtualFileImpl) child).charsetSet
|
||||
assert ((PsiManagerImpl)psiManager).fileManager.getCachedPsiFile(child) == null
|
||||
|
||||
def started = new AtomicBoolean()
|
||||
def finished = new AtomicBoolean()
|
||||
|
||||
dumbService.queueAsynchronousTask(new DumbModeTask() {
|
||||
@Override
|
||||
void performInDumbMode(@NotNull ProgressIndicator indicator) {
|
||||
started.set(true)
|
||||
assert !ApplicationManager.application.dispatchThread
|
||||
try {
|
||||
ProgressIndicatorUtils.withTimeout(20_000) {
|
||||
FileBasedIndexProjectHandler.reindexRefreshedFiles(indicator, [child], project)
|
||||
}
|
||||
}
|
||||
catch (ProcessCanceledException e) {
|
||||
throw new RuntimeException("Successful indexing expected", e)
|
||||
}
|
||||
finished.set(true)
|
||||
}
|
||||
})
|
||||
assert !started.get()
|
||||
WriteAction.run { dumbService.completeJustSubmittedTasks() }
|
||||
assert started.get()
|
||||
assert finished.get()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user