├── .gitignore ├── README.md ├── Cargo.toml ├── examples └── throttle.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alloy-throttle 2 | 3 | Lightweight throttle layer for alloy providers -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alloy-throttle" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | alloy-json-rpc = "0.12" 8 | alloy-transport = "0.12" 9 | governor = "0.8" 10 | tower = "0.5" 11 | thiserror = "2" 12 | 13 | 14 | [dev-dependencies] 15 | tokio = { version = "1.42", default-features = false, features = [ 16 | "rt-multi-thread", 17 | "macros", 18 | ] } 19 | alloy = { version = "0.12", features = ["provider-ws"] } 20 | eyre = "0.6" 21 | -------------------------------------------------------------------------------- /examples/throttle.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | eips::BlockId, 3 | providers::{Provider, ProviderBuilder}, 4 | rpc::client::ClientBuilder, 5 | }; 6 | use alloy_throttle::ThrottleLayer; 7 | use alloy_transport::layers::RetryBackoffLayer; 8 | 9 | #[tokio::main] 10 | async fn main() -> eyre::Result<()> { 11 | let rpc_endpoint = std::env::var("ETHEREUM_PROVIDER")?; 12 | 13 | let client = ClientBuilder::default() 14 | .layer(ThrottleLayer::new(40, None)?) 15 | // The RetryBackoffLayer can be stacked with the throttle layer to retry failed requests 16 | .layer(RetryBackoffLayer::new(10, 300, 330)) 17 | .http(rpc_endpoint.parse()?); 18 | 19 | let provider = ProviderBuilder::new().on_client(client); 20 | 21 | let block_number = provider.get_block_number().await?; 22 | for _ in block_number - 100..block_number { 23 | let block = provider.get_block(BlockId::latest()).await?; 24 | 25 | if let Some(block) = block { 26 | println!("Tx count: {:?}", block.transactions.len()); 27 | } 28 | } 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | num::{NonZero, NonZeroU32}, 3 | sync::Arc, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use alloy_json_rpc::{RequestPacket, ResponsePacket}; 8 | use alloy_transport::{TransportError, TransportFut}; 9 | use governor::{ 10 | clock::{QuantaClock, QuantaInstant}, 11 | middleware::NoOpMiddleware, 12 | state::{InMemoryState, NotKeyed}, 13 | Jitter, Quota, RateLimiter, 14 | }; 15 | 16 | use thiserror::Error; 17 | use tower::{Layer, Service}; 18 | 19 | pub type Throttle = 20 | RateLimiter>; 21 | 22 | pub struct ThrottleLayer { 23 | throttle: Arc, 24 | jitter: Option, 25 | } 26 | 27 | #[derive(Debug, Error)] 28 | pub enum ThrottleError { 29 | #[error("Requests per second must be a non-zero positive integer")] 30 | InvalidRequestsPerSecond, 31 | } 32 | 33 | impl ThrottleLayer { 34 | pub fn new(requests_per_second: u32, jitter: Option) -> Result { 35 | let quota = NonZeroU32::new(requests_per_second) 36 | .ok_or(ThrottleError::InvalidRequestsPerSecond) 37 | .map(Quota::per_second)?; 38 | 39 | Quota::per_second( 40 | NonZeroU32::new(requests_per_second) 41 | .expect("Requests per second must be a non-zero positive integer"), 42 | ); 43 | 44 | let throttle = Arc::new(RateLimiter::direct(quota)); 45 | 46 | Ok(ThrottleLayer { throttle, jitter }) 47 | } 48 | } 49 | 50 | impl Layer for ThrottleLayer { 51 | type Service = ThrottleService; 52 | 53 | fn layer(&self, inner: S) -> Self::Service { 54 | ThrottleService { 55 | inner, 56 | throttle: self.throttle.clone(), 57 | jitter: self.jitter, 58 | } 59 | } 60 | } 61 | 62 | /// A Tower Service used by the ThrottleLayer that is responsible for throttling rpc requests. 63 | #[derive(Debug, Clone)] 64 | pub struct ThrottleService { 65 | /// The inner service 66 | inner: S, 67 | throttle: Arc, 68 | jitter: Option, 69 | } 70 | 71 | impl Service for ThrottleService 72 | where 73 | S: Service 74 | + Send 75 | + 'static 76 | + Clone, 77 | S::Future: Send + 'static, 78 | { 79 | type Response = ResponsePacket; 80 | type Error = TransportError; 81 | type Future = TransportFut<'static>; 82 | 83 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 84 | self.inner.poll_ready(cx) 85 | } 86 | 87 | fn call(&mut self, request: RequestPacket) -> Self::Future { 88 | let throttle = self.throttle.clone(); 89 | let jitter = self.jitter; 90 | let mut inner = self.inner.clone(); 91 | 92 | Box::pin(async move { 93 | if let Some(jitter) = jitter { 94 | throttle.until_ready_with_jitter(jitter).await; 95 | } else { 96 | throttle.until_ready().await; 97 | } 98 | 99 | inner.call(request).await 100 | }) 101 | } 102 | } 103 | --------------------------------------------------------------------------------