@@ -17,6 +17,7 @@ export { NodeHttpHandlerOptions };
17
17
interface ResolvedNodeHttpHandlerConfig {
18
18
requestTimeout ?: number ;
19
19
connectionTimeout ?: number ;
20
+ socketAcquisitionWarningTimeout ?: number ;
20
21
httpAgent : hAgent ;
21
22
httpsAgent : hsAgent ;
22
23
}
@@ -26,6 +27,7 @@ export const DEFAULT_REQUEST_TIMEOUT = 0;
26
27
export class NodeHttpHandler implements HttpHandler < NodeHttpHandlerOptions > {
27
28
private config ?: ResolvedNodeHttpHandlerConfig ;
28
29
private configProvider : Promise < ResolvedNodeHttpHandlerConfig > ;
30
+ private socketWarningTimestamp = 0 ;
29
31
30
32
// Node http handler is hard-coded to http/1.1: https://github.com/nodejs/node/blob/ff5664b83b89c55e4ab5d5f60068fb457f1f5872/lib/_http_server.js#L286
31
33
public readonly metadata = { handlerProtocol : "http/1.1" } ;
@@ -45,6 +47,55 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
45
47
return new NodeHttpHandler ( instanceOrOptions as NodeHttpHandlerOptions ) ;
46
48
}
47
49
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
+ if ( sockets && requests ) {
70
+ for ( const origin in sockets ) {
71
+ const socketsInUse = sockets [ origin ] ?. length ?? 0 ;
72
+ const requestsEnqueued = requests [ origin ] ?. length ?? 0 ;
73
+
74
+ /**
75
+ * Running at maximum socket usage can be intentional and normal.
76
+ * That is why this warning emits at a delay which can be seen
77
+ * at the call site's setTimeout wrapper. The warning will be cancelled
78
+ * if the request finishes in a reasonable amount of time regardless
79
+ * of socket saturation.
80
+ *
81
+ * Additionally, when the warning is emitted, there is an interval
82
+ * lockout.
83
+ */
84
+ if ( socketsInUse >= maxSockets && requestsEnqueued >= 2 * maxSockets ) {
85
+ console . warn (
86
+ "@smithy/node-http-handler:WARN" ,
87
+ `socket usage at capacity=${ socketsInUse } and ${ requestsEnqueued } additional requests are enqueued.` ,
88
+ "See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html" ,
89
+ "or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler config."
90
+ ) ;
91
+ return Date . now ( ) ;
92
+ }
93
+ }
94
+ }
95
+
96
+ return socketWarningTimestamp ;
97
+ }
98
+
48
99
constructor ( options ?: NodeHttpHandlerOptions | Provider < NodeHttpHandlerOptions | void > ) {
49
100
this . configProvider = new Promise ( ( resolve , reject ) => {
50
101
if ( typeof options === "function" ) {
@@ -81,10 +132,15 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
81
132
if ( ! this . config ) {
82
133
this . config = await this . configProvider ;
83
134
}
135
+
136
+ let socketCheckTimeoutId : NodeJS . Timeout ;
137
+
84
138
return new Promise ( ( _resolve , _reject ) => {
85
139
let writeRequestBodyPromise : Promise < void > | undefined = undefined ;
86
140
const resolve = async ( arg : { response : HttpResponse } ) => {
87
141
await writeRequestBodyPromise ;
142
+ // if requests are still resolving, cancel the socket usage check.
143
+ clearTimeout ( socketCheckTimeoutId ) ;
88
144
_resolve ( arg ) ;
89
145
} ;
90
146
const reject = async ( arg : unknown ) => {
@@ -106,6 +162,14 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
106
162
107
163
// determine which http(s) client to use
108
164
const isSSL = request . protocol === "https:" ;
165
+ const agent = isSSL ? this . config . httpsAgent : this . config . httpAgent ;
166
+
167
+ // If the request is taking a long time, check socket usage and potentially warn.
168
+ // This warning will be cancelled if the request resolves.
169
+ socketCheckTimeoutId = setTimeout ( ( ) => {
170
+ this . socketWarningTimestamp = NodeHttpHandler . checkSocketUsage ( agent , this . socketWarningTimestamp ) ;
171
+ } , this . config . socketAcquisitionWarningTimeout ?? ( this . config . requestTimeout ?? 2000 ) + ( this . config . connectionTimeout ?? 1000 ) ) ;
172
+
109
173
const queryString = buildQueryString ( request . query || { } ) ;
110
174
let auth = undefined ;
111
175
if ( request . username != null || request . password != null ) {
@@ -126,7 +190,7 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
126
190
method : request . method ,
127
191
path,
128
192
port : request . port ,
129
- agent : isSSL ? this . config . httpsAgent : this . config . httpAgent ,
193
+ agent,
130
194
auth,
131
195
} ;
132
196
0 commit comments