Skip to content

Commit 9e20e5f

Browse files
authored
Reword circuit break error message (#6423)
The error that is thrown when an RPC endpoint responds with too many 5xx errors in a row — causing the underlying circuit to break — is too cryptic for end users. This commit rewords it to be more friendly and recommend that users switch to another endpoint. It also logs the original Cockatiel error so that users can inspect DevTools to learn more. - Old error: "Execution prevented because the circuit breaker is open" - New error: "RPC endpoint returned too many errors, retrying in X minutes. Consider using a different RPC endpoint."
1 parent 293b908 commit 9e20e5f

File tree

8 files changed

+313
-0
lines changed

8 files changed

+313
-0
lines changed

packages/controller-utils/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `circuitBreakDuration` to the object returned by `createServicePolicy` ([#6423](https://github.com/MetaMask/core/pull/6423))
13+
- This is the amount of time that the underlying circuit breaker policy will pause execution of the input function while the circuit is broken.
14+
- Add `getRemainingCircuitOpenDuration` to the object returned by `createServicePolicy` ([#6423](https://github.com/MetaMask/core/pull/6423))
15+
- This returns the amount of time after which the underlying circuit breaker policy will resume execution of the input function after the circuit reopens.
16+
1017
## [11.12.0]
1118

1219
### Added

packages/controller-utils/src/create-service-policy.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ export type ServicePolicy = IPolicy & {
8080
* internally.
8181
*/
8282
circuitBreakerPolicy: CircuitBreakerPolicy;
83+
/**
84+
* The amount of time to pause requests to the service if the number of
85+
* maximum consecutive failures is reached.
86+
*/
87+
circuitBreakDuration: number;
88+
/**
89+
* If the circuit is open and ongoing requests are paused, returns the number
90+
* of milliseconds before the requests will be attempted again. If the circuit
91+
* is not open, returns null.
92+
*/
93+
getRemainingCircuitOpenDuration: () => number | null;
8394
/**
8495
* The Cockatiel retry policy that the service policy uses internally.
8596
*/
@@ -105,6 +116,17 @@ export type ServicePolicy = IPolicy & {
105116
onRetry: RetryPolicy['onRetry'];
106117
};
107118

119+
/**
120+
* Parts of the circuit breaker's internal and external state as necessary in
121+
* order to compute the time remaining before the circuit will reopen.
122+
*/
123+
type InternalCircuitState =
124+
| {
125+
state: CircuitState.Open;
126+
openedAt: number;
127+
}
128+
| { state: Exclude<CircuitState, CircuitState.Open> };
129+
108130
/**
109131
* The maximum number of times that a failing service should be re-run before
110132
* giving up.
@@ -146,6 +168,25 @@ const isServiceFailure = (error: unknown) => {
146168
return true;
147169
};
148170

171+
/**
172+
* The circuit breaker policy inside of the Cockatiel library exposes some of
173+
* its state, but not all of it. Notably, the time that the circuit opened is
174+
* not publicly accessible. So we have to record this ourselves.
175+
*
176+
* This function therefore allows us to obtain the circuit breaker state that we
177+
* wish we could access.
178+
*
179+
* @param state - The public state of a circuit breaker policy.
180+
* @returns if the circuit is open, the state of the circuit breaker policy plus
181+
* the time that it opened, otherwise just the circuit state.
182+
*/
183+
function getInternalCircuitState(state: CircuitState): InternalCircuitState {
184+
if (state === CircuitState.Open) {
185+
return { state, openedAt: Date.now() };
186+
}
187+
return { state };
188+
}
189+
149190
/**
150191
* Constructs an object exposing an `execute` method which, given a function —
151192
* hereafter called the "service" — will retry that service with ever increasing
@@ -228,6 +269,13 @@ export function createServicePolicy(
228269
halfOpenAfter: circuitBreakDuration,
229270
breaker: new ConsecutiveBreaker(maxConsecutiveFailures),
230271
});
272+
273+
let internalCircuitState: InternalCircuitState = getInternalCircuitState(
274+
circuitBreakerPolicy.state,
275+
);
276+
circuitBreakerPolicy.onStateChange((state) => {
277+
internalCircuitState = getInternalCircuitState(state);
278+
});
231279
const onBreak = circuitBreakerPolicy.onBreak.bind(circuitBreakerPolicy);
232280

233281
const onDegradedEventEmitter =
@@ -251,9 +299,18 @@ export function createServicePolicy(
251299
// breaker policy, which executes the service.
252300
const policy = wrap(retryPolicy, circuitBreakerPolicy);
253301

302+
const getRemainingCircuitOpenDuration = () => {
303+
if (internalCircuitState.state === CircuitState.Open) {
304+
return internalCircuitState.openedAt + circuitBreakDuration - Date.now();
305+
}
306+
return null;
307+
};
308+
254309
return {
255310
...policy,
256311
circuitBreakerPolicy,
312+
circuitBreakDuration,
313+
getRemainingCircuitOpenDuration,
257314
retryPolicy,
258315
onBreak,
259316
onDegraded,

packages/network-controller/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Bump `@metamask/base-controller` from `^8.1.0` to `^8.3.0` ([#6355](https://github.com/MetaMask/core/pull/6355), [#6465](https://github.com/MetaMask/core/pull/6465))
13+
- Rephrase "circuit broken" errors so they are more user-friendly ([#6423](https://github.com/MetaMask/core/pull/6423))
14+
- These are errors produced when a request is made to an RPC endpoint after it returns too many consecutive 5xx responses and the underlying circuit is open.
1315

1416
### Deprecated
1517

packages/network-controller/src/NetworkController.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,7 @@ export class NetworkController extends BaseController<
28002800
getBlockTrackerOptions: this.#getBlockTrackerOptions,
28012801
messenger: this.messagingSystem,
28022802
isRpcFailoverEnabled: this.#isRpcFailoverEnabled,
2803+
logger: this.#log,
28032804
});
28042805
} else {
28052806
autoManagedNetworkClientRegistry[NetworkClientType.Custom][
@@ -2816,6 +2817,7 @@ export class NetworkController extends BaseController<
28162817
getBlockTrackerOptions: this.#getBlockTrackerOptions,
28172818
messenger: this.messagingSystem,
28182819
isRpcFailoverEnabled: this.#isRpcFailoverEnabled,
2820+
logger: this.#log,
28192821
});
28202822
}
28212823
}
@@ -2978,6 +2980,7 @@ export class NetworkController extends BaseController<
29782980
getBlockTrackerOptions: this.#getBlockTrackerOptions,
29792981
messenger: this.messagingSystem,
29802982
isRpcFailoverEnabled: this.#isRpcFailoverEnabled,
2983+
logger: this.#log,
29812984
}),
29822985
] as const;
29832986
}
@@ -2995,6 +2998,7 @@ export class NetworkController extends BaseController<
29952998
getBlockTrackerOptions: this.#getBlockTrackerOptions,
29962999
messenger: this.messagingSystem,
29973000
isRpcFailoverEnabled: this.#isRpcFailoverEnabled,
3001+
logger: this.#log,
29983002
}),
29993003
] as const;
30003004
});

packages/network-controller/src/create-auto-managed-network-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { PollingBlockTrackerOptions } from '@metamask/eth-block-tracker';
2+
import type { Logger } from 'loglevel';
23

34
import type { NetworkClient } from './create-network-client';
45
import { createNetworkClient } from './create-network-client';
@@ -76,6 +77,7 @@ const UNINITIALIZED_TARGET = { __UNINITIALIZED__: true };
7677
* @param args.isRpcFailoverEnabled - Whether or not requests sent to the
7778
* primary RPC endpoint for this network should be automatically diverted to
7879
* provided failover endpoints if the primary is unavailable.
80+
* @param args.logger - A `loglevel` logger.
7981
* @returns The auto-managed network client.
8082
*/
8183
export function createAutoManagedNetworkClient<
@@ -86,6 +88,7 @@ export function createAutoManagedNetworkClient<
8688
getBlockTrackerOptions = () => ({}),
8789
messenger,
8890
isRpcFailoverEnabled: givenIsRpcFailoverEnabled,
91+
logger,
8992
}: {
9093
networkClientConfiguration: Configuration;
9194
getRpcServiceOptions: (
@@ -96,6 +99,7 @@ export function createAutoManagedNetworkClient<
9699
) => Omit<PollingBlockTrackerOptions, 'provider'>;
97100
messenger: NetworkControllerMessenger;
98101
isRpcFailoverEnabled: boolean;
102+
logger?: Logger;
99103
}): AutoManagedNetworkClient<Configuration> {
100104
let isRpcFailoverEnabled = givenIsRpcFailoverEnabled;
101105
let networkClient: NetworkClient | undefined;
@@ -107,6 +111,7 @@ export function createAutoManagedNetworkClient<
107111
getBlockTrackerOptions,
108112
messenger,
109113
isRpcFailoverEnabled,
114+
logger,
110115
});
111116

112117
if (networkClient === undefined) {

packages/network-controller/src/create-network-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '@metamask/json-rpc-engine';
2626
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
2727
import type { Hex, Json, JsonRpcParams } from '@metamask/utils';
28+
import type { Logger } from 'loglevel';
2829

2930
import type { NetworkControllerMessenger } from './NetworkController';
3031
import type { RpcServiceOptions } from './rpc-service/rpc-service';
@@ -64,6 +65,7 @@ export type NetworkClient = {
6465
* provided failover endpoints if the primary is unavailable. This effectively
6566
* causes the `failoverRpcUrls` property of the network client configuration
6667
* to be honored or ignored.
68+
* @param args.logger - A `loglevel` logger.
6769
* @returns The network client.
6870
*/
6971
export function createNetworkClient({
@@ -72,6 +74,7 @@ export function createNetworkClient({
7274
getBlockTrackerOptions,
7375
messenger,
7476
isRpcFailoverEnabled,
77+
logger,
7578
}: {
7679
configuration: NetworkClientConfiguration;
7780
getRpcServiceOptions: (
@@ -82,6 +85,7 @@ export function createNetworkClient({
8285
) => Omit<PollingBlockTrackerOptions, 'provider'>;
8386
messenger: NetworkControllerMessenger;
8487
isRpcFailoverEnabled: boolean;
88+
logger?: Logger;
8589
}): NetworkClient {
8690
const primaryEndpointUrl =
8791
configuration.type === NetworkClientType.Infura
@@ -94,6 +98,7 @@ export function createNetworkClient({
9498
availableEndpointUrls.map((endpointUrl) => ({
9599
...getRpcServiceOptions(endpointUrl),
96100
endpointUrl,
101+
logger,
97102
})),
98103
);
99104
rpcServiceChain.onBreak(({ endpointUrl, failoverEndpointUrl, ...rest }) => {

0 commit comments

Comments
 (0)