mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
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:
@@ -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> {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user