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/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",

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 {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 */

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:
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"