Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve RPC provider request handling #3774

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Improve RPC provider request handling #3774

wants to merge 5 commits into from

Conversation

hyphenized
Copy link
Collaborator

@hyphenized hyphenized commented Feb 6, 2025

This adds TahoRPCProvider to optimize RPC batching and request handling and sets it as default provider used since most
public RPCs support batching. This provider is configurable and sets sane defaults that seem to work fine with most of the public RPCs we use within the wallet.

Furthermore since options can be changed at runtime, the serial fallback provider should be able use error data to adjust the provider options if the already strict limits change in the future.

Latest build: extension-builds-3774 (as of Mon, 17 Feb 2025 03:44:07 GMT).

@hyphenized hyphenized self-assigned this Feb 6, 2025
Copy link
Contributor

@Shadowfiend Shadowfiend left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some thoughts.

The amount of code in TahoRpcProvider makes me twitchy from a maintenance perspective, though the cleanup of concerns makes me happy. Need to mull it a bit more (in addition to code reviewing the rest of the class).

provider: RPCProvider,
): provider is WebSocketProvider => provider instanceof WebSocketProvider

const isJsonRpcProvider = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be isTahoRPCProvider?

Also, why have this function at all if it's an instanceof check? I think we can just inline the instanceof check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this isn't adding a lot of value

const webSocket = provider._websocket as WebSocket
function isClosedOrClosingWebSocketProvider(provider: RPCProvider): boolean {
if (isWebSocketProvider(provider)) {
const webSocket = provider.websocket
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@@ -27,13 +26,24 @@ import {
import { FeatureFlags, isEnabled } from "../../features"
import { RpcConfig } from "./db"
import TahoAlchemyProvider from "./taho-provider"
import TahoRPCProvider from "./taho-rpc-provider"

type RPCProvider = WebSocketProvider | JsonRpcProvider | TahoRPCProvider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we… Just use JsonRpcProvider? WebSocketProvider and TahoRPCProvider are just subclasses, and instanceof checks should do type narrowing (if not we may need to bump our TS version), so it seems like this whole type (and the type unions that preceded it) isn't useful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, my main criteria for adding this alias was to make it clearer which provider types the serial fallback provider can work with since their subclassing isn't immediately obvious without looking at their implementation.

I had initially planned to remove JsonRpcProvider and make TahoAlchemyProvider subclass the new provider as well. However, I found that certain alchemy requests can't be batched, and accounting for that would have extended the scope of this work

method,
params,
)
const batchLen = requestBatch.length
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This intermediary variable doesn't seem necessary, the underlying value doesn't change during use, right?

If we do keep it, let's not abbreviate “length” and maybe we use currentBatchLength rather than just batch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, good catch

Comment on lines 62 to 63
// eslint-disable-next-line no-bitwise
maxBatchSize: 1 << 20, // 1Mb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// eslint-disable-next-line no-bitwise
maxBatchSize: 1 << 20, // 1Mb
1_048_576, // 1Mb

Why not just write this out?

Comment on lines 39 to 57
/**
* Maximum number of requests to be batched
* @default 10 requests
*/
maxBatchLength?: number
/**
* Maximum length in bytes of the batch
* @default 1Mb
*/
maxBatchSize?: number
/**
* How long to wait to aggregate requests
* @default 100ms
*/
batchStallTime?: number
/**
* How long to wait between each payload sent
*/
delayBetweenRequests?: number
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we only modify one of these externally, right? Why expose other settings for modification?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I had initially intended to have these options exposed so they could be changed during error handling. I'll remove delayBetweenRequests since a better option for handling 429s would be better at a higher level in the SerialFallbackProvider, and move both batchStallTime and maxBatchSize to constants.

@Shadowfiend
Copy link
Contributor

Out of curiosity, is there an observed improvement in behavior here? What does it look like?

Use correct types for providers supported by serial-fallback-provider. Should also help avoid some type casts when accessing provider internals.
Added `TahoRPCProvider` to optimize RPC batching and request handling.
Batch limits can now be set per provider.

This also sets TahoRPCProvider as the default provider used since most
json 2.0 rpcs support batching. It does not handle cases where the provider
refuses to handle batches even if they consist of a single request, as is the
case with the flashbots rpc.
Reduced the max batch length from 20 to 10 for better compatibility with
public RPCs and added a configurable delay between requests to prevent
rate limit errors.
@hyphenized hyphenized force-pushed the taho-rpc branch 2 times, most recently from 5f90f2b to ee2aa38 Compare February 17, 2025 03:34
@hyphenized hyphenized requested a review from a team as a code owner February 17, 2025 03:34
@hyphenized
Copy link
Collaborator Author

Out of curiosity, is there an observed improvement in behavior here? What does it look like?

Yes, just having a limit set on the batch length makes a big difference in that it prevents a bunch of failed requests turning into 429s and triggering fallbacks while the rpc tries to figure out the limits set by the provider. Some error cases are handled better as well in comparison to the patched ethers RPC such as missing or bad responses.

hyphenized and others added 2 commits February 17, 2025 03:12
Removed delay between requests as it introduces hidden latency and
moved unused exposed options to constants. Batch stall time has been
adjusted accordingly. Type alias removed

Co-authored-by: Antonio Salazar Cardozo <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants