From c18042fab4cb0d21974ada1d91d5c64ec1688672 Mon Sep 17 00:00:00 2001 From: steveluscher Date: Tue, 6 Dec 2022 21:31:07 +0000 Subject: [PATCH 1/5] Delete `AgentManager` --- web3.js/src/agent-manager.ts | 44 ------------------------------ web3.js/test/agent-manager.test.ts | 40 --------------------------- 2 files changed, 84 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/src/agent-manager.ts b/web3.js/src/agent-manager.ts deleted file mode 100644 index 0a99f8640e90e5..00000000000000 --- 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/test/agent-manager.test.ts b/web3.js/test/agent-manager.test.ts deleted file mode 100644 index 929296a3be3cc4..00000000000000 --- 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); -}); From 56a60618c02aff4339331e7645e7b72edb70523f Mon Sep 17 00:00:00 2001 From: steveluscher Date: Tue, 6 Dec 2022 21:38:10 +0000 Subject: [PATCH 2/5] Replace custom `http.Agent` implementation with `agentkeepalive` package --- web3.js/package.json | 1 + web3.js/src/connection.ts | 18 +++++++----------- web3.js/yarn.lock | 11 ++++++++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web3.js/package.json b/web3.js/package.json index 9e19cd424ca900..193428bb050b5c 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/connection.ts b/web3.js/src/connection.ts index 162a9374f3f670..a0b9bd7f28eb57 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1,3 +1,4 @@ +import Agent from 'agentkeepalive'; import bs58 from 'bs58'; import {Buffer} from 'buffer'; // @ts-ignore @@ -27,7 +28,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'; @@ -1455,9 +1455,7 @@ function createRpcClient( httpAgent?: HttpAgent | HttpsAgent | false, ): RpcClient { const fetch = customFetch ? customFetch : fetchImpl; - let agentManager: - | {requestEnd(): void; requestStart(): HttpAgent | HttpsAgent} - | undefined; + let agent: HttpAgent | HttpsAgent | undefined; if (process.env.BROWSER) { if (httpAgent != null) { console.warn( @@ -1468,9 +1466,10 @@ function createRpcClient( } else { if (httpAgent == null) { if (process.env.NODE_ENV !== 'test') { - agentManager = new AgentManager( - url.startsWith('https:') /* useHttps */, - ); + agent = new Agent({ + keepAlive: true, + maxSockets: 25, + }); } } else { if (httpAgent !== false) { @@ -1490,7 +1489,7 @@ function createRpcClient( '`https.Agent` through `httpAgent`.', ); } - agentManager = {requestEnd() {}, requestStart: () => httpAgent}; + agent = httpAgent; } } } @@ -1515,7 +1514,6 @@ function createRpcClient( } const clientBrowser = new RpcClient(async (request, callback) => { - const agent = agentManager ? agentManager.requestStart() : undefined; const options = { method: 'POST', body: request, @@ -1565,8 +1563,6 @@ function createRpcClient( } } catch (err) { if (err instanceof Error) callback(err); - } finally { - agentManager && agentManager.requestEnd(); } }, {}); diff --git a/web3.js/yarn.lock b/web3.js/yarn.lock index d0c0b5c0829d55..1a2beaf0dd62f7 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" From 3665d791c8800fb0662487ca6c15ad1310ad2d29 Mon Sep 17 00:00:00 2001 From: steveluscher Date: Tue, 6 Dec 2022 22:18:44 +0000 Subject: [PATCH 3/5] Set the default free socket timeout to 1s less than the Solana RPC's default timeout --- web3.js/src/connection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index a0b9bd7f28eb57..c0db2e67e3b036 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1467,6 +1467,9 @@ function createRpcClient( if (httpAgent == null) { if (process.env.NODE_ENV !== 'test') { agent = new Agent({ + // One second fewer than the Solana RPC's keepalive timeout. + // Read more: https://github.com/solana-labs/solana/issues/27859 + freeSocketTimeout: 19000, keepAlive: true, maxSockets: 25, }); From 4861aba338e35d0b3b380746334fd32888a9e0a0 Mon Sep 17 00:00:00 2001 From: steveluscher Date: Tue, 6 Dec 2022 22:30:08 +0000 Subject: [PATCH 4/5] Add link to particular issue comment --- web3.js/src/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index c0db2e67e3b036..b017645da44808 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1468,7 +1468,7 @@ function createRpcClient( if (process.env.NODE_ENV !== 'test') { agent = new Agent({ // One second fewer than the Solana RPC's keepalive timeout. - // Read more: https://github.com/solana-labs/solana/issues/27859 + // Read more: https://github.com/solana-labs/solana/issues/27859#issuecomment-1340097889 freeSocketTimeout: 19000, keepAlive: true, maxSockets: 25, From 9904db5d75a60371108439f84cd16d4465b08efa Mon Sep 17 00:00:00 2001 From: steveluscher Date: Wed, 7 Dec 2022 23:10:00 +0000 Subject: [PATCH 5/5] Create the correct flavor of default agent for http/https --- web3.js/src/connection.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index b017645da44808..c6b88e1ad3bb9a 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1,10 +1,12 @@ -import Agent from 'agentkeepalive'; +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, @@ -1452,10 +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 agent: HttpAgent | HttpsAgent | undefined; + let agent: NodeHttpAgent | NodeHttpsAgent | undefined; if (process.env.BROWSER) { if (httpAgent != null) { console.warn( @@ -1466,25 +1468,30 @@ function createRpcClient( } else { if (httpAgent == null) { if (process.env.NODE_ENV !== 'test') { - agent = new Agent({ + 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 + @@ -2897,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 */