From f37a52da28ac130b7f4de52901618320994ea87a Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Tue, 6 Jun 2023 17:58:06 -0400 Subject: [PATCH] Added support to detect and stop providers spinning on intitial network detection (#4015). --- src.ts/providers/abstract-provider.ts | 19 +++++++++++- src.ts/providers/provider-jsonrpc.ts | 43 ++++++++++++++++++++------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src.ts/providers/abstract-provider.ts b/src.ts/providers/abstract-provider.ts index 30587bd745..63e07ae6d8 100644 --- a/src.ts/providers/abstract-provider.ts +++ b/src.ts/providers/abstract-provider.ts @@ -421,6 +421,8 @@ export class AbstractProvider implements Provider { // null=unpaused, true=paused+dropWhilePaused, false=paused #pausedState: null | boolean; + #destroyed: boolean; + #networkPromise: null | Promise; readonly #anyNetwork: boolean; @@ -462,6 +464,8 @@ export class AbstractProvider implements Provider { this.#plugins = new Map(); this.#pausedState = null; + this.#destroyed = false; + this.#nextTimer = 1; this.#timers = new Map(); @@ -1445,9 +1449,20 @@ export class AbstractProvider implements Provider { return this.off(event, listener); } + /** + * If this provider has been destroyed using the [[destroy]] method. + * + * Once destroyed, all resources are reclaimed, internal event loops + * and timers are cleaned up and no further requests may be sent to + * the provider. + */ + get destroyed(): boolean { + return this.#destroyed; + } + /** * Sub-classes may use this to shutdown any sockets or release their - * resources. + * resources and reject any pending requests. * * Sub-classes **must** call ``super.destroy()``. */ @@ -1459,6 +1474,8 @@ export class AbstractProvider implements Provider { for (const timerId of this.#timers.keys()) { this._clearTimeout(timerId); } + + this.#destroyed = true; } /** diff --git a/src.ts/providers/provider-jsonrpc.ts b/src.ts/providers/provider-jsonrpc.ts index bdcf7d9be7..cf9732a34a 100644 --- a/src.ts/providers/provider-jsonrpc.ts +++ b/src.ts/providers/provider-jsonrpc.ts @@ -495,6 +495,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { // Process results in batch order for (const { resolve, reject, payload } of batch) { + if (this.destroyed) { + reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method })); + continue; + } + // Find the matching result const resp = result.filter((r) => (r.id === payload.id))[0]; @@ -506,7 +511,6 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { // The response is an error if ("error" in resp) { return reject(this.getRpcError(payload, resp)); - } // All good; send the result @@ -578,13 +582,6 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { * Sub-classes **MUST** override this. */ abstract _send(payload: JsonRpcPayload | Array): Promise>; - /* - { - assert(false, "sub-classes must override _send", "UNSUPPORTED_OPERATION", { - operation: "jsonRpcApiProvider._send" - }); - } - */ /** @@ -678,11 +675,12 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { (async () => { // Bootstrap the network - while (this.#network == null) { + while (this.#network == null && !this.destroyed) { try { this.#network = await this._detectNetwork(); } catch (error) { - console.log("JsonRpcProvider failed to startup; retry in 1s"); + console.log("JsonRpcProvider failed to detect network and cannot start up; retry in 1s (perhaps the URL is wrong or the node is not started)"); + this.emit("error", makeError("failed to bootstrap network detection", "NETWORK_ERROR", { event: "initial-network-discovery", info: { error } })); await stall(1000); } } @@ -973,6 +971,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { send(method: string, params: Array | Record): Promise { // @TODO: cache chainId?? purge on switch_networks + // We have been destroyed; no operations are supported anymore + if (this.destroyed) { + return Promise.reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: method })); + } + const id = this.#nextId++; const promise = new Promise((resolve, reject) => { this.#payloads.push({ @@ -1031,6 +1034,26 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { const accounts: Array = await this.send("eth_accounts", [ ]); return accounts.map((a) => new JsonRpcSigner(this, a)); } + + destroy(): void { + + // Stop processing requests + if (this.#drainTimer) { + clearTimeout(this.#drainTimer); + this.#drainTimer = null; + } + + // Cancel all pending requests + for (const { payload, reject } of this.#payloads) { + reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method })); + } + + this.#payloads = [ ]; + + // Parent clean-up + super.destroy(); + + } } export abstract class JsonRpcApiPollingProvider extends JsonRpcApiProvider {