@@ -17,6 +17,7 @@ export { NodeHttpHandlerOptions };
1717interface ResolvedNodeHttpHandlerConfig {
1818 requestTimeout ?: number ;
1919 connectionTimeout ?: number ;
20+ socketAcquisitionWarningTimeout ?: number ;
2021 httpAgent : hAgent ;
2122 httpsAgent : hsAgent ;
2223}
@@ -26,6 +27,7 @@ export const DEFAULT_REQUEST_TIMEOUT = 0;
2627export class NodeHttpHandler implements HttpHandler < NodeHttpHandlerOptions > {
2728 private config ?: ResolvedNodeHttpHandlerConfig ;
2829 private configProvider : Promise < ResolvedNodeHttpHandlerConfig > ;
30+ private socketWarningTimestamp = 0 ;
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,55 @@ 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+ 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+
4899 constructor ( options ?: NodeHttpHandlerOptions | Provider < NodeHttpHandlerOptions | void > ) {
49100 this . configProvider = new Promise ( ( resolve , reject ) => {
50101 if ( typeof options === "function" ) {
@@ -81,10 +132,15 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
81132 if ( ! this . config ) {
82133 this . config = await this . configProvider ;
83134 }
135+
136+ let socketCheckTimeoutId : NodeJS . Timeout ;
137+
84138 return new Promise ( ( _resolve , _reject ) => {
85139 let writeRequestBodyPromise : Promise < void > | undefined = undefined ;
86140 const resolve = async ( arg : { response : HttpResponse } ) => {
87141 await writeRequestBodyPromise ;
142+ // if requests are still resolving, cancel the socket usage check.
143+ clearTimeout ( socketCheckTimeoutId ) ;
88144 _resolve ( arg ) ;
89145 } ;
90146 const reject = async ( arg : unknown ) => {
@@ -106,6 +162,14 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
106162
107163 // determine which http(s) client to use
108164 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+
109173 const queryString = buildQueryString ( request . query || { } ) ;
110174 let auth = undefined ;
111175 if ( request . username != null || request . password != null ) {
@@ -126,7 +190,7 @@ export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
126190 method : request . method ,
127191 path,
128192 port : request . port ,
129- agent : isSSL ? this . config . httpsAgent : this . config . httpAgent ,
193+ agent,
130194 auth,
131195 } ;
132196
0 commit comments