Skip to content

fix(extension-mgmt): record concurrent install result so callers don't see "Unknown error"#314868

Merged
bryanchen-d merged 2 commits into
mainfrom
bryanchen-d/fix-extension-install-unknown-error-race
May 7, 2026
Merged

fix(extension-mgmt): record concurrent install result so callers don't see "Unknown error"#314868
bryanchen-d merged 2 commits into
mainfrom
bryanchen-d/fix-extension-install-unknown-error-race

Conversation

@bryanchen-d
Copy link
Copy Markdown
Contributor

Summary

Fixes a race in installFromGallery that surfaces in telemetry as Unknown error while installing extension <id> for ~1,484 unique users in the last 14 days (bucket 810ac6ce…, github.copilot-chat), plus three other extension-specific buckets.

Linked issues: #310858 (open, currently auto-triaged with the same root-cause analysis), #293368 (closed for "not enough information" — this PR explains the missing piece).

Root cause

AbstractExtensionManagementService.installExtensions(...) keeps a class-level map installingExtensions of in-flight installs keyed by (extensionId, profileLocation). When a second installFromGallery(extA) arrives while a first is still running, the top-level loop takes the "wait for existing task" branch:

const existingInstallExtensionTask = ...this.installingExtensions.get(...);
if (existingInstallExtensionTask) {
    alreadyRequestedInstallations.push(existingInstallExtensionTask.task.waitUntilTaskIsFinished());
} else {
    createInstallExtensionTask(...);
}

Only createInstallExtensionTask populates the local installingExtensionsMap, so the parallel-install loop and the deps loop both iterate over an empty map and nothing writes into installExtensionResultsMap.

After the awaited tasks complete, the function does:

if (alreadyRequestedInstallations.length) {
    await this.joinAllSettled(alreadyRequestedInstallations);   // result discarded
}
...
return [...installExtensionResultsMap.values()];                // → []

Back in installFromGallery:

const results = await this.installGalleryExtensions([{ extension, options }]);
const result = results.find(...);                       // undefined
if (result?.local)  return result.local;                // miss
if (result?.error)  throw result.error;                 // miss
const redirectedResult = results[0];                    // undefined
if (redirectedResult?.local) return redirectedResult.local;     // miss
if (redirectedResult?.error) throw redirectedResult.error;      // miss
throw new ExtensionManagementError(`Unknown error while installing extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Unknown);

Two failure modes, one symptom:

  • In-flight install succeeded → caller throws "Unknown error" even though the extension is installed.
  • In-flight install failedjoinAllSettled rethrows; the outer catch only iterates the (empty) installingExtensionsMap so no real error is recorded; control falls through and return [] → caller throws the generic message instead of the underlying cause. (This is why [Unhandled Error] Unknown error while installing extension github.copilot-chat #293368 had no actionable info.)

Fix

When the existing-task branch is taken, attach .then(...) / .catch(...) to waitUntilTaskIsFinished() and write the resolved ILocalExtension (or rejection reason, normalized via toExtensionManagementError) into installExtensionResultsMap keyed identically to primary tasks (${identifier.id.toLowerCase()}-${profileLocation.toString()}). On the failure path we still re-throw so the existing joinAllSettled rejection / outer catch semantics are preserved.

After the fix:

  • Concurrent install succeeded → installFromGallery's results.find(...) returns the real ILocalExtension. ✅
  • Concurrent install failed → installFromGallery throws the real error (e.g. network / signature / disk-full), which is what sandy081 needs to investigate. ✅

No public API changes. ~28 lines net.

Telemetry context

Bucket Extension Hits / 14d Users / 14d Versions
810ac6ce… github.copilot-chat 3,525 1,484 1.116, 1.117
33a063ed… anthropic.claude-code 432 144 1.116–1.118.1
fad68ea2… ms-ceintl.vscode-language-pack-zh-hans 240 144 1.116–1.118.1
09e28363… ms-python.python 385 121 1.116–1.118.1

All four buckets share the exact stack frame at abstractExtensionManagementService.ts:184 (the throw new ExtensionManagementError("Unknown error...") line).

…t see "Unknown error"

When a concurrent installFromGallery for the same (extensionId, profileLocation) pair is already in flight, installExtensions takes the "wait for existing task" branch but never writes the wait result into installExtensionResultsMap. As a result installFromGallery sees an empty results array and throws the generic "Unknown error while installing extension X" - even when the in-flight install actually succeeded, and hiding the real failure cause when it didn't.

Plumb the resolved ILocalExtension (or rejection reason) into installExtensionResultsMap using the same key shape as primary tasks so installFromGallery's lookup finds the real local / error.

Fixes the symptom tracked in #310858 and #293368.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a race in AbstractExtensionManagementService.installExtensions(...) where concurrent installFromGallery calls could return an empty results array, causing callers to throw a generic Unknown error while installing extension <id> even when the underlying in-flight install actually succeeded or failed with a real error. The change ensures that when an install is already in progress, the waiting caller still records the resolved ILocalExtension or the normalized error into installExtensionResultsMap, allowing installFromGallery to surface the correct outcome.

Changes:

  • When this.installingExtensions already contains an in-flight task, attach handlers to waitUntilTaskIsFinished() and populate installExtensionResultsMap for that caller.
  • Preserve existing rejection behavior by rethrowing after recording the error, so joinAllSettled semantics remain unchanged.
Show a summary per file
File Description
src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts Records results for “already in-flight” installs so concurrent callers don’t fall back to the generic “Unknown error” path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

Comment thread src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts Outdated
Mirror the existing pattern used for the inner waitForInstallation: assign the chained promise to a variable and attach a no-op .catch so the rejection is always observed even when the outer try throws before joinAllSettled(alreadyRequestedInstallations) is reached.

Addresses Copilot review feedback on PR #314868.
@bryanchen-d bryanchen-d marked this pull request as ready for review May 6, 2026 23:05
@bryanchen-d bryanchen-d merged commit f5e79f4 into main May 7, 2026
26 checks passed
@bryanchen-d bryanchen-d deleted the bryanchen-d/fix-extension-install-unknown-error-race branch May 7, 2026 19:56
@vs-code-engineering vs-code-engineering Bot added this to the 1.120.0 milestone May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants