diff --git a/completions/bun-cli.json b/completions/bun-cli.json index 5bcf1c989fbd..2a14b1a1ab85 100644 --- a/completions/bun-cli.json +++ b/completions/bun-cli.json @@ -427,7 +427,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -748,7 +748,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -1079,7 +1079,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -1351,7 +1351,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -1769,7 +1769,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -2149,7 +2149,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -2432,7 +2432,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -2691,7 +2691,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -2943,7 +2943,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -3248,7 +3248,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, @@ -3677,7 +3677,7 @@ }, { "name": "network-concurrency", - "description": "Maximum number of concurrent network requests (default 48)", + "description": "Maximum number of concurrent network requests (default 64)", "hasValue": true, "valueType": "val", "required": false, diff --git a/docs/snippets/cli/add.mdx b/docs/snippets/cli/add.mdx index c12f274153b1..e379acb9fde8 100644 --- a/docs/snippets/cli/add.mdx +++ b/docs/snippets/cli/add.mdx @@ -115,7 +115,7 @@ bun add <@version> variables - + Maximum number of concurrent network requests diff --git a/docs/snippets/cli/install.mdx b/docs/snippets/cli/install.mdx index b0083696f758..e28ccdb7e312 100644 --- a/docs/snippets/cli/install.mdx +++ b/docs/snippets/cli/install.mdx @@ -156,7 +156,7 @@ bun install @ Maximum number of concurrent jobs for lifecycle scripts (default: 2x CPU cores) - + Maximum number of concurrent network requests diff --git a/docs/snippets/cli/link.mdx b/docs/snippets/cli/link.mdx index dd125b9cc34e..c7b134382e8d 100644 --- a/docs/snippets/cli/link.mdx +++ b/docs/snippets/cli/link.mdx @@ -92,7 +92,7 @@ bun link variables - + Maximum number of concurrent network requests diff --git a/docs/snippets/cli/outdated.mdx b/docs/snippets/cli/outdated.mdx index f1813d331f48..336336993f71 100644 --- a/docs/snippets/cli/outdated.mdx +++ b/docs/snippets/cli/outdated.mdx @@ -98,8 +98,8 @@ bun outdated Use a specific registry by default, overriding .npmrc, bunfig.toml and environment variables - - Maximum number of concurrent network requests (default 48) + + Maximum number of concurrent network requests (default 64) ### Caching diff --git a/docs/snippets/cli/patch.mdx b/docs/snippets/cli/patch.mdx index 8fbe95d03367..39230c34865d 100644 --- a/docs/snippets/cli/patch.mdx +++ b/docs/snippets/cli/patch.mdx @@ -100,8 +100,8 @@ bun patch @ variables - - Maximum number of concurrent network requests (default 48) + + Maximum number of concurrent network requests (default 64) ### Performance & Resource diff --git a/docs/snippets/cli/publish.mdx b/docs/snippets/cli/publish.mdx index 478c7fe30c8a..92586ba0bf5e 100644 --- a/docs/snippets/cli/publish.mdx +++ b/docs/snippets/cli/publish.mdx @@ -171,7 +171,7 @@ bun publish --cafile ./ca-cert.pem Platform optimizations: `clonefile` (default), `hardlink`, `symlink`, or `copyfile` - + Maximum concurrent network requests diff --git a/docs/snippets/cli/remove.mdx b/docs/snippets/cli/remove.mdx index b423e83100d9..fc56e2e4b83b 100644 --- a/docs/snippets/cli/remove.mdx +++ b/docs/snippets/cli/remove.mdx @@ -145,6 +145,6 @@ bun remove hardlink, symlink, copyfile - - Maximum number of concurrent network requests (default 48) + + Maximum number of concurrent network requests (default 64) diff --git a/docs/snippets/cli/update.mdx b/docs/snippets/cli/update.mdx index f840a6e3d6f4..2aa994e8be1b 100644 --- a/docs/snippets/cli/update.mdx +++ b/docs/snippets/cli/update.mdx @@ -78,8 +78,8 @@ bun up Use a specific registry by default, overriding .npmrc, bunfig.toml and environment variables - - Maximum number of concurrent network requests (default 48) + + Maximum number of concurrent network requests (default 64) ### Caching diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index be895af25bf1..dfd92eb3c21a 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -50,6 +50,15 @@ const BACKEND_PARAM: ParamType = clap::param!( "--backend Platform-specific optimizations for installing dependencies. Possible values: \"hardlink\" (default), \"symlink\", \"copyfile\"" ); +const NETWORK_CONCURRENCY_PARAM: ParamType = clap::param!( + "--network-concurrency Maximum number of concurrent network requests (default 64)" +); +const _: () = assert!( + super::DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL == 64 + && super::DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL_FOR_PROXIES == 64, + "update the default in the --network-concurrency help text (and docs/snippets/cli/*.mdx)" +); + const SHARED_HEAD_PARAMS: &[ParamType] = &[ clap::param!("-c, --config ? Specify path to config file (bunfig.toml)"), clap::param!("-y, --yarn Write a yarn.lock file (yarn v1)"), @@ -103,9 +112,7 @@ const SHARED_TAIL_PARAMS: &[ParamType] = &[ clap::param!( "--concurrent-scripts Maximum number of concurrent jobs for lifecycle scripts (default: 2x CPU cores)" ), - clap::param!( - "--network-concurrency Maximum number of concurrent network requests (default 48)" - ), + NETWORK_CONCURRENCY_PARAM, clap::param!("--save-text-lockfile Save a text-based lockfile"), clap::param!( "--omit ... Exclude 'dev', 'optional', or 'peer' dependencies from install" diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 279851e5dd27..fb50b57cd80a 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -67,6 +67,66 @@ async function withContext( // Default context options for most tests const defaultOpts = { linker: "hoisted" as const }; +// BUN_CONFIG_MAX_HTTP_REQUESTS is documented as another way to set the request +// limit, so it must not leak in from the outer environment while measuring it. +const networkConcurrencyEnv = { ...env, BUN_CONFIG_MAX_HTTP_REQUESTS: undefined }; + +// Runs `bun install` with one dependency more than `limit`, against a registry +// that holds every manifest request open, and asserts that bun has exactly +// `limit` requests in flight: the extra dependency has to wait for a slot. +async function expectInstallInFlightLimit(ctx: TestContext, limit: number) { + const dependencies: Record = {}; + for (let i = 0; i < limit + 1; i++) dependencies[`dep-${i}`] = "^1"; + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ name: "foo", version: "0.0.1", dependencies }), + ); + + let inFlight = 0; + let maxInFlight = 0; + const limitReached = Promise.withResolvers(); + const limitExceeded = Promise.withResolvers(); + const release = Promise.withResolvers(); + setContextHandler(ctx, async () => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + if (inFlight >= limit) limitReached.resolve(); + if (inFlight > limit) limitExceeded.resolve(); + await release.promise; + inFlight--; + return new Response("404", { status: 404 }); + }); + + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env: networkConcurrencyEnv, + }); + try { + // A real limit below `limit` parks bun (and this wait) with fewer requests + // in flight and no further signal, so the wait is bounded (generously: this + // is a debug build under CI load) and the assertions below report what was + // reached. proc.exited covers bun giving up early. + await Promise.race([limitReached.promise, proc.exited, Bun.sleep(30_000)]); + // bun sends everything its limit allows in one burst, so a request beyond + // the limit arrives right behind the others. That it never arrives can only + // be observed by giving it a moment to show up. + await Promise.race([limitExceeded.promise, Bun.sleep(500)]); + } finally { + release.resolve(); + } + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(maxInFlight).toBe(limit); + expect(ctx.requested).toBe(limit + 1); + expect(stderr).toContain("failed to resolve"); + expect(stdout).toContain("bun install v1."); + expect(exitCode).toBe(1); +} + const gitEnv = { ...bunEnv, GIT_AUTHOR_NAME: "bun-test", @@ -121,6 +181,26 @@ function serveDirectory(root: string) { } describe.concurrent("bun-install", () => { + it("bun install --help states the --network-concurrency default that bun install actually uses", async () => { + await using help = spawn({ + cmd: [bunExe(), "install", "--help"], + stdout: "pipe", + stderr: "pipe", + env, + }); + const [helpStdout, helpStderr, helpExitCode] = await Promise.all([ + help.stdout.text(), + help.stderr.text(), + help.exited, + ]); + const helpLine = (helpStdout + helpStderr).split(/\r?\n/).find(line => line.includes("--network-concurrency")); + const documentedDefault = /Maximum number of concurrent network requests \(default (\d+)\)$/.exec(helpLine ?? ""); + expect(documentedDefault).not.toBeNull(); + expect(helpExitCode).toBe(0); + + await withContext(defaultOpts, ctx => expectInstallInFlightLimit(ctx, Number(documentedDefault![1]))); + }); + for (let input of ["abcdef", "65537", "-1"]) { it(`bun install --network-concurrency=${input} fails`, async () => { await withContext(defaultOpts, async ctx => { @@ -138,7 +218,7 @@ describe.concurrent("bun-install", () => { }`, ); const { stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--network-concurrency", "abcdef"], + cmd: [bunExe(), "install", "--network-concurrency", input], cwd: ctx.package_dir, stdout: "inherit", stdin: "inherit", @@ -146,7 +226,7 @@ describe.concurrent("bun-install", () => { env, }); const err = await stderr.text(); - expect(err).toContain("Expected --network-concurrency to be a number between 0 and 65535"); + expect(err).toContain(`Expected --network-concurrency to be a number between 0 and 65535: ${input}`); expect(await exited).toBe(1); expect(urls).toBeEmpty(); });