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
This commit is contained in:
Steven Luscher 2022-12-19 10:22:25 -08:00 committed by GitHub
parent 3eca364190
commit 456a81982e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 34 additions and 102 deletions

View File

@ -62,6 +62,7 @@
"@noble/hashes": "^1.1.2", "@noble/hashes": "^1.1.2",
"@noble/secp256k1": "^1.6.3", "@noble/secp256k1": "^1.6.3",
"@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout": "^4.0.0",
"agentkeepalive": "^4.2.1",
"bigint-buffer": "^1.1.5", "bigint-buffer": "^1.1.5",
"bn.js": "^5.0.0", "bn.js": "^5.0.0",
"borsh": "^0.7.0", "borsh": "^0.7.0",

View File

@ -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<typeof setTimeout> | 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);
}
}
}

View File

@ -1,9 +1,12 @@
import HttpKeepAliveAgent, {
HttpsAgent as HttpsKeepAliveAgent,
} from 'agentkeepalive';
import bs58 from 'bs58'; import bs58 from 'bs58';
import {Buffer} from 'buffer'; import {Buffer} from 'buffer';
// @ts-ignore // @ts-ignore
import fastStableStringify from 'fast-stable-stringify'; import fastStableStringify from 'fast-stable-stringify';
import type {Agent as HttpAgent} from 'http'; import type {Agent as NodeHttpAgent} from 'http';
import {Agent as HttpsAgent} from 'https'; import {Agent as NodeHttpsAgent} from 'https';
import { import {
type as pick, type as pick,
number, number,
@ -27,7 +30,6 @@ import {Client as RpcWebSocketClient} from 'rpc-websockets';
import RpcClient from 'jayson/lib/client/browser'; import RpcClient from 'jayson/lib/client/browser';
import {JSONRPCError} from 'jayson'; import {JSONRPCError} from 'jayson';
import {AgentManager} from './agent-manager';
import {EpochSchedule} from './epoch-schedule'; import {EpochSchedule} from './epoch-schedule';
import {SendTransactionError, SolanaJSONRPCError} from './errors'; import {SendTransactionError, SolanaJSONRPCError} from './errors';
import fetchImpl, {Response} from './fetch-impl'; import fetchImpl, {Response} from './fetch-impl';
@ -1452,12 +1454,10 @@ function createRpcClient(
customFetch?: FetchFn, customFetch?: FetchFn,
fetchMiddleware?: FetchMiddleware, fetchMiddleware?: FetchMiddleware,
disableRetryOnRateLimit?: boolean, disableRetryOnRateLimit?: boolean,
httpAgent?: HttpAgent | HttpsAgent | false, httpAgent?: NodeHttpAgent | NodeHttpsAgent | false,
): RpcClient { ): RpcClient {
const fetch = customFetch ? customFetch : fetchImpl; const fetch = customFetch ? customFetch : fetchImpl;
let agentManager: let agent: NodeHttpAgent | NodeHttpsAgent | undefined;
| {requestEnd(): void; requestStart(): HttpAgent | HttpsAgent}
| undefined;
if (process.env.BROWSER) { if (process.env.BROWSER) {
if (httpAgent != null) { if (httpAgent != null) {
console.warn( console.warn(
@ -1468,21 +1468,30 @@ function createRpcClient(
} else { } else {
if (httpAgent == null) { if (httpAgent == null) {
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
agentManager = new AgentManager( const agentOptions = {
url.startsWith('https:') /* useHttps */, // 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 { } else {
if (httpAgent !== false) { if (httpAgent !== false) {
const isHttps = url.startsWith('https:'); const isHttps = url.startsWith('https:');
if (isHttps && !(httpAgent instanceof HttpsAgent)) { if (isHttps && !(httpAgent instanceof NodeHttpsAgent)) {
throw new Error( throw new Error(
'The endpoint `' + 'The endpoint `' +
url + url +
'` can only be paired with an `https.Agent`. You have, instead, supplied an ' + '` can only be paired with an `https.Agent`. You have, instead, supplied an ' +
'`http.Agent` through `httpAgent`.', '`http.Agent` through `httpAgent`.',
); );
} else if (!isHttps && httpAgent instanceof HttpsAgent) { } else if (!isHttps && httpAgent instanceof NodeHttpsAgent) {
throw new Error( throw new Error(
'The endpoint `' + 'The endpoint `' +
url + url +
@ -1490,7 +1499,7 @@ function createRpcClient(
'`https.Agent` through `httpAgent`.', '`https.Agent` through `httpAgent`.',
); );
} }
agentManager = {requestEnd() {}, requestStart: () => httpAgent}; agent = httpAgent;
} }
} }
} }
@ -1515,7 +1524,6 @@ function createRpcClient(
} }
const clientBrowser = new RpcClient(async (request, callback) => { const clientBrowser = new RpcClient(async (request, callback) => {
const agent = agentManager ? agentManager.requestStart() : undefined;
const options = { const options = {
method: 'POST', method: 'POST',
body: request, body: request,
@ -1565,8 +1573,6 @@ function createRpcClient(
} }
} catch (err) { } catch (err) {
if (err instanceof Error) callback(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 * persistence). Set this to `false` to create a connection that uses no agent. This applies to
* Node environments only. * Node environments only.
*/ */
httpAgent?: HttpAgent | HttpsAgent | false; httpAgent?: NodeHttpAgent | NodeHttpsAgent | false;
/** Optional commitment level */ /** Optional commitment level */
commitment?: Commitment; commitment?: Commitment;
/** Optional endpoint URL to the fullnode JSON RPC PubSub WebSocket Endpoint */ /** Optional endpoint URL to the fullnode JSON RPC PubSub WebSocket Endpoint */

View File

@ -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);
});

View File

@ -1834,7 +1834,7 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2:
dependencies: dependencies:
debug "4" debug "4"
agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: agentkeepalive@^4.1.3:
version "4.2.1" version "4.2.1"
resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz"
dependencies: dependencies:
@ -1842,6 +1842,15 @@ agentkeepalive@^4.1.3, agentkeepalive@^4.2.1:
depd "^1.1.2" depd "^1.1.2"
humanize-ms "^1.2.1" 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: aggregate-error@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz"