PY-88517 Make pythonSdkConfigurationMutex per-project to prevent cross-project notifications

The mutex was a global singleton whose isLocked StateFlow was observed by
every project's InterpreterFixExecutor. A lock state change from any
project fanned out updateAllNotifications() to all projects, triggering
stale cache refreshes with visible "Checking existing environments"
progress bars in idle projects.

Replace the global val with a per-project service so each project
observes only its own mutex state.


(cherry picked from commit cc14dce7d07231f6d7d4c3f654cdd8d3a2099ffc)

IJ-MR-197413

GitOrigin-RevId: 65753107b7fe9e654c99f7097f1f41c0499231dc
This commit is contained in:
Vitaly Legchilkin
2026-03-23 17:25:40 +00:00
committed by intellij-monorepo-bot
parent ed92591d08
commit e9f911fb1d
13 changed files with 33 additions and 21 deletions
@@ -35,7 +35,7 @@ internal class PySdkFromEnvironmentVariableConfigurator(private val project: Pro
}
}
private suspend fun checkAndSetSdk(project: Project, pycharmPythonPathEnvVariable: String) = pythonSdkConfigurationMutex.withLock {
private suspend fun checkAndSetSdk(project: Project, pycharmPythonPathEnvVariable: String) = project.pythonSdkConfigurationMutex.withLock {
withContext(Dispatchers.EDT) {
val sdk = PySdkFromEnvironmentVariable.findOrCreateSdkByPath(pycharmPythonPathEnvVariable) ?: return@withContext
@@ -175,7 +175,7 @@ private suspend fun createOrOpenProjectAndSdk(
title = PyCharmCommunityCustomizationBundle.message("misc.project.generating.env"),
cancellation = TaskCancellation.cancellable()
) {
pythonSdkConfigurationMutex.withLock {
project.pythonSdkConfigurationMutex.withLock {
createVenvAndSdk(ModuleOrProject.ProjectOnly(project), confirmInstallation, systemPythonService, vfsProjectPath)
}
}
@@ -50,7 +50,7 @@ internal class PyCharmWelcomeScreenProjectProvider : WelcomeScreenProjectProvide
if (PlatformProjectOpenProcessor.isNewProject(project)) {
// Don't prompt to install Python on the welcome screen (PY-88204).
// If Python is already available, the venv/SDK will be configured silently.
pythonSdkConfigurationMutex.withLock {
project.pythonSdkConfigurationMutex.withLock {
createVenvAndSdk(ModuleOrProject.ProjectOnly(project), confirmInstallation = { false }).orLogException(thisLogger())
}
}
@@ -91,7 +91,7 @@ internal class PythonLangSupport(private val errorSink: ErrorSink = ShowingMessa
@Throws(NoSdkException::class)
@RequiresEdt
override fun getSdkForProject(project: Project, selectedSdk: Sdk?): Sdk = runWithModalProgressBlocking(project, "...") {
pythonSdkConfigurationMutex.withLock {
project.pythonSdkConfigurationMutex.withLock {
when (val r = createVenvAndSdk(ModuleOrProject.ProjectOnly(project))) {
is Result.Failure -> {
errorSink.emit(r.error, project)
@@ -105,7 +105,7 @@ private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo
* Returns the configured SDK, or `null` if no SDK could be configured.
*/
@ApiStatus.Internal
suspend fun Module.autoConfigureSdkIfNeeded(): PyResult<Sdk>? = pythonSdkConfigurationMutex.withLock {
suspend fun Module.autoConfigureSdkIfNeeded(): PyResult<Sdk>? = project.pythonSdkConfigurationMutex.withLock {
val moduleInfo = getModuleInfo() ?: return@withLock null
when (moduleInfo) {
@@ -138,7 +138,7 @@ internal class ModulesSdkConfigurator private constructor(
* Errors are logged.
*
*/
suspend fun configureSdks(modulesOnly: Set<ModuleName>) = pythonSdkConfigurationMutex.withLock {
suspend fun configureSdks(modulesOnly: Set<ModuleName>) = project.pythonSdkConfigurationMutex.withLock {
withContext(Dispatchers.Default) {
val modulesMap = project.modules.associateBy { it.name }
val modulesWithSameSdk = mutableMapOf<Module, Module>()
@@ -197,7 +197,7 @@ suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Disp
when (pythonModules.size) {
0 -> return@withContext
1 -> pythonModules.first().autoConfigureSdkIfNeeded()?.orLogException(logger)
else -> pythonSdkConfigurationMutex.withLock {
else -> project.pythonSdkConfigurationMutex.withLock {
for (module in pythonModules) {
if (module.findPythonSdk() != null) continue
val sdkSuggestion = module.suggestSdk()
@@ -1,16 +1,29 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
/**
* Global mutex that serializes all Python SDK configuration operations — both
* Per-project mutex that serializes Python SDK configuration operations — both
* auto-configuration on startup and manual SDK creation from the UI.
*
* This is intentionally global (not per-module) because setting an SDK on one module
* This is per-project (not global) so that SDK configuration in one project
* does not trigger spurious "Checking existing environments" notifications in others.
*
* Within a project the mutex is still global (not per-module) because setting an SDK on one module
* can affect others via inherited project SDK in multi-module workspaces.
*
* Observe [isLocked][ObservableMutex.isLocked] to track whether an SDK configuration is running.
*/
@ApiStatus.Internal
val pythonSdkConfigurationMutex: ObservableMutex = ObservableMutex()
val Project.pythonSdkConfigurationMutex: ObservableMutex
@ApiStatus.Internal
get() = service<PythonSdkConfigurationMutexService>().mutex
@Service(Service.Level.PROJECT)
internal class PythonSdkConfigurationMutexService {
val mutex: ObservableMutex = ObservableMutex()
}
@@ -160,7 +160,7 @@ private class CacheEvictingFix(
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class InterpreterFixExecutor(private val project: Project, internal val scope: CoroutineScope) : BusyGuardExecutor {
override val isBusy: StateFlow<Boolean> = pythonSdkConfigurationMutex.isLocked
override val isBusy: StateFlow<Boolean> = project.pythonSdkConfigurationMutex.isLocked
init {
scope.launch {
@@ -170,7 +170,7 @@ class InterpreterFixExecutor(private val project: Project, internal val scope: C
override fun execute(action: suspend () -> Unit) {
scope.launch {
pythonSdkConfigurationMutex.tryWithLock { action() }.orLogException(LOG)
project.pythonSdkConfigurationMutex.tryWithLock { action() }.orLogException(LOG)
}
}
@@ -129,7 +129,7 @@ class NewPythonProjectStep(parent: NewProjectWizardStep, val createPythonModuleS
}
runWithModalProgressBlocking(project, PyBundle.message("python.sdk.creating.python.sdk")) {
pythonSdkConfigurationMutex.withLock {
project.pythonSdkConfigurationMutex.withLock {
val (sdk, _) = pySdkCreator.getSdk(moduleOrProject).getOr {
errorSink.emit(it.error, project)
return@withLock
@@ -47,7 +47,7 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
}.getOr { return@coroutineScope it }
}
val sdkResult = pythonSdkConfigurationMutex.withLock {
val sdkResult = module.project.pythonSdkConfigurationMutex.withLock {
val (sdk: Sdk, interpreterStatistics: InterpreterStatisticsInfo) = withBackgroundProgress(project, PyBundle.message("python.sdk.creating.python.sdk")) {
getSdkAndInterpreter(module)
}.getOr { return@withLock it }
@@ -44,7 +44,7 @@ class PythonAddLocalInterpreterPresenter(
private val _sdkShared = MutableSharedFlow<Sdk>(1)
val sdkCreatedFlow: Flow<Sdk> = _sdkShared.asSharedFlow()
suspend fun okClicked(addEnvironment: PythonAddEnvironment<PathHolder.Eel>): Unit = pythonSdkConfigurationMutex.withLock {
suspend fun okClicked(addEnvironment: PythonAddEnvironment<PathHolder.Eel>): Unit = moduleOrProject.project.pythonSdkConfigurationMutex.withLock {
when (val r = addEnvironment.getOrCreateSdkWithModal(moduleOrProject)) {
is Result.Failure -> {
errorSink.emit(r.error, moduleOrProject.project)
@@ -57,4 +57,4 @@ class PythonAddLocalInterpreterPresenter(
}
}
}
}
}
@@ -63,7 +63,6 @@ import com.jetbrains.python.sdk.installSdkIfNeeded
import com.jetbrains.python.sdk.moduleIfExists
import com.jetbrains.python.sdk.persist
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.sdk.suggestAssociatedSdkName
@@ -117,7 +117,7 @@ class PythonLanguageRuntimeUI(
val sdk = runWithModalProgressBlocking(project, message("python.sdk.progress.setting.up.environment")) {
withContext(TraceContext(message("trace.context.add.remote.python.sdk.dialog", targetSupplier.get().getTargetType().displayName))) {
pythonSdkConfigurationMutex.withLock {
project.pythonSdkConfigurationMutex.withLock {
sdkManager.getOrCreateSdkWithModal(ModuleOrProject.ModuleAndProject(module)).onFailure {
errorSink.emit(it)
}.successOrNull?.also {
@@ -134,4 +134,4 @@ class PythonLanguageRuntimeUI(
}
override fun validate(): Collection<ValidationInfo> = validationErrors
}
}