[6a3a178] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | const OriginalAgent = require('http').Agent;
|
---|
| 4 | const ms = require('humanize-ms');
|
---|
| 5 | const debug = require('debug')('agentkeepalive');
|
---|
| 6 | const deprecate = require('depd')('agentkeepalive');
|
---|
| 7 | const {
|
---|
| 8 | INIT_SOCKET,
|
---|
| 9 | CURRENT_ID,
|
---|
| 10 | CREATE_ID,
|
---|
| 11 | SOCKET_CREATED_TIME,
|
---|
| 12 | SOCKET_NAME,
|
---|
| 13 | SOCKET_REQUEST_COUNT,
|
---|
| 14 | SOCKET_REQUEST_FINISHED_COUNT,
|
---|
| 15 | } = require('./constants');
|
---|
| 16 |
|
---|
| 17 | // OriginalAgent come from
|
---|
| 18 | // - https://github.com/nodejs/node/blob/v8.12.0/lib/_http_agent.js
|
---|
| 19 | // - https://github.com/nodejs/node/blob/v10.12.0/lib/_http_agent.js
|
---|
| 20 |
|
---|
| 21 | // node <= 10
|
---|
| 22 | let defaultTimeoutListenerCount = 1;
|
---|
| 23 | const majorVersion = parseInt(process.version.split('.', 1)[0].substring(1));
|
---|
| 24 | if (majorVersion >= 11 && majorVersion <= 12) {
|
---|
| 25 | defaultTimeoutListenerCount = 2;
|
---|
| 26 | } else if (majorVersion >= 13) {
|
---|
| 27 | defaultTimeoutListenerCount = 3;
|
---|
| 28 | }
|
---|
| 29 |
|
---|
| 30 | class Agent extends OriginalAgent {
|
---|
| 31 | constructor(options) {
|
---|
| 32 | options = options || {};
|
---|
| 33 | options.keepAlive = options.keepAlive !== false;
|
---|
| 34 | // default is keep-alive and 15s free socket timeout
|
---|
| 35 | if (options.freeSocketTimeout === undefined) {
|
---|
| 36 | options.freeSocketTimeout = 15000;
|
---|
| 37 | }
|
---|
| 38 | // Legacy API: keepAliveTimeout should be rename to `freeSocketTimeout`
|
---|
| 39 | if (options.keepAliveTimeout) {
|
---|
| 40 | deprecate('options.keepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
|
---|
| 41 | options.freeSocketTimeout = options.keepAliveTimeout;
|
---|
| 42 | delete options.keepAliveTimeout;
|
---|
| 43 | }
|
---|
| 44 | // Legacy API: freeSocketKeepAliveTimeout should be rename to `freeSocketTimeout`
|
---|
| 45 | if (options.freeSocketKeepAliveTimeout) {
|
---|
| 46 | deprecate('options.freeSocketKeepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
|
---|
| 47 | options.freeSocketTimeout = options.freeSocketKeepAliveTimeout;
|
---|
| 48 | delete options.freeSocketKeepAliveTimeout;
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | // Sets the socket to timeout after timeout milliseconds of inactivity on the socket.
|
---|
| 52 | // By default is double free socket timeout.
|
---|
| 53 | if (options.timeout === undefined) {
|
---|
| 54 | // make sure socket default inactivity timeout >= 30s
|
---|
| 55 | options.timeout = Math.max(options.freeSocketTimeout * 2, 30000);
|
---|
| 56 | }
|
---|
| 57 |
|
---|
| 58 | // support humanize format
|
---|
| 59 | options.timeout = ms(options.timeout);
|
---|
| 60 | options.freeSocketTimeout = ms(options.freeSocketTimeout);
|
---|
| 61 | options.socketActiveTTL = options.socketActiveTTL ? ms(options.socketActiveTTL) : 0;
|
---|
| 62 |
|
---|
| 63 | super(options);
|
---|
| 64 |
|
---|
| 65 | this[CURRENT_ID] = 0;
|
---|
| 66 |
|
---|
| 67 | // create socket success counter
|
---|
| 68 | this.createSocketCount = 0;
|
---|
| 69 | this.createSocketCountLastCheck = 0;
|
---|
| 70 |
|
---|
| 71 | this.createSocketErrorCount = 0;
|
---|
| 72 | this.createSocketErrorCountLastCheck = 0;
|
---|
| 73 |
|
---|
| 74 | this.closeSocketCount = 0;
|
---|
| 75 | this.closeSocketCountLastCheck = 0;
|
---|
| 76 |
|
---|
| 77 | // socket error event count
|
---|
| 78 | this.errorSocketCount = 0;
|
---|
| 79 | this.errorSocketCountLastCheck = 0;
|
---|
| 80 |
|
---|
| 81 | // request finished counter
|
---|
| 82 | this.requestCount = 0;
|
---|
| 83 | this.requestCountLastCheck = 0;
|
---|
| 84 |
|
---|
| 85 | // including free socket timeout counter
|
---|
| 86 | this.timeoutSocketCount = 0;
|
---|
| 87 | this.timeoutSocketCountLastCheck = 0;
|
---|
| 88 |
|
---|
| 89 | this.on('free', socket => {
|
---|
| 90 | // https://github.com/nodejs/node/pull/32000
|
---|
| 91 | // Node.js native agent will check socket timeout eqs agent.options.timeout.
|
---|
| 92 | // Use the ttl or freeSocketTimeout to overwrite.
|
---|
| 93 | const timeout = this.calcSocketTimeout(socket);
|
---|
| 94 | if (timeout > 0 && socket.timeout !== timeout) {
|
---|
| 95 | socket.setTimeout(timeout);
|
---|
| 96 | }
|
---|
| 97 | });
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | get freeSocketKeepAliveTimeout() {
|
---|
| 101 | deprecate('agent.freeSocketKeepAliveTimeout is deprecated, please use agent.options.freeSocketTimeout instead');
|
---|
| 102 | return this.options.freeSocketTimeout;
|
---|
| 103 | }
|
---|
| 104 |
|
---|
| 105 | get timeout() {
|
---|
| 106 | deprecate('agent.timeout is deprecated, please use agent.options.timeout instead');
|
---|
| 107 | return this.options.timeout;
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | get socketActiveTTL() {
|
---|
| 111 | deprecate('agent.socketActiveTTL is deprecated, please use agent.options.socketActiveTTL instead');
|
---|
| 112 | return this.options.socketActiveTTL;
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | calcSocketTimeout(socket) {
|
---|
| 116 | /**
|
---|
| 117 | * return <= 0: should free socket
|
---|
| 118 | * return > 0: should update socket timeout
|
---|
| 119 | * return undefined: not find custom timeout
|
---|
| 120 | */
|
---|
| 121 | let freeSocketTimeout = this.options.freeSocketTimeout;
|
---|
| 122 | const socketActiveTTL = this.options.socketActiveTTL;
|
---|
| 123 | if (socketActiveTTL) {
|
---|
| 124 | // check socketActiveTTL
|
---|
| 125 | const aliveTime = Date.now() - socket[SOCKET_CREATED_TIME];
|
---|
| 126 | const diff = socketActiveTTL - aliveTime;
|
---|
| 127 | if (diff <= 0) {
|
---|
| 128 | return diff;
|
---|
| 129 | }
|
---|
| 130 | if (freeSocketTimeout && diff < freeSocketTimeout) {
|
---|
| 131 | freeSocketTimeout = diff;
|
---|
| 132 | }
|
---|
| 133 | }
|
---|
| 134 | // set freeSocketTimeout
|
---|
| 135 | if (freeSocketTimeout) {
|
---|
| 136 | // set free keepalive timer
|
---|
| 137 | // try to use socket custom freeSocketTimeout first, support headers['keep-alive']
|
---|
| 138 | // https://github.com/node-modules/urllib/blob/b76053020923f4d99a1c93cf2e16e0c5ba10bacf/lib/urllib.js#L498
|
---|
| 139 | const customFreeSocketTimeout = socket.freeSocketTimeout || socket.freeSocketKeepAliveTimeout;
|
---|
| 140 | return customFreeSocketTimeout || freeSocketTimeout;
|
---|
| 141 | }
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | keepSocketAlive(socket) {
|
---|
| 145 | const result = super.keepSocketAlive(socket);
|
---|
| 146 | // should not keepAlive, do nothing
|
---|
| 147 | if (!result) return result;
|
---|
| 148 |
|
---|
| 149 | const customTimeout = this.calcSocketTimeout(socket);
|
---|
| 150 | if (typeof customTimeout === 'undefined') {
|
---|
| 151 | return true;
|
---|
| 152 | }
|
---|
| 153 | if (customTimeout <= 0) {
|
---|
| 154 | debug('%s(requests: %s, finished: %s) free but need to destroy by TTL, request count %s, diff is %s',
|
---|
| 155 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], customTimeout);
|
---|
| 156 | return false;
|
---|
| 157 | }
|
---|
| 158 | if (socket.timeout !== customTimeout) {
|
---|
| 159 | socket.setTimeout(customTimeout);
|
---|
| 160 | }
|
---|
| 161 | return true;
|
---|
| 162 | }
|
---|
| 163 |
|
---|
| 164 | // only call on addRequest
|
---|
| 165 | reuseSocket(...args) {
|
---|
| 166 | // reuseSocket(socket, req)
|
---|
| 167 | super.reuseSocket(...args);
|
---|
| 168 | const socket = args[0];
|
---|
| 169 | const req = args[1];
|
---|
| 170 | req.reusedSocket = true;
|
---|
| 171 | const agentTimeout = this.options.timeout;
|
---|
| 172 | if (getSocketTimeout(socket) !== agentTimeout) {
|
---|
| 173 | // reset timeout before use
|
---|
| 174 | socket.setTimeout(agentTimeout);
|
---|
| 175 | debug('%s reset timeout to %sms', socket[SOCKET_NAME], agentTimeout);
|
---|
| 176 | }
|
---|
| 177 | socket[SOCKET_REQUEST_COUNT]++;
|
---|
| 178 | debug('%s(requests: %s, finished: %s) reuse on addRequest, timeout %sms',
|
---|
| 179 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
| 180 | getSocketTimeout(socket));
|
---|
| 181 | }
|
---|
| 182 |
|
---|
| 183 | [CREATE_ID]() {
|
---|
| 184 | const id = this[CURRENT_ID]++;
|
---|
| 185 | if (this[CURRENT_ID] === Number.MAX_SAFE_INTEGER) this[CURRENT_ID] = 0;
|
---|
| 186 | return id;
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | [INIT_SOCKET](socket, options) {
|
---|
| 190 | // bugfix here.
|
---|
| 191 | // https on node 8, 10 won't set agent.options.timeout by default
|
---|
| 192 | // TODO: need to fix on node itself
|
---|
| 193 | if (options.timeout) {
|
---|
| 194 | const timeout = getSocketTimeout(socket);
|
---|
| 195 | if (!timeout) {
|
---|
| 196 | socket.setTimeout(options.timeout);
|
---|
| 197 | }
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | if (this.options.keepAlive) {
|
---|
| 201 | // Disable Nagle's algorithm: http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
|
---|
| 202 | // https://fengmk2.com/benchmark/nagle-algorithm-delayed-ack-mock.html
|
---|
| 203 | socket.setNoDelay(true);
|
---|
| 204 | }
|
---|
| 205 | this.createSocketCount++;
|
---|
| 206 | if (this.options.socketActiveTTL) {
|
---|
| 207 | socket[SOCKET_CREATED_TIME] = Date.now();
|
---|
| 208 | }
|
---|
| 209 | // don't show the hole '-----BEGIN CERTIFICATE----' key string
|
---|
| 210 | socket[SOCKET_NAME] = `sock[${this[CREATE_ID]()}#${options._agentKey}]`.split('-----BEGIN', 1)[0];
|
---|
| 211 | socket[SOCKET_REQUEST_COUNT] = 1;
|
---|
| 212 | socket[SOCKET_REQUEST_FINISHED_COUNT] = 0;
|
---|
| 213 | installListeners(this, socket, options);
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | createConnection(options, oncreate) {
|
---|
| 217 | let called = false;
|
---|
| 218 | const onNewCreate = (err, socket) => {
|
---|
| 219 | if (called) return;
|
---|
| 220 | called = true;
|
---|
| 221 |
|
---|
| 222 | if (err) {
|
---|
| 223 | this.createSocketErrorCount++;
|
---|
| 224 | return oncreate(err);
|
---|
| 225 | }
|
---|
| 226 | this[INIT_SOCKET](socket, options);
|
---|
| 227 | oncreate(err, socket);
|
---|
| 228 | };
|
---|
| 229 |
|
---|
| 230 | const newSocket = super.createConnection(options, onNewCreate);
|
---|
| 231 | if (newSocket) onNewCreate(null, newSocket);
|
---|
| 232 | }
|
---|
| 233 |
|
---|
| 234 | get statusChanged() {
|
---|
| 235 | const changed = this.createSocketCount !== this.createSocketCountLastCheck ||
|
---|
| 236 | this.createSocketErrorCount !== this.createSocketErrorCountLastCheck ||
|
---|
| 237 | this.closeSocketCount !== this.closeSocketCountLastCheck ||
|
---|
| 238 | this.errorSocketCount !== this.errorSocketCountLastCheck ||
|
---|
| 239 | this.timeoutSocketCount !== this.timeoutSocketCountLastCheck ||
|
---|
| 240 | this.requestCount !== this.requestCountLastCheck;
|
---|
| 241 | if (changed) {
|
---|
| 242 | this.createSocketCountLastCheck = this.createSocketCount;
|
---|
| 243 | this.createSocketErrorCountLastCheck = this.createSocketErrorCount;
|
---|
| 244 | this.closeSocketCountLastCheck = this.closeSocketCount;
|
---|
| 245 | this.errorSocketCountLastCheck = this.errorSocketCount;
|
---|
| 246 | this.timeoutSocketCountLastCheck = this.timeoutSocketCount;
|
---|
| 247 | this.requestCountLastCheck = this.requestCount;
|
---|
| 248 | }
|
---|
| 249 | return changed;
|
---|
| 250 | }
|
---|
| 251 |
|
---|
| 252 | getCurrentStatus() {
|
---|
| 253 | return {
|
---|
| 254 | createSocketCount: this.createSocketCount,
|
---|
| 255 | createSocketErrorCount: this.createSocketErrorCount,
|
---|
| 256 | closeSocketCount: this.closeSocketCount,
|
---|
| 257 | errorSocketCount: this.errorSocketCount,
|
---|
| 258 | timeoutSocketCount: this.timeoutSocketCount,
|
---|
| 259 | requestCount: this.requestCount,
|
---|
| 260 | freeSockets: inspect(this.freeSockets),
|
---|
| 261 | sockets: inspect(this.sockets),
|
---|
| 262 | requests: inspect(this.requests),
|
---|
| 263 | };
|
---|
| 264 | }
|
---|
| 265 | }
|
---|
| 266 |
|
---|
| 267 | // node 8 don't has timeout attribute on socket
|
---|
| 268 | // https://github.com/nodejs/node/pull/21204/files#diff-e6ef024c3775d787c38487a6309e491dR408
|
---|
| 269 | function getSocketTimeout(socket) {
|
---|
| 270 | return socket.timeout || socket._idleTimeout;
|
---|
| 271 | }
|
---|
| 272 |
|
---|
| 273 | function installListeners(agent, socket, options) {
|
---|
| 274 | debug('%s create, timeout %sms', socket[SOCKET_NAME], getSocketTimeout(socket));
|
---|
| 275 |
|
---|
| 276 | // listener socket events: close, timeout, error, free
|
---|
| 277 | function onFree() {
|
---|
| 278 | // create and socket.emit('free') logic
|
---|
| 279 | // https://github.com/nodejs/node/blob/master/lib/_http_agent.js#L311
|
---|
| 280 | // no req on the socket, it should be the new socket
|
---|
| 281 | if (!socket._httpMessage && socket[SOCKET_REQUEST_COUNT] === 1) return;
|
---|
| 282 |
|
---|
| 283 | socket[SOCKET_REQUEST_FINISHED_COUNT]++;
|
---|
| 284 | agent.requestCount++;
|
---|
| 285 | debug('%s(requests: %s, finished: %s) free',
|
---|
| 286 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
| 287 |
|
---|
| 288 | // should reuse on pedding requests?
|
---|
| 289 | const name = agent.getName(options);
|
---|
| 290 | if (socket.writable && agent.requests[name] && agent.requests[name].length) {
|
---|
| 291 | // will be reuse on agent free listener
|
---|
| 292 | socket[SOCKET_REQUEST_COUNT]++;
|
---|
| 293 | debug('%s(requests: %s, finished: %s) will be reuse on agent free event',
|
---|
| 294 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
| 295 | }
|
---|
| 296 | }
|
---|
| 297 | socket.on('free', onFree);
|
---|
| 298 |
|
---|
| 299 | function onClose(isError) {
|
---|
| 300 | debug('%s(requests: %s, finished: %s) close, isError: %s',
|
---|
| 301 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], isError);
|
---|
| 302 | agent.closeSocketCount++;
|
---|
| 303 | }
|
---|
| 304 | socket.on('close', onClose);
|
---|
| 305 |
|
---|
| 306 | // start socket timeout handler
|
---|
| 307 | function onTimeout() {
|
---|
| 308 | // onTimeout and emitRequestTimeout(_http_client.js)
|
---|
| 309 | // https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L711
|
---|
| 310 | const listenerCount = socket.listeners('timeout').length;
|
---|
| 311 | // node <= 10, default listenerCount is 1, onTimeout
|
---|
| 312 | // 11 < node <= 12, default listenerCount is 2, onTimeout and emitRequestTimeout
|
---|
| 313 | // node >= 13, default listenerCount is 3, onTimeout,
|
---|
| 314 | // onTimeout(https://github.com/nodejs/node/pull/32000/files#diff-5f7fb0850412c6be189faeddea6c5359R333)
|
---|
| 315 | // and emitRequestTimeout
|
---|
| 316 | const timeout = getSocketTimeout(socket);
|
---|
| 317 | const req = socket._httpMessage;
|
---|
| 318 | const reqTimeoutListenerCount = req && req.listeners('timeout').length || 0;
|
---|
| 319 | debug('%s(requests: %s, finished: %s) timeout after %sms, listeners %s, defaultTimeoutListenerCount %s, hasHttpRequest %s, HttpRequest timeoutListenerCount %s',
|
---|
| 320 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
| 321 | timeout, listenerCount, defaultTimeoutListenerCount, !!req, reqTimeoutListenerCount);
|
---|
| 322 | if (debug.enabled) {
|
---|
| 323 | debug('timeout listeners: %s', socket.listeners('timeout').map(f => f.name).join(', '));
|
---|
| 324 | }
|
---|
| 325 | agent.timeoutSocketCount++;
|
---|
| 326 | const name = agent.getName(options);
|
---|
| 327 | if (agent.freeSockets[name] && agent.freeSockets[name].indexOf(socket) !== -1) {
|
---|
| 328 | // free socket timeout, destroy quietly
|
---|
| 329 | socket.destroy();
|
---|
| 330 | // Remove it from freeSockets list immediately to prevent new requests
|
---|
| 331 | // from being sent through this socket.
|
---|
| 332 | agent.removeSocket(socket, options);
|
---|
| 333 | debug('%s is free, destroy quietly', socket[SOCKET_NAME]);
|
---|
| 334 | } else {
|
---|
| 335 | // if there is no any request socket timeout handler,
|
---|
| 336 | // agent need to handle socket timeout itself.
|
---|
| 337 | //
|
---|
| 338 | // custom request socket timeout handle logic must follow these rules:
|
---|
| 339 | // 1. Destroy socket first
|
---|
| 340 | // 2. Must emit socket 'agentRemove' event tell agent remove socket
|
---|
| 341 | // from freeSockets list immediately.
|
---|
| 342 | // Otherise you may be get 'socket hang up' error when reuse
|
---|
| 343 | // free socket and timeout happen in the same time.
|
---|
| 344 | if (reqTimeoutListenerCount === 0) {
|
---|
| 345 | const error = new Error('Socket timeout');
|
---|
| 346 | error.code = 'ERR_SOCKET_TIMEOUT';
|
---|
| 347 | error.timeout = timeout;
|
---|
| 348 | // must manually call socket.end() or socket.destroy() to end the connection.
|
---|
| 349 | // https://nodejs.org/dist/latest-v10.x/docs/api/net.html#net_socket_settimeout_timeout_callback
|
---|
| 350 | socket.destroy(error);
|
---|
| 351 | agent.removeSocket(socket, options);
|
---|
| 352 | debug('%s destroy with timeout error', socket[SOCKET_NAME]);
|
---|
| 353 | }
|
---|
| 354 | }
|
---|
| 355 | }
|
---|
| 356 | socket.on('timeout', onTimeout);
|
---|
| 357 |
|
---|
| 358 | function onError(err) {
|
---|
| 359 | const listenerCount = socket.listeners('error').length;
|
---|
| 360 | debug('%s(requests: %s, finished: %s) error: %s, listenerCount: %s',
|
---|
| 361 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
| 362 | err, listenerCount);
|
---|
| 363 | agent.errorSocketCount++;
|
---|
| 364 | if (listenerCount === 1) {
|
---|
| 365 | // if socket don't contain error event handler, don't catch it, emit it again
|
---|
| 366 | debug('%s emit uncaught error event', socket[SOCKET_NAME]);
|
---|
| 367 | socket.removeListener('error', onError);
|
---|
| 368 | socket.emit('error', err);
|
---|
| 369 | }
|
---|
| 370 | }
|
---|
| 371 | socket.on('error', onError);
|
---|
| 372 |
|
---|
| 373 | function onRemove() {
|
---|
| 374 | debug('%s(requests: %s, finished: %s) agentRemove',
|
---|
| 375 | socket[SOCKET_NAME],
|
---|
| 376 | socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
| 377 | // We need this function for cases like HTTP 'upgrade'
|
---|
| 378 | // (defined by WebSockets) where we need to remove a socket from the
|
---|
| 379 | // pool because it'll be locked up indefinitely
|
---|
| 380 | socket.removeListener('close', onClose);
|
---|
| 381 | socket.removeListener('error', onError);
|
---|
| 382 | socket.removeListener('free', onFree);
|
---|
| 383 | socket.removeListener('timeout', onTimeout);
|
---|
| 384 | socket.removeListener('agentRemove', onRemove);
|
---|
| 385 | }
|
---|
| 386 | socket.on('agentRemove', onRemove);
|
---|
| 387 | }
|
---|
| 388 |
|
---|
| 389 | module.exports = Agent;
|
---|
| 390 |
|
---|
| 391 | function inspect(obj) {
|
---|
| 392 | const res = {};
|
---|
| 393 | for (const key in obj) {
|
---|
| 394 | res[key] = obj[key].length;
|
---|
| 395 | }
|
---|
| 396 | return res;
|
---|
| 397 | }
|
---|