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:
parent
3eca364190
commit
456a81982e
|
@ -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",
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue