Skip to content

feat: FallbackLayer transport #2135

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

Merged
merged 16 commits into from
Mar 14, 2025

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
@merklefruit merklefruit force-pushed the nico/feat/fallback-layer branch from 2ca3bc0 to 71331a1 Compare March 12, 2025 16:47
@merklefruit
Copy link
Contributor Author

Hi @mattsse, can I have another round of review please?

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.

a few more suggestions

@github-project-automation github-project-automation bot moved this to In Progress in Alloy Mar 13, 2025
@merklefruit merklefruit force-pushed the nico/feat/fallback-layer branch from 360eb6f to 1ad5d77 Compare March 13, 2025 14:12
@merklefruit merklefruit requested a review from mattsse March 13, 2025 22:14
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.

lgtm!

@github-project-automation github-project-automation bot moved this from In Progress to Reviewed in Alloy Mar 14, 2025
@mattsse mattsse merged commit df36d8d into alloy-rs:main Mar 14, 2025
27 checks passed
@github-project-automation github-project-automation bot moved this from Reviewed to Done in Alloy Mar 14, 2025
@merklefruit merklefruit deleted the nico/feat/fallback-layer branch March 15, 2025 10:06
frankudoags pushed a commit to frankudoags/alloy that referenced this pull request Mar 15, 2025
* feat: initial impl of fallback layer and service

* chore: released trait bounds

* chore: use random ping id

* chore: update fn name

* chore: update fn name

* chore: rm health check task and sample count

* fix: lock scope

* chore: adjust logs

* chore: small cleanup

* chore: cargo fmt

* chore: cargo fmt again

* chore: addressed review, dont take transports from the list on each request

* chore: rm private doc

* chore: update mod doc comment ordering

* chore: addressed review

---------

Co-authored-by: Jennifer <[email protected]>
yash-atreya pushed a commit that referenced this pull request Mar 18, 2025
* feat: define subscription type

* feat: `FallbackLayer` transport (#2135)

* feat: initial impl of fallback layer and service

* chore: released trait bounds

* chore: use random ping id

* chore: update fn name

* chore: update fn name

* chore: rm health check task and sample count

* fix: lock scope

* chore: adjust logs

* chore: small cleanup

* chore: cargo fmt

* chore: cargo fmt again

* chore: addressed review, dont take transports from the list on each request

* chore: rm private doc

* chore: update mod doc comment ordering

* chore: addressed review

---------

Co-authored-by: Jennifer <[email protected]>

* feat: add BlobAndProofV2 (#2202)

* feat: add BlobAndProofV2

* Update crates/eips/src/eip4844/engine.rs

---------

Co-authored-by: Matthias Seitz <[email protected]>

* fix: buffer size go brrr

* refactor: rename EthSuscribe to GetSubscription, naming is hard sigh

* feat:
- fix description
- add `method` member
- make params optional
- handle optional channel size and also handle params case and no params case separately

* chore: fmt, rename buffer to channel_size

---------

Co-authored-by: nicolas <[email protected]>
Co-authored-by: Jennifer <[email protected]>
Co-authored-by: Varun Doshi <[email protected]>
Co-authored-by: Matthias Seitz <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

[Feature] HttpMultiClient<T> Transport
3 participants