Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export enum EventNames {
* - errorType: string (error class name, on failure only)
*/
ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY',
MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT',
}

// Map all events to their properties
Expand Down Expand Up @@ -226,4 +227,15 @@ export interface IEventNamePropertyMapping {
envCount?: number;
errorType?: string;
};

/* __GDPR__
"manager_ready.timeout": {
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"managerKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/
[EventNames.MANAGER_READY_TIMEOUT]: {
managerId: string;
managerKind: 'environment' | 'package';
};
}
61 changes: 53 additions & 8 deletions src/features/common/managerReady.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Disposable, l10n, Uri } from 'vscode';
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
import { createDeferred, Deferred } from '../../common/utils/deferred';
import { allExtensions, getExtension } from '../../common/extension.apis';
import { traceError, traceInfo } from '../../common/logging';
import { showErrorMessage } from '../../common/window.apis';
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers';
import { WorkbenchStrings } from '../../common/localize';
import { traceError, traceInfo, traceWarn } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { createDeferred, Deferred } from '../../common/utils/deferred';
import { showErrorMessage } from '../../common/window.apis';
import { installExtension } from '../../common/workbenchCommands';
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers';

const MANAGER_READY_TIMEOUT_MS = 30_000;

interface ManagerReady extends Disposable {
waitForEnvManager(uris?: Uri[]): Promise<void>;
Expand All @@ -29,7 +33,10 @@ class ManagerReadyImpl implements ManagerReady {
private readonly checked: Set<string> = new Set();
private readonly disposables: Disposable[] = [];

constructor(em: EnvironmentManagers, private readonly pm: PythonProjectManager) {
constructor(
em: EnvironmentManagers,
private readonly pm: PythonProjectManager,
) {
this.disposables.push(
em.onDidChangeEnvironmentManager((e) => {
if (this.envManagers.has(e.manager.id)) {
Expand Down Expand Up @@ -104,6 +111,44 @@ class ManagerReadyImpl implements ManagerReady {
}
}

/**
* Wraps a deferred with a timeout so a missing/dead manager cannot block the API forever.
* On timeout the deferred is resolved (not rejected) so callers proceed with degraded results
* instead of hanging.
*/
private _withTimeout(deferred: Deferred<void>, managerId: string, kind: string): Promise<void> {
if (deferred.completed) {
return deferred.promise;
}
return new Promise<void>((resolve) => {
const timer = setTimeout(() => {
if (!deferred.completed) {
traceWarn(
`Timed out after ${MANAGER_READY_TIMEOUT_MS / 1000}s waiting for ${kind} manager "${managerId}" to register. ` +
`The manager may not be installed or its extension failed to activate. Proceeding without it. ` +
`To prevent this, check your "python-envs.defaultEnvManager" and "python-envs.pythonProjects" settings.`,
);
sendTelemetryEvent(EventNames.MANAGER_READY_TIMEOUT, undefined, {
managerId,
managerKind: kind as 'environment' | 'package',
});
deferred.resolve();
}
}, MANAGER_READY_TIMEOUT_MS);

deferred.promise.then(
() => {
clearTimeout(timer);
resolve();
},
() => {
clearTimeout(timer);
resolve();
},
);
});
}

public dispose(): void {
this.disposables.forEach((d) => d.dispose());
this.envManagers.clear();
Expand All @@ -116,7 +161,7 @@ class ManagerReadyImpl implements ManagerReady {
}
const deferred = createDeferred<void>();
this.envManagers.set(managerId, deferred);
return deferred.promise;
return this._withTimeout(deferred, managerId, 'environment');
}

public async waitForEnvManager(uris?: Uri[]): Promise<void> {
Expand Down Expand Up @@ -165,7 +210,7 @@ class ManagerReadyImpl implements ManagerReady {
}
const deferred = createDeferred<void>();
this.pkgManagers.set(managerId, deferred);
return deferred.promise;
return this._withTimeout(deferred, managerId, 'package');
}

public async waitForPkgManager(uris?: Uri[]): Promise<void> {
Expand Down