Skip to content

Commit

Permalink
Deflake fetch tests (#16000)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner authored Dec 27, 2024
1 parent d8e644f commit 7b06872
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 63 deletions.
1 change: 1 addition & 0 deletions test/js/bun/http/async-iterator-throws.fixture.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const server = Bun.serve({
port: 0,
idleTimeout: 0,

async fetch(req) {
return new Response(
Expand Down
1 change: 1 addition & 0 deletions test/js/bun/http/big-form-data.fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const content = Buffer.alloc(3 * 15360000, "Bun").toString();

const server = Bun.serve({
port: 0,
idleTimeout: 0,
fetch: async req => {
const data = await req.formData();
return new Response(data.get("name") === content ? "OK" : "NO");
Expand Down
1 change: 1 addition & 0 deletions test/js/bun/http/body-leak-test-fixture.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/js/bun/http/readable-stream-throws.fixture.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const server = Bun.serve({
port: 0,

idleTimeout: 0,
error(err) {
return new Response("Failed", { status: 555 });
},
Expand Down
1 change: 1 addition & 0 deletions test/js/bun/http/rejected-promise-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 25 additions & 31 deletions test/js/bun/http/serve-body-leak.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,9 @@ const totalCount = 10_000;
const zeroCopyPayload = new Blob([payload]);
const zeroCopyJSONPayload = new Blob([JSON.stringify({ bun: payload })]);

let url: URL;
let process: Subprocess<"ignore", "pipe", "inherit"> | null = null;
beforeEach(async () => {
if (process) {
process?.kill();
}

let defer = Promise.withResolvers();
process = Bun.spawn([bunExe(), "--smol", join(import.meta.dirname, "body-leak-test-fixture.ts")], {
async function getURL() {
let defer = Promise.withResolvers<string>();
const process = Bun.spawn([bunExe(), "--smol", join(import.meta.dirname, "body-leak-test-fixture.ts")], {
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
Expand All @@ -26,19 +20,17 @@ beforeEach(async () => {
defer.resolve(message);
},
});
url = new URL(await defer.promise);
const url: URL = new URL(await defer.promise);
process.unref();
await warmup();
});
afterEach(() => {
process?.kill();
});
await warmup(url);
return { url, process };
}

async function getMemoryUsage(): Promise<number> {
async function getMemoryUsage(url: URL): Promise<number> {
return (await fetch(`${url.origin}/report`).then(res => res.json())) as number;
}

async function warmup() {
async function warmup(url: URL) {
var remaining = totalCount;

while (remaining > 0) {
Expand All @@ -54,77 +46,77 @@ async function warmup() {
remaining -= batchSize;
}
// clean up memory before first test
await getMemoryUsage();
await getMemoryUsage(url);
}

async function callBuffering() {
async function callBuffering(url: URL) {
const result = await fetch(`${url.origin}/buffering`, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}
async function callJSONBuffering() {
async function callJSONBuffering(url: URL) {
const result = await fetch(`${url.origin}/json-buffering`, {
method: "POST",
body: zeroCopyJSONPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}

async function callBufferingBodyGetter() {
async function callBufferingBodyGetter(url: URL) {
const result = await fetch(`${url.origin}/buffering+body-getter`, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}
async function callStreaming() {
async function callStreaming(url: URL) {
const result = await fetch(`${url.origin}/streaming`, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}
async function callIncompleteStreaming() {
async function callIncompleteStreaming(url: URL) {
const result = await fetch(`${url.origin}/incomplete-streaming`, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}
async function callStreamingEcho() {
async function callStreamingEcho(url: URL) {
const result = await fetch(`${url.origin}/streaming-echo`, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe(payload);
}
async function callIgnore() {
async function callIgnore(url: URL) {
const result = await fetch(url, {
method: "POST",
body: zeroCopyPayload,
}).then(res => res.text());
expect(result).toBe("Ok");
}

async function calculateMemoryLeak(fn: () => Promise<void>) {
const start_memory = await getMemoryUsage();
async function calculateMemoryLeak(fn: (url: URL) => Promise<void>, url: URL) {
const start_memory = await getMemoryUsage(url);
const memory_examples: Array<number> = [];
let peak_memory = start_memory;

var remaining = totalCount;
while (remaining > 0) {
const batch = new Array(batchSize);
for (let j = 0; j < batchSize; j++) {
batch[j] = fn();
batch[j] = fn(url);
}
await Promise.all(batch);
remaining -= batchSize;

// garbage collect and check memory usage every 1000 requests
if (remaining > 0 && remaining % 1000 === 0) {
const report = await getMemoryUsage();
const report = await getMemoryUsage(url);
if (report > peak_memory) {
peak_memory = report;
}
Expand All @@ -133,7 +125,7 @@ async function calculateMemoryLeak(fn: () => Promise<void>) {
}

// wait for the last memory usage to be stable
const end_memory = await getMemoryUsage();
const end_memory = await getMemoryUsage(url);
if (end_memory > peak_memory) {
peak_memory = end_memory;
}
Expand All @@ -160,7 +152,9 @@ for (const test_info of [
it.todoIf(skip)(
testName,
async () => {
const report = await calculateMemoryLeak(fn);
const { url, process } = await getURL();
await using processHandle = process;
const report = await calculateMemoryLeak(fn, url);
// peak memory is too high
expect(report.peak_memory).not.toBeGreaterThan(report.start_memory * 2.5);
// acceptable memory leak
Expand Down
1 change: 1 addition & 0 deletions test/js/bun/websocket/websocket-server-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions test/js/web/fetch/fetch-leak-test-fixture-3.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions test/js/web/fetch/fetch-leak-test-fixture-4.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions test/js/web/fetch/fetch-leak.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe("fetch doesn't leak", () => {
var count = 0;
using server = Bun.serve({
port: 0,

idleTimeout: 0,
fetch(req) {
count++;
return new Response(body);
Expand All @@ -20,7 +20,7 @@ describe("fetch doesn't leak", () => {
const proc = Bun.spawn({
env: {
...bunEnv,
SERVER: `http://${server.hostname}:${server.port}`,
SERVER: server.url.href,
COUNT: "200",
},
stderr: "inherit",
Expand Down Expand Up @@ -49,6 +49,7 @@ describe("fetch doesn't leak", () => {

const serveOptions = {
port: 0,
idleTimeout: 0,
fetch(req) {
return new Response(body, { headers });
},
Expand All @@ -62,8 +63,8 @@ describe("fetch doesn't leak", () => {

const env = {
...bunEnv,
SERVER: `${tls ? "https" : "http"}://${server.hostname}:${server.port}`,
BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString("10"),
SERVER: server.url.href,
BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString(10),
NAME: name,
};

Expand Down Expand Up @@ -105,6 +106,7 @@ describe.each(["FormData", "Blob", "Buffer", "String", "URLSearchParams", "strea
async () => {
using server = Bun.serve({
port: 0,
idleTimeout: 0,
fetch(req) {
return new Response();
},
Expand Down Expand Up @@ -151,7 +153,7 @@ test("do not leak", async () => {

let url;
let isDone = false;
server.listen(0, function attack() {
server.listen(0, "127.0.0.1", function attack() {
if (isDone) {
return;
}
Expand All @@ -165,7 +167,7 @@ test("do not leak", async () => {

let prev = Infinity;
let count = 0;
const interval = setInterval(() => {
var interval = setInterval(() => {
isDone = true;
gc();
const next = process.memoryUsage().heapUsed;
Expand Down
51 changes: 30 additions & 21 deletions test/js/web/fetch/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const fetchFixture4 = join(import.meta.dir, "fetch-leak-test-fixture-4.js");
let server: Server;
function startServer({ fetch, ...options }: ServeOptions) {
server = serve({
idleTimeout: 0,
...options,
fetch,
port: 0,
Expand Down Expand Up @@ -1314,9 +1315,16 @@ describe("Response", () => {
method: "POST",
body: await Bun.file(import.meta.dir + "/fixtures/file.txt").arrayBuffer(),
});
var input = await response.arrayBuffer();
const input = await response.bytes();
var output = await Bun.file(import.meta.dir + "/fixtures/file.txt").stream();
expect(new Uint8Array(input)).toEqual((await output.getReader().read()).value);
let chunks: Uint8Array[] = [];
const reader = output.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
expect(input).toEqual(Buffer.concat(chunks));
});
});

Expand Down Expand Up @@ -2018,35 +2026,31 @@ describe("http/1.1 response body length", () => {
});
describe("fetch Response life cycle", () => {
it("should not keep Response alive if not consumed", async () => {
const serverProcess = Bun.spawn({
let deferred = Promise.withResolvers<string>();

await using serverProcess = Bun.spawn({
cmd: [bunExe(), "--smol", fetchFixture3],
stderr: "inherit",
stdout: "pipe",
stdin: "ignore",
stdout: "inherit",
stdin: "inherit",
env: bunEnv,
ipc(message) {
deferred.resolve(message);
},
});

async function getServerUrl() {
const reader = serverProcess.stdout.getReader();
const { done, value } = await reader.read();
return new TextDecoder().decode(value);
}
const serverUrl = await getServerUrl();
const clientProcess = Bun.spawn({
const serverUrl = await deferred.promise;
await using clientProcess = Bun.spawn({
cmd: [bunExe(), "--smol", fetchFixture4, serverUrl],
stderr: "inherit",
stdout: "pipe",
stdin: "ignore",
stdout: "inherit",
stdin: "inherit",
env: bunEnv,
});
try {
expect(await clientProcess.exited).toBe(0);
} finally {
serverProcess.kill();
}
expect(await clientProcess.exited).toBe(0);
});
it("should allow to get promise result after response is GC'd", async () => {
const server = Bun.serve({
using server = Bun.serve({
port: 0,
async fetch(request: Request) {
return new Response(
Expand Down Expand Up @@ -2235,6 +2239,7 @@ describe("fetch should allow duplex", () => {
it("should work in redirects .manual when using duplex", async () => {
using server = Bun.serve({
port: 0,
idleTimeout: 0,
async fetch(req) {
if (req.url.indexOf("/redirect") === -1) {
return Response.redirect("/");
Expand Down Expand Up @@ -2298,7 +2303,11 @@ it("should allow to follow redirect if connection is closed, abort should work e
await once(server.listen(0), "listening");

try {
const response = await fetch(`http://localhost:${(server.address() as AddressInfo).port}/redirect`, {
let { address, port } = server.address() as AddressInfo;
if (address === "::") {
address = "[::]";
}
const response = await fetch(`http://${address}:${port}/redirect`, {
signal: AbortSignal.timeout(150),
});
if (type === "delay") {
Expand Down

0 comments on commit 7b06872

Please sign in to comment.