Skip to content

Commit

Permalink
Synchronously update internal sockets length so http.Agent pooling is…
Browse files Browse the repository at this point in the history
… used

Fixes #299
  • Loading branch information
lukekarrys committed Mar 29, 2024
1 parent b5f94e3 commit 2d90753
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 8 deletions.
71 changes: 63 additions & 8 deletions packages/agent-base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,81 @@ export abstract class Agent extends http.Agent {
);
}

// In order to support async signatures in `connect()` and Node's native
// connection pooling in http.Agent, the array of sockets for each origin has
// to be updated syncronously. This is so the length of the array is accurate
// when `addRequest()` is next called. We achieve this by creating a fake socket
// and adding it to this.sockets and incrementing totalSocketCount.
private incrementSockets(name: string) {
// If maxSockets and maxTotalSockets are both Infinity then there is no need
// to create a fake socket because Node.js native connection pooling will
// never be invoked.
if (this.maxSockets === Infinity && this.maxTotalSockets === Infinity) {
return null;
}
if (!this.sockets[name]) {
// All instances of `sockets` are expected TypeScript errors. The alternative is to
// add it as a private property of this class but that will break TypeScript subclassing.
// @ts-expect-error `sockets` is readonly in `@types/node` but we need to write to it
this.sockets[name] = [];
}
const fakeSocket = new net.Socket({ writable: false });
// @ts-expect-error

Check failure on line 99 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
this.sockets[name].push(fakeSocket);
// @ts-expect-error `totalSocketCount` isn't defined in `@types/node`
this.totalSocketCount++;
return fakeSocket;
}

private decrementSockets(name: string, socket: null | net.Socket) {
if (!this.sockets[name] || socket === null) {
return;
}
// @ts-expect-error

Check failure on line 110 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
const index = this.sockets[name].indexOf(socket);
if (index !== -1) {
// @ts-expect-error

Check failure on line 113 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
this.sockets[name].splice(index, 1);
// @ts-expect-error

Check failure on line 115 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
this.totalSocketCount--;
// @ts-expect-error

Check failure on line 117 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
if (this.sockets[name].length === 0) {
// @ts-expect-error

Check failure on line 119 in packages/agent-base/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
delete this.sockets[name];
}
}
}

createSocket(
req: http.ClientRequest,
options: AgentConnectOpts,
cb: (err: Error | null, s?: Duplex) => void
) {
// @ts-expect-error `getName()` isn't defined in `@types/node`
const name = this.getName(options);
const fakeSocket = this.incrementSockets(name);
const connectOpts = {
...options,
secureEndpoint: this.isSecureEndpoint(options),
};
Promise.resolve()
.then(() => this.connect(req, connectOpts))
.then((socket) => {
if (socket instanceof http.Agent) {
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
return socket.addRequest(req, connectOpts);
.then(
(socket) => {
this.decrementSockets(name, fakeSocket);
if (socket instanceof http.Agent) {
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
return socket.addRequest(req, connectOpts);
}
this[INTERNAL].currentSocket = socket;
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
super.createSocket(req, options, cb);
},
(err) => {
this.decrementSockets(name, fakeSocket);
cb(err);
}
this[INTERNAL].currentSocket = socket;
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
super.createSocket(req, options, cb);
}, cb);
);
}

createConnection(): Duplex {
Expand Down
49 changes: 49 additions & 0 deletions packages/agent-base/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,55 @@ describe('Agent (TypeScript)', () => {
});
});

it('should support `keepAlive: true` with `maxSockets`', async () => {
let reqCount = 0;
let connectCount = 0;

class MyAgent extends Agent {
async connect(_req: http.ClientRequest, opts: AgentConnectOpts) {
connectCount++;
assert(opts.secureEndpoint === false);
await sleep(10);
return net.connect(opts);
}
}
const agent = new MyAgent({ keepAlive: true, maxSockets: 1 });

const server = http.createServer(async (req, res) => {
expect(req.headers.connection).toEqual('keep-alive');
reqCount++;
await sleep(10);
res.end();
});
const addr = await listen(server);

try {
const resPromise = req(new URL('/foo', addr), { agent });
const res2Promise = req(new URL('/another', addr), { agent });

const res = await resPromise;
expect(reqCount).toEqual(1);
expect(connectCount).toEqual(1);
expect(res.headers.connection).toEqual('keep-alive');

res.resume();
const s1 = res.socket;
await once(s1, 'free');

const res2 = await res2Promise;
expect(reqCount).toEqual(2);
expect(connectCount).toEqual(1);
expect(res2.headers.connection).toEqual('keep-alive');
assert(res2.socket === s1);

res2.resume();
await once(res2.socket, 'free');
} finally {
agent.destroy();
server.close();
}
});

describe('"https" module', () => {
it('should work for basic HTTPS requests', async () => {
let gotReq = false;
Expand Down

0 comments on commit 2d90753

Please sign in to comment.