From 34b07455ae0bd7939b25d2b20ff3065d3f57bbc9 Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Tue, 1 Sep 2026 11:45:59 +0300 Subject: [PATCH] PY-91167 stop a background SDK update that outlives its project or SDK An update starts while a project is open and can still be running when the project closes or the SDK is removed. `PyUpdateSdkTask.run` then works with a disposed SDK. Registering the indicator disposable under it throws IncorrectOperationException, and `PythonPackageManager.forSdk` can throw AlreadyDisposedException. In tests both surface as uncaught exceptions during fixture teardown. `scheduleUpdate` and `run` now bail out when the project or the SDK is gone, and the indicator disposable uses `tryRegister`, which reports a disposed parent instead of throwing. `onFinished` decided the same thing too late. It removed the SDK from `ourUnderRefresh`, added it back for the queued update, and only then checked disposal, so the entry stayed in a static set forever. That retained a disposed SDK and made every later `scheduleUpdate` believe a refresh was in flight, which blocked all further updates for that SDK. The decision now happens once, under `ourLock`. (cherry picked from commit 37010251b6d5b9798d29a03488984047a7272328) GitOrigin-RevId: ca0ac05248574297525455bab8144e8bbfe6626d --- .../python/sdk/PythonSdkUpdater.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java index b3c9a25f7622..41645c63c1bb 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java @@ -156,7 +156,7 @@ public final class PythonSdkUpdater { } private static void scheduleUpdate(@NotNull Sdk sdk, @NotNull Project project, @NotNull PyUpdateSdkRequestData requestData) { - if (project.isDisposed()) { + if (project.isDisposed() || (sdk instanceof Disposable sdkDisposable && Disposer.isDisposed(sdkDisposable))) { return; } @@ -261,12 +261,17 @@ public final class PythonSdkUpdater { @Override public void run(@NotNull ProgressIndicator indicator) { + if (myProject.isDisposed() || isSdkDisposed()) { + return; + } PythonPackageManager manager = PythonPackageManager.Companion.forSdk(myProject, mySdk); // Cancel the indicator when the SDK is disposed to terminate any running processes (e.g., skeleton generation). // This explicit cancellation should become unnecessary on migrating PythonSdkUpdater to coroutines and withBackgroundProgress. Disposable indicatorDisposable = getIndicatorDisposable(indicator); if (mySdk instanceof Disposable sdkDisposable) { - Disposer.register(sdkDisposable, indicatorDisposable); + if (!Disposer.tryRegister(sdkDisposable, indicatorDisposable)) { + return; + } } else { Disposer.register(PythonPluginDisposable.getInstance(myProject), indicatorDisposable); @@ -420,15 +425,18 @@ public final class PythonSdkUpdater { boolean existed = ourUnderRefresh.remove(mySdk); LOG.assertTrue(existed, "Error in SDK refresh scheduling: refreshed SDK is not in the set."); requestData = ourToBeRefreshed.remove(mySdk); - if (requestData != null) { + // Decide under the lock whether the queued update really starts: re-adding the SDK and only then + // discovering that the project or the SDK is gone would leave it in ourUnderRefresh forever, which + // both retains a disposed SDK and makes every later scheduleUpdate believe a refresh is in flight. + if (requestData != null && !myProject.isDisposed() && !isSdkDisposed()) { ourUnderRefresh.add(mySdk); } + else { + requestData = null; + } } if (requestData != null) { - if (Disposer.isDisposed(myProject)) { - return; - } ProgressManager.getInstance().run(new PyUpdateSdkTask(myProject, mySdk, requestData)); } }