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

feat: FallbackLayer transport #2135

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

Conversation

merklefruit
Copy link
Contributor

@merklefruit merklefruit commented Mar 1, 2025

Motivation

Having a provider be able to consume a list of transports can increase both availability and performance, both desirable features of many applications that need a connection to Ethereum.

Several issues such as #1938 and #1760 outline a feature like this in Alloy.
Other libraries such as Viem and Ethers.js also have something similar.

Solution

This PR implements FallbackLayer and FallbackService, following the usual tower abstractions. Most notably, the layer here works with a Vec<S> because this service is meant for managing a list of transports.

When sending a request to a provider using this layer, it queries the top ranked N transports (configurable with the active_transport_count option) concurrently, returning the first successful result. Whenever a result arrives, metrics are updated to keep track of the performance of each transport, using a simple combination of average latency and success rate.

I tried to keep this simple, but there is potential for future work if someone finds it useful:

  • adding a health check task to keep ranks always up to date
  • adding a minimum threshold of consistent responses (aka "quorum response")
  • adding rate-limiting per transport
  • adding circuit-breakers for non-responding transports

Usage

I've added an example usage binary here: merklefruit/alloy-examples#1, but in short here is how it works:

// Configure the fallback layer
let fallback_layer = FallbackLayer::default()
    .with_active_transport_count(NonZeroUsize::new(3).unwrap())
    .with_log_transport_rankings(true);

// Define your list of transports to use
let transports = vec![
    Http::new(Url::parse("https://eth.merkle.io/")?),
    Http::new(Url::parse("https://eth.llamarpc.com/")?),
    Http::new(Url::parse("https://ethereum-rpc.publicnode.com/")?),
];

// Apply the FallbackLayer to the transports
let transport = ServiceBuilder::new().layer(fallback_layer).service(transports);
let client = RpcClient::builder().transport(transport, false);
let provider = ProviderBuilder::new().on_client(client);

// use the provider as normal
let latest_block = provider.get_block_number().await?;

PR Checklist

Tested on a live example, as I couldn't add comprehensive unit tests without running into cyclic dependencies in alloy itself.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

mostly questions, I think overall this makes sense and I was able to follow, but I have some concerns about the make_request impl, because if there are multiple concurrent requests, would this result in empty transports error rn?

Comment on lines 266 to 269
let mut transports = self.transports.lock().expect("Lock poisoned");
for _ in 0..self.active_transport_count.min(transports.len()) {
if let Some(transport) = transports.pop() {
top_transports.push(transport);
Copy link
Member

Choose a reason for hiding this comment

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

not exactly sure I fully understood this but it seems like this pops the transports and on success it adds them back?

what happens if there are no available transports?

Copy link
Contributor Author

@merklefruit merklefruit Mar 8, 2025

Choose a reason for hiding this comment

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

Yes, I've updated this to use a Vec instead of a BinaryHeap and re-sort it whenever a score changes.
This way I don't need to take the transports from the list on each request anymore.

@yash-atreya yash-atreya linked an issue Mar 7, 2025 that may be closed by this pull request
@merklefruit merklefruit force-pushed the nico/feat/fallback-layer branch from 6f68e2a to 45994f8 Compare March 8, 2025 08:37
@merklefruit merklefruit requested a review from mattsse March 8, 2025 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

[Feature] HttpMultiClient<T> Transport
2 participants