From 55b63eb78d0c33cdb92f1a6001948d627c8e480f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Apr 2026 18:40:27 +0000 Subject: [PATCH] feat: parallelize container monitor dependency requests Co-authored-by: bgardiner --- src/lib/ecosystems/monitor.ts | 119 ++++++++++++------ .../unit/ecosystems-monitor-docker.spec.ts | 85 +++++++++++++ 2 files changed, 167 insertions(+), 37 deletions(-) diff --git a/src/lib/ecosystems/monitor.ts b/src/lib/ecosystems/monitor.ts index 04e95fde30..7456db1792 100644 --- a/src/lib/ecosystems/monitor.ts +++ b/src/lib/ecosystems/monitor.ts @@ -144,45 +144,36 @@ async function monitorDependencies( const errors: EcosystemMonitorError[] = []; for (const [path, scanResults] of Object.entries(scans)) { await spinner(`Monitoring dependencies in ${path}`); - for (const scanResult of scanResults) { - const monitorDependenciesRequest = - await generateMonitorDependenciesRequest(scanResult, options); + const [firstScanResult, ...remainingScanResults] = scanResults; + if (!firstScanResult) { + spinner.clearAll(); + continue; + } + + const firstResult = await monitorDependenciesForScanResult( + firstScanResult, + options, + path, + ); + if (firstResult.monitorResult) { + results.push(firstResult.monitorResult); + } + if (firstResult.monitorError) { + errors.push(firstResult.monitorError); + } - const configOrg = config.org ? decodeURIComponent(config.org) : undefined; + const remainingResults = await Promise.all( + remainingScanResults.map((scanResult) => + monitorDependenciesForScanResult(scanResult, options, path), + ), + ); - const payload = { - method: 'PUT', - url: `${config.API}/monitor-dependencies`, - json: true, - headers: { - 'x-is-ci': isCI(), - authorization: getAuthHeader(), - }, - body: monitorDependenciesRequest, - qs: { - org: options.org || configOrg, - }, - }; - try { - const response = - await makeRequest(payload); - results.push({ - ...response, - path, - scanResult, - }); - } catch (error) { - if (error.code === 401) { - throw AuthFailedError(); - } - if (error.code >= 400 && error.code < 500) { - throw new MonitorError(error.code, error.message); - } - errors.push({ - error: 'Could not monitor dependencies in ' + path, - path, - scanResult, - }); + for (const remainingResult of remainingResults) { + if (remainingResult.monitorResult) { + results.push(remainingResult.monitorResult); + } + if (remainingResult.monitorError) { + errors.push(remainingResult.monitorError); } } spinner.clearAll(); @@ -190,6 +181,60 @@ async function monitorDependencies( return [results, errors]; } +async function monitorDependenciesForScanResult( + scanResult: ScanResult, + options: Options & MonitorOptions, + path: string, +): Promise<{ + monitorResult?: EcosystemMonitorResult; + monitorError?: EcosystemMonitorError; +}> { + const monitorDependenciesRequest = await generateMonitorDependenciesRequest( + scanResult, + options, + ); + + const configOrg = config.org ? decodeURIComponent(config.org) : undefined; + const payload = { + method: 'PUT', + url: `${config.API}/monitor-dependencies`, + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: getAuthHeader(), + }, + body: monitorDependenciesRequest, + qs: { + org: options.org || configOrg, + }, + }; + + try { + const response = await makeRequest(payload); + return { + monitorResult: { + ...response, + path, + scanResult, + }, + }; + } catch (error) { + if (error.code === 401) { + throw AuthFailedError(); + } + if (error.code >= 400 && error.code < 500) { + throw new MonitorError(error.code, error.message); + } + return { + monitorError: { + error: 'Could not monitor dependencies in ' + path, + path, + scanResult, + }, + }; + } +} + export async function getFormattedMonitorOutput( results: Array, monitorResults: EcosystemMonitorResult[], diff --git a/test/jest/unit/ecosystems-monitor-docker.spec.ts b/test/jest/unit/ecosystems-monitor-docker.spec.ts index b2f113b9cc..f06f6c231f 100644 --- a/test/jest/unit/ecosystems-monitor-docker.spec.ts +++ b/test/jest/unit/ecosystems-monitor-docker.spec.ts @@ -285,4 +285,89 @@ describe('monitorEcosystem docker/container', () => { ); expect(parsedOutput.projectName).not.toBe('my-custom-project-name'); }); + + it('should monitor base scan result first and then parallelize remaining requests', async () => { + const baseScanResult = { + ...readJsonFixture('container-deb-scan-result.json'), + identity: { + type: 'deb', + targetFile: 'base-os', + }, + } as ScanResult; + const appScanResultOne = { + ...readJsonFixture('maven-project-0-dependencies-scan-result.json'), + identity: { + type: 'maven', + targetFile: 'app-1', + }, + } as ScanResult; + const appScanResultTwo = { + ...readJsonFixture('maven-project-0-dependencies-scan-result.json'), + identity: { + type: 'maven', + targetFile: 'app-2', + }, + } as ScanResult; + + jest.spyOn(dockerPlugin, 'scan').mockResolvedValue({ + scanResults: [baseScanResult, appScanResultOne, appScanResultTwo], + }); + + const requestsByIdentity: string[] = []; + let baseRequestResolved = false; + let appOneStartedBeforeBaseResolved = false; + let appTwoStartedBeforeBaseResolved = false; + + jest.spyOn(request, 'makeRequest').mockImplementation((payload: any) => { + const identity = payload.body.scanResult.identity.targetFile as string; + requestsByIdentity.push(identity); + const baseResponse = readJsonFixture( + 'monitor-dependencies-response-with-project-name.json', + ) as ecosystemsTypes.MonitorDependenciesResponse; + const responseForIdentity = { + ...baseResponse, + id: `${identity}-id`, + projectName: identity, + }; + + return new Promise((resolve) => { + if (identity === 'base-os') { + setTimeout(() => { + baseRequestResolved = true; + resolve(responseForIdentity); + }, 25); + return; + } + + if (identity === 'app-1' && !baseRequestResolved) { + appOneStartedBeforeBaseResolved = true; + } + if (identity === 'app-2' && !baseRequestResolved) { + appTwoStartedBeforeBaseResolved = true; + } + + resolve(responseForIdentity); + }); + }); + + const [monitorResults, monitorErrors] = await ecosystems.monitorEcosystem( + 'docker', + ['/srv'], + { + path: '/srv', + docker: true, + org: 'my-org', + }, + ); + + expect(monitorErrors).toEqual([]); + expect(requestsByIdentity).toEqual(['base-os', 'app-1', 'app-2']); + expect(appOneStartedBeforeBaseResolved).toBe(false); + expect(appTwoStartedBeforeBaseResolved).toBe(false); + expect(monitorResults.map((result) => result.projectName)).toEqual([ + 'base-os', + 'app-1', + 'app-2', + ]); + }); });