Skip to content

Commit 0c8b150

Browse files
committed
feat: add checked socket exhaustion warning when throughput is slow
1 parent 74d1aa5 commit 0c8b150

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

.changeset/strong-taxis-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/node-http-handler": minor
3+
---
4+
5+
add socket exhaustion checked warning when throughput is slow

packages/node-http-handler/src/node-http-handler.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,37 @@ describe("NodeHttpHandler", () => {
704704
expect(nodeHttpHandler.httpHandlerConfigs()).toEqual({});
705705
});
706706
});
707+
708+
describe("checkSocketUsage", () => {
709+
beforeEach(() => {
710+
jest.spyOn(console, "warn").mockImplementation(jest.fn());
711+
});
712+
713+
afterEach(() => {
714+
jest.resetAllMocks();
715+
});
716+
717+
it("warns when socket exhaustion is detected", async () => {
718+
const lastTimestamp = Date.now() - 30_000;
719+
const warningTimestamp = NodeHttpHandler.checkSocketUsage(
720+
{
721+
maxSockets: 2,
722+
sockets: {
723+
addr: [null, null],
724+
},
725+
requests: {
726+
addr: [null, null, null, null],
727+
},
728+
} as any,
729+
lastTimestamp
730+
);
731+
732+
expect(warningTimestamp).toBeGreaterThan(lastTimestamp);
733+
expect(console.warn).toHaveBeenCalledWith(
734+
"@smithy/node-http-handler:WARN",
735+
"socket usage at capacity=2 and 4 additional requests are enqueued.",
736+
"See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html"
737+
);
738+
});
739+
});
707740
});

packages/node-http-handler/src/node-http-handler.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const DEFAULT_REQUEST_TIMEOUT = 0;
2626
export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
2727
private config?: ResolvedNodeHttpHandlerConfig;
2828
private configProvider: Promise<ResolvedNodeHttpHandlerConfig>;
29+
private socketWarningTimestamp = 0;
30+
private socketCheckTimeoutId = (null as unknown) as NodeJS.Timeout;
2931

3032
// Node http handler is hard-coded to http/1.1: https://github.com/nodejs/node/blob/ff5664b83b89c55e4ab5d5f60068fb457f1f5872/lib/_http_server.js#L286
3133
public readonly metadata = { handlerProtocol: "http/1.1" };
@@ -45,6 +47,53 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
4547
return new NodeHttpHandler(instanceOrOptions as NodeHttpHandlerOptions);
4648
}
4749

50+
/**
51+
* @internal
52+
*
53+
* @param agent - http(s) agent in use by the NodeHttpHandler instance.
54+
* @returns timestamp of last emitted warning.
55+
*/
56+
public static checkSocketUsage(agent: hAgent | hsAgent, socketWarningTimestamp: number): number {
57+
// note, maxSockets is per origin.
58+
const { sockets, requests, maxSockets } = agent;
59+
60+
if (typeof maxSockets !== "number" || maxSockets === Infinity) {
61+
return socketWarningTimestamp;
62+
}
63+
64+
const interval = 15_000;
65+
if (Date.now() - interval < socketWarningTimestamp) {
66+
return socketWarningTimestamp;
67+
}
68+
69+
let socketsInUse = 0;
70+
let requestsEnqueued = 0;
71+
72+
if (sockets) {
73+
for (const key in sockets) {
74+
socketsInUse = Math.max(socketsInUse, sockets[key]?.length ?? 0);
75+
}
76+
}
77+
78+
if (requests) {
79+
for (const key in requests) {
80+
requestsEnqueued = Math.max(requestsEnqueued, requests[key]?.length ?? 0);
81+
}
82+
}
83+
84+
// This threshold is somewhat arbitrary.
85+
// A few enqueued requests is not worth warning about.
86+
if (socketsInUse >= maxSockets && requestsEnqueued >= 2 * maxSockets) {
87+
console.warn(
88+
"@smithy/node-http-handler:WARN",
89+
`socket usage at capacity=${socketsInUse} and ${requestsEnqueued} additional requests are enqueued.`,
90+
"See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html"
91+
);
92+
return Date.now();
93+
}
94+
return socketWarningTimestamp;
95+
}
96+
4897
constructor(options?: NodeHttpHandlerOptions | Provider<NodeHttpHandlerOptions | void>) {
4998
this.configProvider = new Promise((resolve, reject) => {
5099
if (typeof options === "function") {
@@ -81,10 +130,13 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
81130
if (!this.config) {
82131
this.config = await this.configProvider;
83132
}
133+
84134
return new Promise((_resolve, _reject) => {
85135
let writeRequestBodyPromise: Promise<void> | undefined = undefined;
86136
const resolve = async (arg: { response: HttpResponse }) => {
87137
await writeRequestBodyPromise;
138+
// if requests are still resolving, cancel the socket usage check.
139+
clearTimeout(this.socketCheckTimeoutId);
88140
_resolve(arg);
89141
};
90142
const reject = async (arg: unknown) => {
@@ -106,6 +158,14 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
106158

107159
// determine which http(s) client to use
108160
const isSSL = request.protocol === "https:";
161+
const agent = isSSL ? this.config.httpsAgent : this.config.httpAgent;
162+
163+
// If the request is taking a long time, check socket usage and potentially warn.
164+
// This warning will be cancelled if the request resolves.
165+
this.socketCheckTimeoutId = setTimeout(() => {
166+
this.socketWarningTimestamp = NodeHttpHandler.checkSocketUsage(agent, this.socketWarningTimestamp);
167+
}, (this.config.requestTimeout ?? 2000) + (this.config.connectionTimeout ?? 1000));
168+
109169
const queryString = buildQueryString(request.query || {});
110170
let auth = undefined;
111171
if (request.username != null || request.password != null) {
@@ -126,7 +186,7 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
126186
method: request.method,
127187
path,
128188
port: request.port,
129-
agent: isSSL ? this.config.httpsAgent : this.config.httpAgent,
189+
agent,
130190
auth,
131191
};
132192

0 commit comments

Comments
 (0)