Skip to content

Commit 3ba9be0

Browse files
majiayu000claude
andcommitted
fix(http): prevent CPU spin on SSL connection pool idle sockets
When SSL connections are released to the keep-alive pool, they remain registered for READABLE events. This can cause 100% CPU usage when: 1. SSL has buffered data in internal OpenSSL buffers 2. Server sends unexpected data on the pooled connection This fix: - Pauses readable events on sockets when released to the pool - Resumes readable events when socket is reused from the pool - Closes pooled sockets that receive unexpected data instead of ignoring Fixes #25430 Co-Authored-By: Claude <[email protected]>
1 parent a2d8b75 commit 3ba9be0

File tree

2 files changed

+163
-2
lines changed

2 files changed

+163
-2
lines changed

src/http/HTTPContext.zig

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
189189
pending.hostname_len = @as(u8, @truncate(hostname.len));
190190
pending.port = port;
191191

192+
// Pause readable events on pooled sockets to prevent CPU spin
193+
// when SSL has buffered data or server sends unexpected data.
194+
// The socket will be resumed when reused in existingSocket().
195+
_ = socket.pauseStream();
196+
192197
log("Keep-Alive release {s}:{d}", .{
193198
hostname,
194199
port,
@@ -323,8 +328,10 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
323328
return;
324329
}
325330

326-
log("Unexpected data on socket", .{});
327-
331+
// Unexpected data on a pooled socket - close it to prevent
332+
// potential busy-polling if server sends unsolicited data.
333+
log("Unexpected data on pooled socket, closing", .{});
334+
terminateSocket(socket);
328335
return;
329336
}
330337
log("Unexpected data on unknown socket", .{});
@@ -423,6 +430,10 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
423430
continue;
424431
}
425432

433+
// Resume readable events before returning the socket for reuse.
434+
// The socket was paused when released to the pool in releaseSocket().
435+
_ = http_socket.resumeStream();
436+
426437
assert(context().pending_sockets.put(socket));
427438
log("+ Keep-Alive reuse {s}:{d}", .{ hostname, port });
428439
return http_socket;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { expect, test, describe } from "bun:test";
2+
import { bunEnv, bunExe } from "harness";
3+
4+
// Test for https://github.com/oven-sh/bun/issues/25430
5+
// HTTPS fetch in node:http server should not cause 100% CPU usage
6+
describe("#25430 CPU spin after HTTPS fetch in node:http server", () => {
7+
test("CPU should be idle after HTTPS fetch completes", async () => {
8+
// Spawn a separate process to test CPU usage
9+
// The process makes HTTPS requests and then measures CPU usage
10+
await using proc = Bun.spawn({
11+
cmd: [
12+
bunExe(),
13+
"-e",
14+
`
15+
const http = require("node:http");
16+
17+
const server = http.createServer(async (req, res) => {
18+
// Make multiple HTTPS fetch requests to trigger connection pooling
19+
await Promise.all([
20+
fetch("https://example.com"),
21+
fetch("https://example.com"),
22+
]);
23+
res.writeHead(200, { "Content-Type": "application/json" });
24+
res.end(JSON.stringify({ ok: true }));
25+
});
26+
27+
server.listen(0, async () => {
28+
const port = server.address().port;
29+
30+
// Make a request to trigger the HTTPS fetches
31+
const response = await fetch("http://localhost:" + port);
32+
const data = await response.json();
33+
34+
if (!data.ok) {
35+
console.error("Request failed");
36+
process.exit(1);
37+
}
38+
39+
// Wait a moment for connections to settle into the pool
40+
await Bun.sleep(100);
41+
42+
// Measure CPU usage over 500ms
43+
const startUsage = process.cpuUsage();
44+
await Bun.sleep(500);
45+
const endUsage = process.cpuUsage(startUsage);
46+
47+
// Calculate CPU percentage
48+
const totalCpuTime = endUsage.user + endUsage.system;
49+
const elapsedMicros = 500 * 1000; // 500ms in microseconds
50+
const cpuPercent = (totalCpuTime / elapsedMicros) * 100;
51+
52+
server.close();
53+
54+
// CPU should be mostly idle (< 20% of elapsed time)
55+
// Before the fix, this would be ~100%
56+
if (cpuPercent >= 20) {
57+
console.error("CPU usage too high: " + cpuPercent.toFixed(2) + "%");
58+
process.exit(1);
59+
}
60+
61+
console.log("CPU usage: " + cpuPercent.toFixed(2) + "% (OK)");
62+
process.exit(0);
63+
});
64+
`,
65+
],
66+
env: bunEnv,
67+
stdout: "pipe",
68+
stderr: "pipe",
69+
});
70+
71+
const [stdout, stderr, exitCode] = await Promise.all([
72+
new Response(proc.stdout).text(),
73+
new Response(proc.stderr).text(),
74+
proc.exited,
75+
]);
76+
77+
if (exitCode !== 0) {
78+
console.log("stdout:", stdout);
79+
console.log("stderr:", stderr);
80+
}
81+
82+
expect(exitCode).toBe(0);
83+
});
84+
85+
test("Multiple sequential requests should not accumulate CPU usage", async () => {
86+
await using proc = Bun.spawn({
87+
cmd: [
88+
bunExe(),
89+
"-e",
90+
`
91+
const http = require("node:http");
92+
93+
const server = http.createServer(async (req, res) => {
94+
await fetch("https://example.com");
95+
res.writeHead(200);
96+
res.end("ok");
97+
});
98+
99+
server.listen(0, async () => {
100+
const port = server.address().port;
101+
102+
// Make multiple sequential requests
103+
for (let i = 0; i < 3; i++) {
104+
const response = await fetch("http://localhost:" + port);
105+
await response.text();
106+
}
107+
108+
// Wait for connections to settle
109+
await Bun.sleep(100);
110+
111+
// Measure CPU usage
112+
const startUsage = process.cpuUsage();
113+
await Bun.sleep(500);
114+
const endUsage = process.cpuUsage(startUsage);
115+
116+
const totalCpuTime = endUsage.user + endUsage.system;
117+
const elapsedMicros = 500 * 1000;
118+
const cpuPercent = (totalCpuTime / elapsedMicros) * 100;
119+
120+
server.close();
121+
122+
if (cpuPercent >= 20) {
123+
console.error("CPU usage too high after sequential requests: " + cpuPercent.toFixed(2) + "%");
124+
process.exit(1);
125+
}
126+
127+
console.log("CPU usage after sequential requests: " + cpuPercent.toFixed(2) + "% (OK)");
128+
process.exit(0);
129+
});
130+
`,
131+
],
132+
env: bunEnv,
133+
stdout: "pipe",
134+
stderr: "pipe",
135+
});
136+
137+
const [stdout, stderr, exitCode] = await Promise.all([
138+
new Response(proc.stdout).text(),
139+
new Response(proc.stderr).text(),
140+
proc.exited,
141+
]);
142+
143+
if (exitCode !== 0) {
144+
console.log("stdout:", stdout);
145+
console.log("stderr:", stderr);
146+
}
147+
148+
expect(exitCode).toBe(0);
149+
});
150+
});

0 commit comments

Comments
 (0)