From f0d1d64f669d07bb6dd037508d2bb31187d8bba0 Mon Sep 17 00:00:00 2001 From: Peter Gromov Date: Wed, 4 Dec 2019 09:24:52 +0100 Subject: [PATCH] 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 --- .../intellij/openapi/project/DumbService.java | 2 + .../FileBasedIndexProjectHandler.java | 11 ++- .../application/impl/ApplicationImpl.java | 25 ++--- .../application/impl/ReadMostlyRWLock.java | 82 ++-------------- .../openapi/project/CacheUpdateRunner.java | 8 +- .../application/impl/ApplicationImplTest.java | 97 ------------------- .../project/DumbServiceImplTest.groovy | 53 ++++++++++ 7 files changed, 85 insertions(+), 193 deletions(-) diff --git a/platform/core-api/src/com/intellij/openapi/project/DumbService.java b/platform/core-api/src/com/intellij/openapi/project/DumbService.java index c010e639ac49..98a197742d35 100644 --- a/platform/core-api/src/com/intellij/openapi/project/DumbService.java +++ b/platform/core-api/src/com/intellij/openapi/project/DumbService.java @@ -248,6 +248,8 @@ public abstract class DumbService { * (which could start "dumb mode") some reference resolve is required (which again requires "smart mode").

*

* 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(); diff --git a/platform/lang-impl/src/com/intellij/util/indexing/FileBasedIndexProjectHandler.java b/platform/lang-impl/src/com/intellij/util/indexing/FileBasedIndexProjectHandler.java index 8e0f96bb20e9..f0665c858e5b 100644 --- a/platform/lang-impl/src/com/intellij/util/indexing/FileBasedIndexProjectHandler.java +++ b/platform/lang-impl/src/com/intellij/util/indexing/FileBasedIndexProjectHandler.java @@ -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 files, - final Project project, - final FileBasedIndexImpl index) { + @VisibleForTesting + @ApiStatus.Internal + public static void reindexRefreshedFiles(ProgressIndicator indicator, Collection files, Project project) { + FileBasedIndexImpl index = (FileBasedIndexImpl)FileBasedIndex.getInstance(); CacheUpdateRunner.processFiles(indicator, files, project, content -> index.processRefreshedFile(project, content)); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java index 2b38a21fd6be..63e63ca0a503 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java @@ -226,7 +226,6 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App @NotNull @Override public Future executeOnPooledThread(@SuppressWarnings("BoundedWildcard") @NotNull Callable action) { - ReadMostlyRWLock.SuspensionId suspensionId = myLock.currentReadPrivilege(); return ourThreadExecutorsService.submit(new Callable() { @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; } diff --git a/platform/platform-impl/src/com/intellij/openapi/application/impl/ReadMostlyRWLock.java b/platform/platform-impl/src/com/intellij/openapi/application/impl/ReadMostlyRWLock.java index 986778a629bc..2a8f5efe2c93 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/impl/ReadMostlyRWLock.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/impl/ReadMostlyRWLock.java @@ -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 readers = ContainerUtil.createConcurrentList(); - private final Map 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 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 + '}'; } } diff --git a/platform/platform-impl/src/com/intellij/openapi/project/CacheUpdateRunner.java b/platform/platform-impl/src/com/intellij/openapi/project/CacheUpdateRunner.java index 15ca31700594..e369b2d44bf6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/CacheUpdateRunner.java +++ b/platform/platform-impl/src/com/intellij/openapi/project/CacheUpdateRunner.java @@ -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 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); diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java index f842df17f7a3..88bb1c5d61fb 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java @@ -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> futures = new ArrayList<>(); - - ApplicationImpl app = (ApplicationImpl)ApplicationManager.getApplication(); - List 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 = 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 = 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(); diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/project/DumbServiceImplTest.groovy b/platform/platform-tests/testSrc/com/intellij/openapi/project/DumbServiceImplTest.groovy index 031f268bdcc8..d9d26c860323 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/project/DumbServiceImplTest.groovy +++ b/platform/platform-tests/testSrc/com/intellij/openapi/project/DumbServiceImplTest.groovy @@ -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() + } }