don't let PCE during free-threaded document change lead to leaks and eternal semi-committedness (IDEA-182419)

This commit is contained in:
peter
2018-01-03 16:58:26 +01:00
parent d7cf5d79de
commit d4414dc4ee
2 changed files with 64 additions and 20 deletions
@@ -705,17 +705,45 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
assertNotNestedModification();
myChangeInProgress = true;
DelayedExceptions exceptions = new DelayedExceptions();
try {
DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced, initialStartOffset, initialOldLength);
beforeChangedUpdate(event);
beforeChangedUpdate(event, exceptions);
myTextString = null;
ImmutableCharSequence prevText = myText;
myText = newText;
sequence.incrementAndGet(); // increment sequence before firing events so that modification sequence on commit will match this sequence now
changedUpdate(event, newModificationStamp, prevText);
changedUpdate(event, newModificationStamp, prevText, exceptions);
}
finally {
myChangeInProgress = false;
exceptions.rethrowPCE();
}
}
private class DelayedExceptions {
Throwable myException = null;
void register(Throwable e) {
if (myException == null) {
myException = e;
} else {
myException.addSuppressed(e);
}
if (!(e instanceof ProcessCanceledException)) {
LOG.error(e);
}
else if (myAssertThreading) {
LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e));
}
}
void rethrowPCE() {
if (myException instanceof ProcessCanceledException) {
// the case of some wise inspection modifying non-physical document during highlighting to be interrupted
throw (ProcessCanceledException)myException;
}
}
}
@@ -724,7 +752,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
return sequence.get();
}
private void beforeChangedUpdate(DocumentEvent event) {
private void beforeChangedUpdate(DocumentEvent event, DelayedExceptions exceptions) {
Application app = ApplicationManager.getApplication();
if (app != null) {
FileDocumentManager manager = FileDocumentManager.getInstance();
@@ -743,11 +771,8 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
try {
listeners[i].beforeDocumentChange(event);
}
catch (ProcessCanceledException e) {
throw e; // the case of some wise inspection modifying non-physical document during highlighting to be interrupted
}
catch (Throwable e) {
LOG.error(e);
exceptions.register(e);
}
}
}
@@ -764,7 +789,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
}
}
private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText) {
private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText, DelayedExceptions exceptions) {
try {
if (LOG.isDebugEnabled()) LOG.debug(event.toString());
@@ -784,16 +809,8 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
try {
listener.documentChanged(event);
}
catch (ProcessCanceledException e) {
if (!myAssertThreading) {
throw e;
}
else {
LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e));
}
}
catch (Throwable e) {
LOG.error(e);
exceptions.register(e);
}
}
}
@@ -26,8 +26,10 @@ import com.intellij.openapi.application.impl.LaterInvocator;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.ex.PrioritizedDocumentListener;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.TrailingSpacesStripper;
@@ -35,8 +37,10 @@ import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileTypes.PlainTextFileType;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
@@ -807,7 +811,7 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase {
Document document = file.getViewProvider().getDocument();
Semaphore semaphore = new Semaphore(1);
TransactionGuard.submitTransaction(getTestRootDisposable(), () -> {
TransactionGuard.submitTransaction(getTestRootDisposable(), () ->
WriteCommandAction.runWriteCommandAction(myProject, () -> {
document.insertString(0, "x");
@@ -817,8 +821,7 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase {
getPsiDocumentManager().commitAndRunReadAction(() -> semaphore.up());
}
}, new ProgressWindow(false, myProject));
});
});
}));
int iteration = 0;
while (!semaphore.waitFor(10)) {
UIUtil.dispatchAllInvocationEvents();
@@ -829,6 +832,30 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase {
}
}
public void testNoLeaksAfterPCEInListener() {
PsiFile file = PsiFileFactory.getInstance(myProject).createFileFromText("a.txt", PlainTextFileType.INSTANCE, "");
Document document = file.getViewProvider().getDocument();
document.addDocumentListener(new PrioritizedDocumentListener() {
@Override
public int getPriority() {
return 0;
}
@Override
public void beforeDocumentChange(DocumentEvent event) {
throw new ProcessCanceledException();
}
});
try {
document.insertString(0, "a");
fail("PCE expected");
}
catch (ProcessCanceledException ignored) {
}
waitTenSecondsForCommit(document);
LeakHunter.checkLeak(getPsiDocumentManager(), Document.class, d -> d == document);
}
private static void assertLargeFileContentLimited(@NotNull String content, @NotNull VirtualFile vFile, @NotNull Document document) {
Charset charset = EncodingManager.getInstance().getEncoding(vFile, false);
float bytesPerChar = charset == null ? 2 : charset.newEncoder().averageBytesPerChar();