Drop a socket's 'drain' listeners when it closes

The close step drops a socket's listener lists so that a listener which
captures its socket does not keep it alive; the 'drain' list was not
among them. A once('drain') listener still pending when the peer
disconnected, or an on('drain') listener, held its socket and the
socket's buffers after 'close'. The close step now drops the 'drain'
listeners with the others.
This commit is contained in:
Chris Tate
2026-10-25 09:25:45 -05:00
parent adf97e900f
commit 64e350edb6
4 changed files with 70 additions and 0 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7519-net-socket-drain-listener-close.ts"
],
"diags": []
}
+1
View File
@@ -5648,6 +5648,7 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) {
scr_net_ls_drop(&sock->conn_ls);
scr_net_ls_drop(&sock->timeout_ls);
scr_net_ls_drop(&sock->readable_ls);
scr_net_ls_drop(&sock->drain_ls);
scr_net_ls_drop(&sock->finish_ls);
scr_net_ls_drop(&sock->wcb_ls);
scr_net_ls_drop(&sock->ready_ls);
@@ -0,0 +1,62 @@
// 'drain' listeners that capture their socket do not keep it after it
// closes: a once('drain') listener still pending when the peer disconnects
// before the buffer drains, and an on('drain') listener on a socket that
// drained and then closed. The sanitized lane's exit audit checks that
// both sockets are freed.
import { connect, createServer, Socket } from "node:net";
function disconnectBeforeDrain(): Promise<void> {
return new Promise<void>((resolve) => {
const server = createServer((socket: Socket) => {
socket.on("error", () => {});
socket.once("drain", () => console.log("unexpected drain", socket.writableLength));
console.log("server write", socket.write(Buffer.alloc(16 * 1024 * 1024, 97)));
socket.on("close", () => {
console.log("server socket closed, need drain", socket.writableNeedDrain);
server.close(() => resolve());
});
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
const client = connect(port, "127.0.0.1", () => {
client.pause();
setTimeout(() => client.destroy(), 30);
});
});
});
}
function drainThenClose(): Promise<void> {
return new Promise<void>((resolve) => {
const server = createServer((socket: Socket) => {
socket.pause();
setTimeout(() => socket.resume(), 30);
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
const client = connect(port, "127.0.0.1", () => {
let drains = 0;
client.on("drain", () => {
drains++;
console.log("drain", drains, client.writableLength);
client.end();
});
console.log("client write", client.write(Buffer.alloc(16 * 1024 * 1024, 98)));
});
client.on("close", () => {
console.log("client closed");
server.close(() => resolve());
});
});
});
}
async function main(): Promise<void> {
await disconnectBeforeDrain();
await drainThenClose();
console.log("done");
}
main();
@@ -0,0 +1 @@
{ "plain": 2300, "san": 3100 }