From 456a81982e6b08f3ab5e4052459c3ab6640e6b0a Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 19 Dec 2022 10:22:25 -0800 Subject: [PATCH] fix: reduce Connection keep-alive timeout to 1 second fewer than the Solana RPC's keep-alive timeout (#29130) * Delete `AgentManager` * Replace custom `http.Agent` implementation with `agentkeepalive` package * Set the default free socket timeout to 1s less than the Solana RPC's default timeout * Add link to particular issue comment * Create the correct flavor of default agent for http/https --- web3.js/package.json | 1 + web3.js/src/agent-manager.ts | 44 ------------------------------ web3.js/src/connection.ts | 40 +++++++++++++++------------ web3.js/test/agent-manager.test.ts | 40 --------------------------- web3.js/yarn.lock | 11 +++++++- 5 files changed, 34 insertions(+), 102 deletions(-) delete mode 100644 web3.js/src/agent-manager.ts delete mode 100644 web3.js/test/agent-manager.test.ts diff --git a/web3.js/package.json b/web3.js/package.json index 9e19cd424c..193428bb05 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -62,6 +62,7 @@ "@noble/hashes": "^1.1.2", "@noble/secp256k1": "^1.6.3", "@solana/buffer-layout": "^4.0.0", + "agentkeepalive": "^4.2.1", "bigint-buffer": "^1.1.5", "bn.js": "^5.0.0", "borsh": "^0.7.0", diff --git a/web3.js/src/agent-manager.ts b/web3.js/src/agent-manager.ts deleted file mode 100644 index 0a99f8640e..0000000000 --- a/web3.js/src/agent-manager.ts +++ /dev/null @@ -1,44 +0,0 @@ -import http from 'http'; -import https from 'https'; - -export const DESTROY_TIMEOUT_MS = 5000; - -export class AgentManager { - _agent: http.Agent | https.Agent; - _activeRequests = 0; - _destroyTimeout: ReturnType | null = null; - _useHttps: boolean; - - static _newAgent(useHttps: boolean): http.Agent | https.Agent { - const options = {keepAlive: true, maxSockets: 25}; - if (useHttps) { - return new https.Agent(options); - } else { - return new http.Agent(options); - } - } - - constructor(useHttps?: boolean) { - this._useHttps = useHttps === true; - this._agent = AgentManager._newAgent(this._useHttps); - } - - requestStart(): http.Agent | https.Agent { - this._activeRequests++; - if (this._destroyTimeout !== null) { - clearTimeout(this._destroyTimeout); - this._destroyTimeout = null; - } - return this._agent; - } - - requestEnd() { - this._activeRequests--; - if (this._activeRequests === 0 && this._destroyTimeout === null) { - this._destroyTimeout = setTimeout(() => { - this._agent.destroy(); - this._agent = AgentManager._newAgent(this._useHttps); - }, DESTROY_TIMEOUT_MS); - } - } -} diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 162a9374f3..c6b88e1ad3 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1,9 +1,12 @@ +import HttpKeepAliveAgent, { + HttpsAgent as HttpsKeepAliveAgent, +} from 'agentkeepalive'; import bs58 from 'bs58'; import {Buffer} from 'buffer'; // @ts-ignore import fastStableStringify from 'fast-stable-stringify'; -import type {Agent as HttpAgent} from 'http'; -import {Agent as HttpsAgent} from 'https'; +import type {Agent as NodeHttpAgent} from 'http'; +import {Agent as NodeHttpsAgent} from 'https'; import { type as pick, number, @@ -27,7 +30,6 @@ import {Client as RpcWebSocketClient} from 'rpc-websockets'; import RpcClient from 'jayson/lib/client/browser'; import {JSONRPCError} from 'jayson'; -import {AgentManager} from './agent-manager'; import {EpochSchedule} from './epoch-schedule'; import {SendTransactionError, SolanaJSONRPCError} from './errors'; import fetchImpl, {Response} from './fetch-impl'; @@ -1452,12 +1454,10 @@ function createRpcClient( customFetch?: FetchFn, fetchMiddleware?: FetchMiddleware, disableRetryOnRateLimit?: boolean, - httpAgent?: HttpAgent | HttpsAgent | false, + httpAgent?: NodeHttpAgent | NodeHttpsAgent | false, ): RpcClient { const fetch = customFetch ? customFetch : fetchImpl; - let agentManager: - | {requestEnd(): void; requestStart(): HttpAgent | HttpsAgent} - | undefined; + let agent: NodeHttpAgent | NodeHttpsAgent | undefined; if (process.env.BROWSER) { if (httpAgent != null) { console.warn( @@ -1468,21 +1468,30 @@ function createRpcClient( } else { if (httpAgent == null) { if (process.env.NODE_ENV !== 'test') { - agentManager = new AgentManager( - url.startsWith('https:') /* useHttps */, - ); + const agentOptions = { + // One second fewer than the Solana RPC's keepalive timeout. + // Read more: https://github.com/solana-labs/solana/issues/27859#issuecomment-1340097889 + freeSocketTimeout: 19000, + keepAlive: true, + maxSockets: 25, + }; + if (url.startsWith('https:')) { + agent = new HttpsKeepAliveAgent(agentOptions); + } else { + agent = new HttpKeepAliveAgent(agentOptions); + } } } else { if (httpAgent !== false) { const isHttps = url.startsWith('https:'); - if (isHttps && !(httpAgent instanceof HttpsAgent)) { + if (isHttps && !(httpAgent instanceof NodeHttpsAgent)) { throw new Error( 'The endpoint `' + url + '` can only be paired with an `https.Agent`. You have, instead, supplied an ' + '`http.Agent` through `httpAgent`.', ); - } else if (!isHttps && httpAgent instanceof HttpsAgent) { + } else if (!isHttps && httpAgent instanceof NodeHttpsAgent) { throw new Error( 'The endpoint `' + url + @@ -1490,7 +1499,7 @@ function createRpcClient( '`https.Agent` through `httpAgent`.', ); } - agentManager = {requestEnd() {}, requestStart: () => httpAgent}; + agent = httpAgent; } } } @@ -1515,7 +1524,6 @@ function createRpcClient( } const clientBrowser = new RpcClient(async (request, callback) => { - const agent = agentManager ? agentManager.requestStart() : undefined; const options = { method: 'POST', body: request, @@ -1565,8 +1573,6 @@ function createRpcClient( } } catch (err) { if (err instanceof Error) callback(err); - } finally { - agentManager && agentManager.requestEnd(); } }, {}); @@ -2898,7 +2904,7 @@ export type ConnectionConfig = { * persistence). Set this to `false` to create a connection that uses no agent. This applies to * Node environments only. */ - httpAgent?: HttpAgent | HttpsAgent | false; + httpAgent?: NodeHttpAgent | NodeHttpsAgent | false; /** Optional commitment level */ commitment?: Commitment; /** Optional endpoint URL to the fullnode JSON RPC PubSub WebSocket Endpoint */ diff --git a/web3.js/test/agent-manager.test.ts b/web3.js/test/agent-manager.test.ts deleted file mode 100644 index 929296a3be..0000000000 --- a/web3.js/test/agent-manager.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {expect} from 'chai'; - -import {AgentManager, DESTROY_TIMEOUT_MS} from '../src/agent-manager'; -import {sleep} from '../src/utils/sleep'; - -describe('AgentManager', () => { - it('works', async () => { - const manager = new AgentManager(); - const agent = manager._agent; - expect(manager._activeRequests).to.eq(0); - expect(manager._destroyTimeout).to.be.null; - - manager.requestStart(); - - expect(manager._activeRequests).to.eq(1); - expect(manager._destroyTimeout).to.be.null; - - manager.requestEnd(); - - expect(manager._activeRequests).to.eq(0); - expect(manager._destroyTimeout).not.to.be.null; - - manager.requestStart(); - manager.requestStart(); - - expect(manager._activeRequests).to.eq(2); - expect(manager._destroyTimeout).to.be.null; - - manager.requestEnd(); - manager.requestEnd(); - - expect(manager._activeRequests).to.eq(0); - expect(manager._destroyTimeout).not.to.be.null; - expect(manager._agent).to.eq(agent); - - await sleep(DESTROY_TIMEOUT_MS); - - expect(manager._agent).not.to.eq(agent); - }).timeout(2 * DESTROY_TIMEOUT_MS); -}); diff --git a/web3.js/yarn.lock b/web3.js/yarn.lock index d0c0b5c082..1a2beaf0dd 100644 --- a/web3.js/yarn.lock +++ b/web3.js/yarn.lock @@ -1834,7 +1834,7 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: dependencies: debug "4" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.1.3: version "4.2.1" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz" dependencies: @@ -1842,6 +1842,15 @@ agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: depd "^1.1.2" humanize-ms "^1.2.1" +agentkeepalive@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz"