[threading] IJPL-176722: Do not enable read access for runnables in runWithModalProgressBlocking

This violates the mutual exclusion principle of the RW lock, as within `runWithModalProgressBlocking` there can be an upgrade to WA

GitOrigin-RevId: 7af9e9dae003cfe11f314a24ed764b9210e93b4d
This commit is contained in:
Konstantin Nisht
2025-02-13 10:48:22 +00:00
committed by intellij-monorepo-bot
parent 3be3c53d34
commit c8c6becc72
3 changed files with 83 additions and 4 deletions
@@ -142,6 +142,9 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport {
@Volatile
private var myWriteAcquired: Thread? = null
@Volatile
private var myWriteIntentAcquired: Thread? = null
override fun getPermitAsContextElement(baseContext: CoroutineContext, shared: Boolean): CoroutineContext {
if (!isLockStoredInContext) {
return EmptyCoroutineContext
@@ -247,7 +250,10 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport {
}
when (ts.permit) {
null -> ts.acquire(getWriteIntentPermit())
null -> {
ts.acquire(getWriteIntentPermit())
myWriteIntentAcquired = Thread.currentThread()
}
is ReadPermit -> error("WriteIntentReadAction can not be called from ReadAction")
is WriteIntentPermit -> {
// Volatile read
@@ -271,6 +277,7 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport {
ThreadingAssertions.setImplicitLockOnEDT(prevImplicitLock)
if (release) {
ts.release()
myWriteIntentAcquired = null
}
if (releaseSecondary) {
mySecondaryPermits.get().removeLast().release()
@@ -282,10 +289,30 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport {
override fun isWriteIntentLocked(): Boolean {
val ts = myState.get()
return ts.hasWrite || ts.hasWriteIntent
val shared = ts.sharedLock
if (shared == null) {
return ts.hasWrite || ts.hasWriteIntent
}
else {
return myWriteIntentAcquired == Thread.currentThread()
}
}
override fun isReadAccessAllowed(): Boolean = getThreadState().hasPermit
override fun isReadAccessAllowed(): Boolean {
val threadState = getThreadState()
val shared = threadState.sharedLock
if (shared == null) {
// Having any permit (r/w/wi) without the presence of the second lock means that this thread has _some_ permission,
// and even the weakest possible permission allows read access
return threadState.hasPermit
}
else {
// When there is the second lock installed, it is not enough to look at the primary lock:
// the current thread now has inherited write intent permit, which should not give read access.
// Otherwise, it would be impossible to upgrade WI to W
return !mySecondaryPermits.get().isNullOrEmpty()
}
}
override fun executeOnPooledThread(action: Runnable, expired: BooleanSupplier): Future<*> {
val actionDecorated = decorateRunnable(action)
@@ -402,7 +402,7 @@ public class ApplicationImplTest extends LightPlatformTestCase {
ProgressManager.getInstance().runProcessWithProgressSynchronously((ThrowableComputable<Void, Exception>)() -> {
try {
assertFalse(ApplicationManager.getApplication().holdsReadLock());
assertEquals(isLockStoredInContext(), ApplicationManager.getApplication().isReadAccessAllowed());
assertFalse(ApplicationManager.getApplication().isReadAccessAllowed());
assertFalse(ApplicationManager.getApplication().isDispatchThread());
ApplicationManager.getApplication().assertIsNonDispatchThread();
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.lang.Runnable
import kotlin.coroutines.ContinuationInterceptor
import kotlin.time.Duration.Companion.seconds
@@ -446,6 +447,57 @@ class RunWithModalProgressBlockingTest : ModalCoroutineTest() {
assertTrue { application.isReadAccessAllowed }
}
}
@Suppress("ForbiddenInSuspectContextMethod")
@Test
fun `RA and WA are mutually exclusive inside modal progress`(): Unit = timeoutRunBlocking(context = Dispatchers.EDT) {
val writeCoroutineStarted = Job()
val writeActionCanStart = Job()
val inWaCounter = AtomicBoolean(false)
writeIntentReadAction {
runWithModalProgressBlocking {
launch(Dispatchers.EDT) {
writeCoroutineStarted.complete()
writeActionCanStart.join()
writeAction {
inWaCounter.set(true)
}
}
ReadAction.nonBlocking<Unit> {
writeCoroutineStarted.asCompletableFuture().join()
writeActionCanStart.complete()
Thread.sleep(100)
assertFalse(inWaCounter.get())
}.executeSynchronously()
}
}
}
@Test
fun `read access is not allowed by default within modal progress`() = timeoutRunBlocking(context = Dispatchers.EDT) {
writeIntentReadAction {
runWithModalProgressBlocking {
assertFalse(ApplicationManager.getApplication().isReadAccessAllowed)
readAction {
assertTrue(ApplicationManager.getApplication().isReadAccessAllowed)
}
}
}
}
@Test
fun `write intent read access is not allowed by default within modal progress`() = timeoutRunBlocking(context = Dispatchers.EDT) {
writeIntentReadAction {
runWithModalProgressBlocking {
assertFalse(ApplicationManager.getApplication().isWriteIntentLockAcquired)
withContext(Dispatchers.EDT) {
assertTrue(ApplicationManager.getApplication().isWriteIntentLockAcquired)
}
}
}
}
}
private fun CoroutineScope.runWithModalProgressBlockingCoroutine(action: suspend CoroutineScope.() -> Unit): Job {