diff --git a/platform/locking-impl/src/NestedLocksThreadingSupport.kt b/platform/locking-impl/src/NestedLocksThreadingSupport.kt index b76f65c8d196..3d2ed8907e32 100644 --- a/platform/locking-impl/src/NestedLocksThreadingSupport.kt +++ b/platform/locking-impl/src/NestedLocksThreadingSupport.kt @@ -1336,6 +1336,10 @@ class NestedLocksThreadingSupport : ThreadingSupport { return object : AccessToken() { override fun finish() { myWriteActionPending.get()[state.level()].incrementAndGet() + val newThisLevelPermit = state.getThisThreadPermit() + require(newThisLevelPermit is ParallelizablePermit.WriteIntent) { + "When suspending write action is finishing, the thread must hold write-intent lock" + } val (newWritePermits, newWritePermit) = try { myWriteLockReacquisitionListener.zip(listOfReacquisitionData).forEachGuaranteed { (listener, data) -> @Suppress("UNCHECKED_CAST") @@ -1343,7 +1347,7 @@ class NestedLocksThreadingSupport : ThreadingSupport { castedListener.beforeWriteLockReacquired(data) } val newWritePermit = runSuspendMaybeConsuming(false) { - rootWriteIntentPermit.acquireWriteActionPermit() + newThisLevelPermit.writeIntentPermit.acquireWriteActionPermit() } myWriteLockReacquisitionListener.zip(listOfReacquisitionData).forEachGuaranteed { (listener, data) -> @Suppress("UNCHECKED_CAST") @@ -1361,7 +1365,7 @@ class NestedLocksThreadingSupport : ThreadingSupport { finally { myWriteActionPending.get()[state.level()].decrementAndGet() } - hack_setPublishedPermitData(exposedPermitData.copy(writePermitStack = newWritePermits, finalWritePermit = newWritePermit)) + hack_setPublishedPermitData(exposedPermitData.copy(writePermitStack = newWritePermits, finalWritePermit = newWritePermit, originalWriteIntentPermit = newThisLevelPermit.writeIntentPermit, oldPermit = newThisLevelPermit.writeIntentPermit)) myWriteAcquired = Thread.currentThread() myWriteStackBase = prevBase } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/SuspendingWriteActionTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/SuspendingWriteActionTest.kt index 040aaab4fc3d..9331965266bd 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/SuspendingWriteActionTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/SuspendingWriteActionTest.kt @@ -2,6 +2,7 @@ package com.intellij.openapi.application.impl import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.WriteActionListener import com.intellij.openapi.application.backgroundWriteAction import com.intellij.openapi.application.edtWriteAction @@ -22,6 +23,7 @@ import com.intellij.openapi.util.use import com.intellij.testFramework.common.timeoutRunBlocking import com.intellij.testFramework.junit5.TestApplication import com.intellij.util.ThrowableRunnable +import com.intellij.util.application import com.intellij.util.ui.EDT import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -38,6 +40,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference +import kotlin.test.assertFalse +import kotlin.test.assertTrue private const val repetitions: Int = 100 @@ -230,4 +234,25 @@ class SuspendingWriteActionTest { } checkpoint(3) } + + @Test + fun `release of WI inside suspending write action does not lead to broken IDE state`(): Unit = timeoutRunBlocking { + readAction { } // init internal structures + edtWriteAction { + assertTrue { application.isWriteAccessAllowed } + ApplicationManagerEx.getApplicationEx().threadingSupport!!.executeSuspendingWriteAction { + assertFalse { application.isWriteAccessAllowed } + assertTrue { application.isWriteIntentLockAcquired } + TestOnlyThreading.releaseTheAcquiredWriteIntentLockThenExecuteActionAndTakeWriteIntentLockBack { + assertFalse { application.isWriteAccessAllowed } + assertFalse { application.isWriteIntentLockAcquired } + } + assertFalse { application.isWriteAccessAllowed } + assertTrue { application.isWriteIntentLockAcquired } + } + assertTrue { application.isWriteAccessAllowed } + assertTrue { application.isWriteIntentLockAcquired } + } + withContext(Dispatchers.EDT) {} // check that WI can be acquired again + } }