diff --git a/CHANGELOG.md b/CHANGELOG.md index a9dc96c09a..450c53b2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes: - Support for rejection of hanging RPC requests (30s timeout) +- Add `.stats()` to provider interfaces with running counters ## 7.12.1 Mar 13, 2022 diff --git a/packages/rpc-provider/src/http/index.ts b/packages/rpc-provider/src/http/index.ts index 3f882ba010..92e93ef12b 100644 --- a/packages/rpc-provider/src/http/index.ts +++ b/packages/rpc-provider/src/http/index.ts @@ -1,7 +1,7 @@ // Copyright 2017-2022 @polkadot/rpc-provider authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types'; +import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted, ProviderStats } from '../types'; import { assert, logger } from '@polkadot/util'; import { fetch } from '@polkadot/x-fetch'; @@ -43,6 +43,8 @@ export class HttpProvider implements ProviderInterface { readonly #headers: Record; + readonly #stats: ProviderStats; + /** * @param {string} endpoint The endpoint url starting with http:// */ @@ -52,6 +54,10 @@ export class HttpProvider implements ProviderInterface { this.#coder = new RpcCoder(); this.#endpoint = endpoint; this.#headers = headers; + this.#stats = { + active: { requests: 0, subscriptions: 0 }, + total: { cached: 0, requests: 0, subscriptions: 0, timeout: 0 } + }; } /** @@ -82,6 +88,13 @@ export class HttpProvider implements ProviderInterface { // noop } + /** + * @description Returns the connection stats + */ + public stats (): ProviderStats { + return this.#stats; + } + /** * @summary Whether the node is connected or not. * @return {boolean} true if connected @@ -107,6 +120,8 @@ export class HttpProvider implements ProviderInterface { * @summary Send HTTP POST Request with Body to configured HTTP Endpoint. */ public async send (method: string, params: unknown[], isCacheable?: boolean): Promise { + this.#stats.total.requests++; + const body = this.#coder.encodeJson(method, params); let resultPromise: Promise | null = isCacheable ? this.#callCache.get(body) as Promise @@ -118,28 +133,41 @@ export class HttpProvider implements ProviderInterface { if (isCacheable) { this.#callCache.set(body, resultPromise); } + } else { + this.#stats.total.cached++; } return resultPromise; } async #send (body: string): Promise { - const response = await fetch(this.#endpoint, { - body, - headers: { - Accept: 'application/json', - 'Content-Length': `${body.length}`, - 'Content-Type': 'application/json', - ...this.#headers - }, - method: 'POST' - }); + this.#stats.active.requests++; - assert(response.ok, () => `[${response.status}]: ${response.statusText}`); + try { + const response = await fetch(this.#endpoint, { + body, + headers: { + Accept: 'application/json', + 'Content-Length': `${body.length}`, + 'Content-Type': 'application/json', + ...this.#headers + }, + method: 'POST' + }); - const result = (await response.json()) as JsonRpcResponse; + assert(response.ok, () => `[${response.status}]: ${response.statusText}`); - return this.#coder.decodeResponse(result) as T; + const result = (await response.json()) as JsonRpcResponse; + const decoded = this.#coder.decodeResponse(result) as T; + + this.#stats.active.requests--; + + return decoded; + } catch (e) { + this.#stats.active.requests--; + + throw e; + } } /** diff --git a/packages/rpc-provider/src/types.ts b/packages/rpc-provider/src/types.ts index 87d3654b7f..fc25584f34 100644 --- a/packages/rpc-provider/src/types.ts +++ b/packages/rpc-provider/src/types.ts @@ -57,6 +57,20 @@ export interface ProviderInterface { disconnect (): Promise; on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void; send (method: string, params: unknown[], isCacheable?: boolean): Promise; + stats?: () => ProviderStats; subscribe (type: string, method: string, params: unknown[], cb: ProviderInterfaceCallback): Promise; unsubscribe (type: string, method: string, id: number | string): Promise; } + +export interface ProviderStats { + active: { + requests: number; + subscriptions: number; + }; + total: { + cached: number; + requests: number; + subscriptions: number; + timeout: number; + }; +} diff --git a/packages/rpc-provider/src/ws/index.ts b/packages/rpc-provider/src/ws/index.ts index 4d9fafc4a5..ea296cc271 100644 --- a/packages/rpc-provider/src/ws/index.ts +++ b/packages/rpc-provider/src/ws/index.ts @@ -3,7 +3,7 @@ /* eslint-disable camelcase */ -import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types'; +import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted, ProviderStats } from '../types'; import EventEmitter from 'eventemitter3'; @@ -95,6 +95,8 @@ export class WsProvider implements ProviderInterface { readonly #isReadyPromise: Promise; + readonly #stats: ProviderStats; + readonly #waitingForId: Record = {}; #autoConnectMs: number; @@ -129,6 +131,10 @@ export class WsProvider implements ProviderInterface { this.#endpoints = endpoints; this.#headers = headers; this.#websocket = null; + this.#stats = { + active: { requests: 0, subscriptions: 0 }, + total: { cached: 0, requests: 0, subscriptions: 0, timeout: 0 } + }; if (autoConnectMs > 0) { this.connectWithRetry().catch((): void => { @@ -251,6 +257,19 @@ export class WsProvider implements ProviderInterface { } } + /** + * @description Returns the connection stats + */ + public stats (): ProviderStats { + return { + active: { + requests: Object.keys(this.#handlers).length, + subscriptions: Object.keys(this.#subscriptions).length + }, + total: this.#stats.total + }; + } + /** * @summary Listens on events after having subscribed using the [[subscribe]] function. * @param {ProviderInterfaceEmitted} type Event @@ -272,6 +291,8 @@ export class WsProvider implements ProviderInterface { * @param subscription Subscription details (internally used) */ public send (method: string, params: unknown[], isCacheable?: boolean, subscription?: SubscriptionHandler): Promise { + this.#stats.total.requests++; + const body = this.#coder.encodeJson(method, params); let resultPromise: Promise | null = isCacheable ? this.#callCache.get(body) as Promise @@ -283,6 +304,8 @@ export class WsProvider implements ProviderInterface { if (isCacheable) { this.#callCache.set(body, resultPromise); } + } else { + this.#stats.total.cached++; } return resultPromise; @@ -337,6 +360,8 @@ export class WsProvider implements ProviderInterface { * ``` */ public subscribe (type: string, method: string, params: unknown[], callback: ProviderInterfaceCallback): Promise { + this.#stats.total.subscriptions++; + // subscriptions are not cached, LRU applies to .at() only return this.send(method, params, false, { callback, type }); } @@ -539,6 +564,7 @@ export class WsProvider implements ProviderInterface { // ignore } + this.#stats.total.timeout++; delete this.#handlers[ids[i]]; } }