-
Notifications
You must be signed in to change notification settings - Fork 0
fix: retry failed manager initialization #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,7 +95,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { | |
| return this._initialized.promise; | ||
| } | ||
|
|
||
| this._initialized = createDeferred(); | ||
| const initialized = createDeferred<void>(); | ||
| this._initialized = initialized; | ||
| const stopWatch = new StopWatch(); | ||
| let result: 'success' | 'tool_not_found' | 'error' = 'success'; | ||
| let envCount = 0; | ||
|
|
@@ -165,6 +166,9 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { | |
| result = 'error'; | ||
| errorType = classifyError(ex); | ||
| traceError('Conda lazy initialization failed', ex); | ||
| if (this._initialized === initialized) { | ||
| this._initialized = undefined; | ||
| } | ||
| } finally { | ||
| sendTelemetryEvent(EventNames.MANAGER_LAZY_INIT, stopWatch.elapsedTime, { | ||
| managerName: 'conda', | ||
|
|
@@ -173,7 +177,7 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { | |
| toolSource, | ||
| errorType, | ||
| }); | ||
| this._initialized.resolve(); | ||
| initialized.resolve(); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Telemetry still runs before |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,7 +81,8 @@ export class PipenvManager implements EnvironmentManager, Disposable { | |
| if (this._initialized) { | ||
| return this._initialized.promise; | ||
| } | ||
| this._initialized = createDeferred(); | ||
| const initialized = createDeferred<void>(); | ||
| this._initialized = initialized; | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
📍 src/managers/pipenv/pipenvManager.ts:84 [verified]
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed as an intentional follow-up. This correctness-focused change deliberately keeps the reset/ownership protocol inline (see the PR's "no shared helper extraction" note and the constraint to avoid introducing a new abstraction in this fix). Notably, this round extends the same ownership pattern into |
||
| const stopWatch = new StopWatch(); | ||
| let result: 'success' | 'tool_not_found' | 'error' = 'success'; | ||
| let envCount = 0; | ||
|
|
@@ -129,6 +130,9 @@ export class PipenvManager implements EnvironmentManager, Disposable { | |
| result = 'error'; | ||
| errorType = classifyError(ex); | ||
| traceError('Pipenv lazy initialization failed', ex); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This ownership check protects only the failure path. If |
||
| if (this._initialized === initialized) { | ||
| this._initialized = undefined; | ||
| } | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Clearing [verified] |
||
| } finally { | ||
| sendTelemetryEvent(EventNames.MANAGER_LAZY_INIT, stopWatch.elapsedTime, { | ||
| managerName: 'pipenv', | ||
|
|
@@ -137,7 +141,7 @@ export class PipenvManager implements EnvironmentManager, Disposable { | |
| toolSource, | ||
| errorType, | ||
| }); | ||
| this._initialized.resolve(); | ||
| initialized.resolve(); | ||
|
StellaHuang95 marked this conversation as resolved.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Telemetry still runs before |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,7 +79,8 @@ export class PoetryManager implements EnvironmentManager, Disposable { | |
| if (this._initialized) { | ||
| return this._initialized.promise; | ||
| } | ||
| this._initialized = createDeferred(); | ||
| const initialized = createDeferred<void>(); | ||
| this._initialized = initialized; | ||
| const stopWatch = new StopWatch(); | ||
| let result: 'success' | 'tool_not_found' | 'error' = 'success'; | ||
| let envCount = 0; | ||
|
|
@@ -127,6 +128,9 @@ export class PoetryManager implements EnvironmentManager, Disposable { | |
| result = 'error'; | ||
| errorType = classifyError(ex); | ||
| traceError('Poetry lazy initialization failed', ex); | ||
| if (this._initialized === initialized) { | ||
| this._initialized = undefined; | ||
| } | ||
| } finally { | ||
| sendTelemetryEvent(EventNames.MANAGER_LAZY_INIT, stopWatch.elapsedTime, { | ||
| managerName: 'poetry', | ||
|
|
@@ -135,7 +139,7 @@ export class PoetryManager implements EnvironmentManager, Disposable { | |
| toolSource, | ||
| errorType, | ||
| }); | ||
| this._initialized.resolve(); | ||
| initialized.resolve(); | ||
|
StellaHuang95 marked this conversation as resolved.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Telemetry still precedes deferred settlement. Resolve the captured deferred first and catch/log telemetry failures so this manager preserves its never-throw behavior and concurrent waiters cannot hang. |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,7 +81,8 @@ export class PyEnvManager implements EnvironmentManager, Disposable { | |
| if (this._initialized) { | ||
| return this._initialized.promise; | ||
| } | ||
| this._initialized = createDeferred(); | ||
| const initialized = createDeferred<void>(); | ||
| this._initialized = initialized; | ||
| const stopWatch = new StopWatch(); | ||
| let result: 'success' | 'tool_not_found' | 'error' = 'success'; | ||
| let envCount = 0; | ||
|
|
@@ -128,6 +129,9 @@ export class PyEnvManager implements EnvironmentManager, Disposable { | |
| result = 'error'; | ||
| errorType = classifyError(ex); | ||
| traceError('Pyenv lazy initialization failed', ex); | ||
| if (this._initialized === initialized) { | ||
| this._initialized = undefined; | ||
| } | ||
| } finally { | ||
| sendTelemetryEvent(EventNames.MANAGER_LAZY_INIT, stopWatch.elapsedTime, { | ||
| managerName: 'pyenv', | ||
|
|
@@ -136,7 +140,7 @@ export class PyEnvManager implements EnvironmentManager, Disposable { | |
| toolSource, | ||
| errorType, | ||
| }); | ||
| this._initialized.resolve(); | ||
| initialized.resolve(); | ||
|
StellaHuang95 marked this conversation as resolved.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Telemetry can still throw before the captured deferred is resolved, leaving concurrent waiters pending. Settle first and make telemetry best-effort. |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import assert from 'assert'; | ||
| import * as sinon from 'sinon'; | ||
| import { anything, reset, when } from 'ts-mockito'; | ||
| import { PythonEnvironmentApi } from '../../../api'; | ||
| import * as logging from '../../../common/logging'; | ||
| import * as cache from '../../../managers/builtin/cache'; | ||
| import { SysPythonManager } from '../../../managers/builtin/sysPythonManager'; | ||
| import * as utils from '../../../managers/builtin/utils'; | ||
| import * as uvInstaller from '../../../managers/builtin/uvPythonInstaller'; | ||
| import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; | ||
| import { mockedVSCodeNamespaces } from '../../unittests'; | ||
|
|
||
| suite('SysPythonManager.initialize - retry after failure (throw style)', () => { | ||
| let refreshPythonsStub: sinon.SinonStub; | ||
|
|
||
| setup(() => { | ||
| when(mockedVSCodeNamespaces.window!.withProgress(anything(), anything())).thenCall( | ||
| (_options: any, task: any) => task({ report: sinon.stub() }, { isCancellationRequested: false }), | ||
| ); | ||
| refreshPythonsStub = sinon.stub(utils, 'refreshPythons'); | ||
| sinon.stub(uvInstaller, 'promptInstallPythonViaUv').resolves(undefined); | ||
| sinon.stub(cache, 'getSystemEnvForGlobal').resolves(undefined); | ||
| sinon.stub(logging, 'traceError'); | ||
| sinon.stub(logging, 'traceWarn'); | ||
| }); | ||
|
|
||
| teardown(() => { | ||
| sinon.restore(); | ||
| reset(mockedVSCodeNamespaces.window!); | ||
| }); | ||
|
|
||
| function createManager(): SysPythonManager { | ||
| const api = { | ||
| getPythonProjects: sinon.stub().returns([]), | ||
| getPythonProject: sinon.stub().returns(undefined), | ||
| } as any as PythonEnvironmentApi; | ||
| return new SysPythonManager({} as NativePythonFinder, api, { | ||
| info: sinon.stub(), | ||
| error: sinon.stub(), | ||
| warn: sinon.stub(), | ||
| } as any); | ||
| } | ||
|
|
||
| test('rethrows on failure but clears state so a later call retries', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These new manager test suites define [verified] |
||
| refreshPythonsStub.onFirstCall().rejects(new Error('discovery boom')); | ||
| refreshPythonsStub.onSecondCall().resolves([]); | ||
|
|
||
| const mgr = createManager(); | ||
|
|
||
| await assert.rejects(mgr.initialize(), /discovery boom/); | ||
| assert.strictEqual(refreshPythonsStub.callCount, 1); | ||
|
|
||
| await assert.doesNotReject(mgr.initialize()); | ||
| assert.strictEqual(refreshPythonsStub.callCount, 2, 'a later call must retry after a failure'); | ||
|
|
||
| await mgr.initialize(); | ||
| assert.strictEqual(refreshPythonsStub.callCount, 2, 'no re-discovery after a successful init'); | ||
| }); | ||
|
|
||
| test('settles concurrent waiters during a failing run (leader rejects, waiter resolves)', async () => { | ||
| refreshPythonsStub.rejects(new Error('discovery boom')); | ||
|
|
||
| const mgr = createManager(); | ||
|
|
||
| const leader = mgr.initialize(); | ||
| const waiter = mgr.initialize(); | ||
|
|
||
| await assert.rejects(leader, /discovery boom/); | ||
| await assert.doesNotReject(waiter); | ||
| assert.strictEqual(refreshPythonsStub.callCount, 1, 'concurrent callers share one discovery run'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import assert from 'assert'; | ||
| import * as sinon from 'sinon'; | ||
| import { EnvironmentManager, PythonEnvironmentApi } from '../../../api'; | ||
| import * as logging from '../../../common/logging'; | ||
| import * as windowApis from '../../../common/window.apis'; | ||
| import { VenvManager } from '../../../managers/builtin/venvManager'; | ||
| import * as venvUtils from '../../../managers/builtin/venvUtils'; | ||
| import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; | ||
|
|
||
| suite('VenvManager.initialize - retry after failure (throw style)', () => { | ||
| let findVirtualEnvironmentsStub: sinon.SinonStub; | ||
|
|
||
| setup(() => { | ||
| findVirtualEnvironmentsStub = sinon.stub(venvUtils, 'findVirtualEnvironments'); | ||
| sinon.stub(venvUtils, 'getVenvForGlobal').resolves(undefined); | ||
| sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => { | ||
| return await (task as any)({ report: sinon.stub() }, { isCancellationRequested: false } as any); | ||
| }); | ||
| sinon.stub(logging, 'traceError'); | ||
| sinon.stub(logging, 'traceWarn'); | ||
| }); | ||
|
|
||
| teardown(() => { | ||
| sinon.restore(); | ||
| }); | ||
|
|
||
| function createManager(): VenvManager { | ||
| const api = { | ||
| getEnvironments: sinon.stub().resolves([]), | ||
| getPythonProject: sinon.stub().returns(undefined), | ||
| getPythonProjects: sinon.stub().returns([]), | ||
| refreshEnvironments: sinon.stub().resolves(undefined), | ||
| } as any as PythonEnvironmentApi; | ||
| const baseManager = { | ||
| getEnvironments: sinon.stub().resolves([]), | ||
| } as any as EnvironmentManager; | ||
| return new VenvManager({} as NativePythonFinder, api, baseManager, { | ||
| info: sinon.stub(), | ||
| error: sinon.stub(), | ||
| warn: sinon.stub(), | ||
| } as any); | ||
| } | ||
|
|
||
| test('rethrows on failure but clears state so a later call retries and succeeds', async () => { | ||
| findVirtualEnvironmentsStub.onFirstCall().rejects(new Error('discovery boom')); | ||
| findVirtualEnvironmentsStub.onSecondCall().resolves([]); | ||
|
|
||
| const mgr = createManager(); | ||
|
|
||
| await assert.rejects(mgr.initialize(), /discovery boom/); | ||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 1); | ||
|
|
||
| await assert.doesNotReject(mgr.initialize()); | ||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 2, 'a later call must retry after a failure'); | ||
|
|
||
| await mgr.initialize(); | ||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 2, 'no re-discovery after a successful init'); | ||
| }); | ||
|
|
||
| test('settles concurrent waiters during a failing run (leader rejects, waiter resolves)', async () => { | ||
| findVirtualEnvironmentsStub.rejects(new Error('discovery boom')); | ||
|
|
||
| const mgr = createManager(); | ||
|
|
||
| const leader = mgr.initialize(); | ||
| const waiter = mgr.initialize(); | ||
|
|
||
| await assert.rejects(leader, /discovery boom/); | ||
| await assert.doesNotReject(waiter); | ||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 1, 'concurrent callers share one discovery run'); | ||
|
|
||
| findVirtualEnvironmentsStub.resetBehavior(); | ||
| findVirtualEnvironmentsStub.resolves([]); | ||
| await assert.doesNotReject(mgr.initialize()); | ||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 2, 'a fresh call retries after failure'); | ||
| }); | ||
|
|
||
| test('does not re-run discovery after a successful initialize()', async () => { | ||
| findVirtualEnvironmentsStub.resolves([]); | ||
| const mgr = createManager(); | ||
|
|
||
| await mgr.initialize(); | ||
| await mgr.initialize(); | ||
|
|
||
| assert.strictEqual(findVirtualEnvironmentsStub.callCount, 1); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.