-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(server) handle ignored body and node:http destroy #10268
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
8a6c07d
if Locked and never consumed we still need to handle it
cirospaciari db6b2a8
memory leak test
cirospaciari 862ab39
body leak tests
cirospaciari 17f3ef7
ops
cirospaciari 1ec7090
rename lol
cirospaciari e0992c0
process for ignored streams
cirospaciari 9402c19
stream not consumed case
cirospaciari f2a356c
undo
cirospaciari 8437630
call cancel instead of done
cirospaciari 0425551
fix peak memory usage
cirospaciari bc5ee9b
tweeaks
cirospaciari d73ae5f
add a test for this fix too
cirospaciari a237677
cleanup
cirospaciari 1f5342b
echo test
cirospaciari ee589a7
expected in ignore
cirospaciari 15957ef
simplify
cirospaciari c484e80
cleanup
cirospaciari 6bf9383
make cancel actually cancel
cirospaciari a3bff6a
more time
cirospaciari fc2e017
oopsie
cirospaciari 8efc161
skip + time
cirospaciari f479fc4
done actually mark source as done now
cirospaciari 0d689a2
reduce test load
cirospaciari 8320ebc
still slow
cirospaciari 1e9947f
less flaky
cirospaciari 111c476
tweaks
cirospaciari 634c197
WIP end current request properly
cirospaciari a7a735f
oops
cirospaciari 3c425fa
add comment
cirospaciari 35ae823
revert
cirospaciari 552f9b1
fix http destroy
cirospaciari 58ae8db
fix abort call
cirospaciari 87d4aad
opsie make this test actual be a test
cirospaciari 64ba7f3
change test hostname
cirospaciari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { join } from "path"; | ||
import { it, expect, beforeAll, afterAll } from "bun:test"; | ||
import { bunExe, bunEnv } from "harness"; | ||
import type { Subprocess } from "bun"; | ||
|
||
const ACCEPTABLE_MEMORY_LEAK = 2; //MB for acceptable memory leak variance | ||
const payload = "1".repeat(32 * 1024); // decent size payload to test memory leak | ||
|
||
let url: URL; | ||
let process: Subprocess<"ignore", "pipe", "inherit"> | null = null; | ||
beforeAll(async () => { | ||
process = Bun.spawn([bunExe(), "--smol", join(import.meta.dirname, "body-leak-test-fixture.ts")], { | ||
env: bunEnv, | ||
stdout: "pipe", | ||
stderr: "inherit", | ||
stdin: "ignore", | ||
}); | ||
const { value } = await process.stdout.getReader().read(); | ||
url = new URL(new TextDecoder().decode(value)); | ||
|
||
await warmup(); | ||
}); | ||
afterAll(() => { | ||
process?.kill(); | ||
}); | ||
|
||
async function getMemoryUsage(): Promise<number> { | ||
return (await fetch(`${url.origin}/report`).then(res => res.json())) as number; | ||
} | ||
|
||
async function warmup() { | ||
const batch = new Array(100); | ||
for (let i = 0; i < 100; i++) { | ||
for (let j = 0; j < 100; j++) { | ||
// warmup the server with streaming requests, because is the most memory intensive | ||
batch[j] = fetch(`${url.origin}/streaming`, { | ||
method: "POST", | ||
body: payload, | ||
}); | ||
} | ||
await Promise.all(batch); | ||
} | ||
// clean up memory before first test | ||
await getMemoryUsage(); | ||
} | ||
|
||
async function callBuffering() { | ||
const result = await fetch(`${url.origin}/buffering`, { | ||
method: "POST", | ||
body: payload, | ||
}).then(res => res.text()); | ||
expect(result).toBe("Ok"); | ||
} | ||
async function callStreaming() { | ||
const result = await fetch(`${url.origin}/streaming`, { | ||
method: "POST", | ||
body: payload, | ||
}).then(res => res.text()); | ||
expect(result).toBe("Ok"); | ||
} | ||
async function callIncompleteStreaming() { | ||
const result = await fetch(`${url.origin}/incomplete-streaming`, { | ||
method: "POST", | ||
body: payload, | ||
}).then(res => res.text()); | ||
expect(result).toBe("Ok"); | ||
} | ||
async function callStreamingEcho() { | ||
const result = await fetch(`${url.origin}/streaming-echo`, { | ||
method: "POST", | ||
body: payload, | ||
}).then(res => res.text()); | ||
expect(result).toBe(payload); | ||
} | ||
async function callIgnore() { | ||
const result = await fetch(url, { | ||
method: "POST", | ||
body: payload, | ||
}).then(res => res.text()); | ||
expect(result).toBe("Ok"); | ||
} | ||
|
||
async function calculateMemoryLeak(fn: () => Promise<void>) { | ||
const start_memory = await getMemoryUsage(); | ||
const memory_examples: Array<number> = []; | ||
let peak_memory = start_memory; | ||
const batch = new Array(100); | ||
for (let i = 0; i < 100; i++) { | ||
for (let j = 0; j < 100; j++) { | ||
batch[j] = fn(); | ||
} | ||
await Promise.all(batch); | ||
// garbage collect and check memory usage every 1000 requests | ||
if (i > 0 && i % 10 === 0) { | ||
const report = await getMemoryUsage(); | ||
if (report > peak_memory) { | ||
peak_memory = report; | ||
} | ||
memory_examples.push(report); | ||
} | ||
} | ||
|
||
// wait for the last memory usage to be stable | ||
const end_memory = await getMemoryUsage(); | ||
if (end_memory > peak_memory) { | ||
peak_memory = end_memory; | ||
} | ||
// use first example as a reference if is a memory leak this should keep increasing and not be stable | ||
const consumption = end_memory - memory_examples[0]; | ||
// memory leak in MB | ||
const leak = Math.floor(consumption > 0 ? consumption / 1024 / 1024 : 0); | ||
return { leak, start_memory, peak_memory, end_memory, memory_examples }; | ||
} | ||
|
||
for (const test_info of [ | ||
["#10265 should not leak memory when ignoring the body", callIgnore, false], | ||
["should not leak memory when buffering the body", callBuffering, false], | ||
["should not leak memory when streaming the body", callStreaming, false], | ||
["should not leak memory when streaming the body incompletely", callIncompleteStreaming, true], | ||
["should not leak memory when streaming the body and echoing it back", callStreamingEcho, true], | ||
]) { | ||
const [testName, fn, skip] = test_info as [string, () => Promise<void>, boolean]; | ||
it.todoIf(skip)( | ||
testName, | ||
async () => { | ||
const report = await calculateMemoryLeak(fn); | ||
// peak memory is too high | ||
expect(report.peak_memory > report.start_memory * 2).toBe(false); | ||
// acceptable memory leak | ||
expect(report.leak).toBeLessThanOrEqual(ACCEPTABLE_MEMORY_LEAK); | ||
}, | ||
30_000, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just calling
abort
should be enoughThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abort/cancel actually was only canceling the stream not the source, so I fixed that now.
onCancel actually only mark as done, not actually emits a error so I changed the test that I add to match this expectations.