From e9f911fb1de60f4a2bc423f48a7ed50742979714 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Mon, 23 Mar 2026 16:24:41 +0100 Subject: [PATCH] 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 --- ...ySdkFromEnvironmentVariableConfigurator.kt | 2 +- .../ide/impl/miscProject/impl/lib.kt | 2 +- .../PyCharmWelcomeScreenProjectProvider.kt | 2 +- .../featuresTrainer/ift/PythonLangSupport.kt | 2 +- .../pyproject/model/api/sdkSuggestionTools.kt | 2 +- .../src/impl/ModulesSdkConfigurator.kt | 4 ++-- .../python/sdk/PythonSdkConfigurationMutex.kt | 23 +++++++++++++++---- .../PyAsyncFileInspectionRunner.kt | 4 ++-- .../newProject/NewProjectWizardPythonData.kt | 2 +- .../PyV3BaseProjectSettings.kt | 2 +- .../v2/PythonAddLocalInterpreterPresenter.kt | 4 ++-- .../com/jetbrains/python/sdk/add/v2/common.kt | 1 - .../python/target/PythonLanguageRuntimeUI.kt | 4 ++-- 13 files changed, 33 insertions(+), 21 deletions(-) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PySdkFromEnvironmentVariableConfigurator.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PySdkFromEnvironmentVariableConfigurator.kt index 093cea0342ff..30a93e6d1c69 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PySdkFromEnvironmentVariableConfigurator.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PySdkFromEnvironmentVariableConfigurator.kt @@ -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 diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt index f54792359090..03f22cc0c4fe 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/miscProject/impl/lib.kt @@ -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) } } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/welcomeScreen/PyCharmWelcomeScreenProjectProvider.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/welcomeScreen/PyCharmWelcomeScreenProjectProvider.kt index 8aea1ba73586..00906afbc835 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/welcomeScreen/PyCharmWelcomeScreenProjectProvider.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/welcomeScreen/PyCharmWelcomeScreenProjectProvider.kt @@ -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()) } } diff --git a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt index d0b75efd429f..dc138b6e07c8 100644 --- a/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt +++ b/python/python-features-trainer/src/com/intellij/python/featuresTrainer/ift/PythonLangSupport.kt @@ -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) diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt index dcba578af4c4..0b632e1f4127 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/api/sdkSuggestionTools.kt @@ -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? = pythonSdkConfigurationMutex.withLock { +suspend fun Module.autoConfigureSdkIfNeeded(): PyResult? = project.pythonSdkConfigurationMutex.withLock { val moduleInfo = getModuleInfo() ?: return@withLock null when (moduleInfo) { diff --git a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt index 2aea5666ea4e..440fd427b536 100644 --- a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt +++ b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt @@ -138,7 +138,7 @@ internal class ModulesSdkConfigurator private constructor( * Errors are logged. * */ - suspend fun configureSdks(modulesOnly: Set) = pythonSdkConfigurationMutex.withLock { + suspend fun configureSdks(modulesOnly: Set) = project.pythonSdkConfigurationMutex.withLock { withContext(Dispatchers.Default) { val modulesMap = project.modules.associateBy { it.name } val modulesWithSameSdk = mutableMapOf() @@ -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() diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkConfigurationMutex.kt b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkConfigurationMutex.kt index 36b4c517d72d..c2d5a5567869 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkConfigurationMutex.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkConfigurationMutex.kt @@ -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().mutex + + +@Service(Service.Level.PROJECT) +internal class PythonSdkConfigurationMutexService { + val mutex: ObservableMutex = ObservableMutex() +} diff --git a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt index e0b71269253a..5666eb39a682 100644 --- a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt +++ b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt @@ -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 = pythonSdkConfigurationMutex.isLocked + override val isBusy: StateFlow = 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) } } diff --git a/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt b/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt index 7e15c26d2bc4..3c5c5c7b749b 100644 --- a/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt +++ b/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt @@ -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 diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3BaseProjectSettings.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3BaseProjectSettings.kt index ce599d9923fe..510db57f5cce 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/PyV3BaseProjectSettings.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3BaseProjectSettings.kt @@ -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 } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterPresenter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterPresenter.kt index f1554264ab23..a769b400b341 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterPresenter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterPresenter.kt @@ -44,7 +44,7 @@ class PythonAddLocalInterpreterPresenter( private val _sdkShared = MutableSharedFlow(1) val sdkCreatedFlow: Flow = _sdkShared.asSharedFlow() - suspend fun okClicked(addEnvironment: PythonAddEnvironment): Unit = pythonSdkConfigurationMutex.withLock { + suspend fun okClicked(addEnvironment: PythonAddEnvironment): 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( } } } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index eb7778291717..fb05269d1b1d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -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 diff --git a/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt b/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt index cdf90183f948..d271edeaaedd 100644 --- a/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt +++ b/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt @@ -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 = validationErrors -} \ No newline at end of file +}