diff --git a/src/lib/snyk-test/common.ts b/src/lib/snyk-test/common.ts index 73dcf723cd..3116397e4f 100644 --- a/src/lib/snyk-test/common.ts +++ b/src/lib/snyk-test/common.ts @@ -111,6 +111,28 @@ export type FailOn = 'all' | 'upgradable' | 'patchable'; export const RETRY_ATTEMPTS = 3; export const RETRY_DELAY = 500; +const DEFAULT_REQUEST_CONCURRENCY = 10; +const MIN_REQUEST_CONCURRENCY = 1; +const MAX_REQUEST_CONCURRENCY = 50; + +/** + * Returns the maximum number of in-flight Snyk dependency-test or + * dependency-monitor HTTP requests permitted at once. Override with the + * SNYK_REQUEST_CONCURRENCY environment variable; values are clamped to + * [MIN_REQUEST_CONCURRENCY, MAX_REQUEST_CONCURRENCY]. + */ +export function getRequestConcurrency(): number { + const raw = process.env.SNYK_REQUEST_CONCURRENCY; + if (!raw) { + return DEFAULT_REQUEST_CONCURRENCY; + } + const parsed = parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < MIN_REQUEST_CONCURRENCY) { + return DEFAULT_REQUEST_CONCURRENCY; + } + return Math.min(parsed, MAX_REQUEST_CONCURRENCY); +} + /** * printDepGraph writes the given dep-graph and target name to the destination * stream as expected by the `depgraph` CLI workflow. diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 7be926b564..7210d73129 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -39,6 +39,7 @@ import { isCI } from '../is-ci'; import { RETRY_ATTEMPTS, RETRY_DELAY, + getRequestConcurrency, printDepGraph, printEffectiveDepGraph, printEffectiveDepGraphError, @@ -94,9 +95,6 @@ import { ProblemError } from '@snyk/error-catalog-nodejs-public'; const debug = debugModule('snyk:run-test'); -// Controls the number of simultaneous test requests that can be in-flight. -const MAX_CONCURRENCY = 5; - function prepareResponseForParsing( payload: Payload, response: TestDependenciesResponse, @@ -293,7 +291,7 @@ async function sendAndParseResults( }; const responses = await pMap(payloads, sendRequest, { - concurrency: MAX_CONCURRENCY, + concurrency: getRequestConcurrency(), }); for (const { payload, originalPayload, response } of responses) { diff --git a/test/jest/unit/lib/snyk-test/common.spec.ts b/test/jest/unit/lib/snyk-test/common.spec.ts index 5afe8cad37..4df25d4c4e 100644 --- a/test/jest/unit/lib/snyk-test/common.spec.ts +++ b/test/jest/unit/lib/snyk-test/common.spec.ts @@ -1,7 +1,10 @@ import { CLI, ProblemError } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from '../../../../../src/lib/errors'; import { FailedProjectScanError } from '../../../../../src/lib/plugins/get-multi-plugin-result'; -import { getOrCreateErrorCatalogError } from '../../../../../src/lib/snyk-test/common'; +import { + getOrCreateErrorCatalogError, + getRequestConcurrency, +} from '../../../../../src/lib/snyk-test/common'; describe('getOrCreateErrorCatalogError', () => { const defaultErrMessage = 'Default error message'; @@ -116,3 +119,60 @@ describe('getOrCreateErrorCatalogError', () => { expect(result.detail).toBe(defaultErrMessage); }); }); + +describe('getRequestConcurrency', () => { + const originalValue = process.env.SNYK_REQUEST_CONCURRENCY; + + afterEach(() => { + if (originalValue === undefined) { + delete process.env.SNYK_REQUEST_CONCURRENCY; + } else { + process.env.SNYK_REQUEST_CONCURRENCY = originalValue; + } + }); + + it('returns the default of 10 when SNYK_REQUEST_CONCURRENCY is unset', () => { + delete process.env.SNYK_REQUEST_CONCURRENCY; + expect(getRequestConcurrency()).toBe(10); + }); + + it('returns the default of 10 when SNYK_REQUEST_CONCURRENCY is empty', () => { + process.env.SNYK_REQUEST_CONCURRENCY = ''; + expect(getRequestConcurrency()).toBe(10); + }); + + it('returns the parsed value when SNYK_REQUEST_CONCURRENCY is a valid integer', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '15'; + expect(getRequestConcurrency()).toBe(15); + }); + + it('clamps to the maximum of 50 when SNYK_REQUEST_CONCURRENCY exceeds the cap', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '500'; + expect(getRequestConcurrency()).toBe(50); + }); + + it('returns the default when SNYK_REQUEST_CONCURRENCY is below the minimum', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '0'; + expect(getRequestConcurrency()).toBe(10); + }); + + it('returns the default when SNYK_REQUEST_CONCURRENCY is negative', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '-5'; + expect(getRequestConcurrency()).toBe(10); + }); + + it('returns the default when SNYK_REQUEST_CONCURRENCY is non-numeric', () => { + process.env.SNYK_REQUEST_CONCURRENCY = 'abc'; + expect(getRequestConcurrency()).toBe(10); + }); + + it('honors the minimum boundary', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '1'; + expect(getRequestConcurrency()).toBe(1); + }); + + it('honors the maximum boundary', () => { + process.env.SNYK_REQUEST_CONCURRENCY = '50'; + expect(getRequestConcurrency()).toBe(50); + }); +});