[threading] CPP-51261: Handle temporary release of write-intent lock during suspending write action

(cherry picked from commit 5241fb3698fd9caee14555e4bf5a9762f8aef058)


(cherry picked from commit d8408b5a7d5e82b72d6cbf60d3aca07fcaa272d5)

IJ-MR-219976

GitOrigin-RevId: 4cd9e91df336a1bc9809177ea1752da5b17397ee
This commit is contained in:
Konstantin Nisht
2026-08-26 20:10:29 +00:00
committed by intellij-monorepo-bot
parent e16aa85075
commit 0d5ce4a978
2 changed files with 31 additions and 2 deletions
@@ -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
}
@@ -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
}
}