fix: serialize concurrent marketplace repo clones via SequencerByKey (#301289)

* Initial plan

* fix: use SequencerByKey to prevent parallel cloning of the same marketplace repo

Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com>
Co-authored-by: Connor Peet <copeet@microsoft.com>
This commit is contained in:
Copilot
2026-03-13 01:09:31 +00:00
committed by GitHub
parent f78ba93e2c
commit e7e6cbe3a9
2 changed files with 64 additions and 13 deletions

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Action } from '../../../../base/common/actions.js';
import { SequencerByKey } from '../../../../base/common/async.js';
import { Lazy } from '../../../../base/common/lazy.js';
import { revive } from '../../../../base/common/marshalling.js';
import { dirname, isEqual, isEqualOrParent, joinPath } from '../../../../base/common/resources.js';
@@ -38,6 +39,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
private readonly _cacheRoot: URI;
private readonly _marketplaceIndex = new Lazy<Map<string, IMarketplaceIndexEntry>>(() => this._loadMarketplaceIndex());
private readonly _pluginSources: ReadonlyMap<PluginSourceKind, IPluginSource>;
private readonly _cloneSequencer = new SequencerByKey<string>();
constructor(
@ICommandService private readonly _commandService: ICommandService,
@@ -90,21 +92,23 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
async ensureRepository(marketplace: IMarketplaceReference, options?: IEnsureRepositoryOptions): Promise<URI> {
const repoDir = this.getRepositoryUri(marketplace, options?.marketplaceType);
const repoExists = await this._fileService.exists(repoDir);
if (repoExists) {
return this._cloneSequencer.queue(repoDir.fsPath, async () => {
const repoExists = await this._fileService.exists(repoDir);
if (repoExists) {
this._updateMarketplaceIndex(marketplace, repoDir, options?.marketplaceType);
return repoDir;
}
if (marketplace.kind === MarketplaceReferenceKind.LocalFileUri) {
throw new Error(`Local marketplace repository does not exist: ${repoDir.fsPath}`);
}
const progressTitle = options?.progressTitle ?? localize('preparingMarketplace', "Preparing plugin marketplace '{0}'...", marketplace.displayLabel);
const failureLabel = options?.failureLabel ?? marketplace.displayLabel;
await this._cloneRepository(repoDir, marketplace.cloneUrl, progressTitle, failureLabel);
this._updateMarketplaceIndex(marketplace, repoDir, options?.marketplaceType);
return repoDir;
}
if (marketplace.kind === MarketplaceReferenceKind.LocalFileUri) {
throw new Error(`Local marketplace repository does not exist: ${repoDir.fsPath}`);
}
const progressTitle = options?.progressTitle ?? localize('preparingMarketplace', "Preparing plugin marketplace '{0}'...", marketplace.displayLabel);
const failureLabel = options?.failureLabel ?? marketplace.displayLabel;
await this._cloneRepository(repoDir, marketplace.cloneUrl, progressTitle, failureLabel);
this._updateMarketplaceIndex(marketplace, repoDir, options?.marketplaceType);
return repoDir;
});
}
async pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise<boolean> {

View File

@@ -110,6 +110,53 @@ suite('AgentPluginRepositoryService', () => {
assert.strictEqual(uri.path, '/cache/agentPlugins/github.com/microsoft/vscode');
});
test('concurrent ensureRepository calls for the same marketplace clone only once', async () => {
let cloneCount = 0;
const instantiationService = store.add(new TestInstantiationService());
// Track whether the repo exists (set to true after the first clone completes)
let repoExists = false;
const fileService = {
exists: async (_resource: URI) => repoExists,
createFolder: async () => undefined,
} as unknown as IFileService;
const progressService = {
withProgress: async (_options: unknown, callback: (...args: unknown[]) => Promise<unknown>) => callback(),
} as unknown as IProgressService;
instantiationService.stub(ICommandService, {
executeCommand: async (id: string) => {
if (id === '_git.cloneRepository') {
cloneCount++;
// Simulate async clone by yielding, then mark repo as existing
await new Promise<void>(r => setTimeout(r, 0));
repoExists = true;
}
return undefined;
},
} as unknown as ICommandService);
instantiationService.stub(IEnvironmentService, { cacheHome: URI.file('/cache') } as unknown as IEnvironmentService);
instantiationService.stub(IFileService, fileService);
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(INotificationService, { notify: () => undefined } as unknown as INotificationService);
instantiationService.stub(IProgressService, progressService);
instantiationService.stub(IStorageService, store.add(new InMemoryStorageService()));
const service = instantiationService.createInstance(AgentPluginRepositoryService);
const plugin = createPlugin('microsoft/vscode', 'plugins/myPlugin');
// Fire two concurrent ensureRepository calls for the same marketplace
const [uri1, uri2] = await Promise.all([
service.ensureRepository(plugin.marketplaceReference, { marketplaceType: plugin.marketplaceType }),
service.ensureRepository(plugin.marketplaceReference, { marketplaceType: plugin.marketplaceType }),
]);
assert.strictEqual(cloneCount, 1);
assert.strictEqual(uri1.path, '/cache/agentPlugins/github.com/microsoft/vscode');
assert.strictEqual(uri2.path, '/cache/agentPlugins/github.com/microsoft/vscode');
});
test('builds install URI from source inside repository root', () => {
const service = createService();
const plugin = createPlugin('microsoft/vscode', 'plugins/myPlugin');