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
This commit is contained in:
Alexey Katsman
2026-09-01 19:26:16 +00:00
committed by intellij-monorepo-bot
parent 67c65997d7
commit 34b07455ae
@@ -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));
}
}