connection pooling
This commit is contained in:
parent
3ddde56665
commit
8cc2fab48e
69
Cargo.lock
generated
69
Cargo.lock
generated
@ -212,6 +212,16 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-client-ip"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ba6eab8967a7b1121e3cd7491a205dff7a33bb05e65df6c199d687a32e4d3b"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"forwarded-header-value",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.2.6"
|
||||
@ -281,6 +291,30 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
|
||||
|
||||
[[package]]
|
||||
name = "bb8"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1627eccf3aa91405435ba240be23513eeca466b5dc33866422672264de061582"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"parking_lot 0.12.1",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bb8-redis"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b081e2864000416c5a4543fed63fb8dd979c4a0806b071fddab0e548c6cd4c74"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bb8",
|
||||
"redis",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.7.3"
|
||||
@ -1455,6 +1489,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
@ -2281,6 +2325,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "4.0.17"
|
||||
@ -2824,7 +2874,6 @@ dependencies = [
|
||||
"itoa 0.4.8",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"tokio-util 0.6.10",
|
||||
"url",
|
||||
@ -2835,7 +2884,7 @@ name = "redis-cell-client"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"redis",
|
||||
"bb8-redis",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3199,21 +3248,6 @@ dependencies = [
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
|
||||
dependencies = [
|
||||
"sha1_smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.8.2"
|
||||
@ -4095,6 +4129,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"argh",
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
"counter",
|
||||
"dashmap",
|
||||
"derive_more",
|
||||
|
11
TODO.md
11
TODO.md
@ -34,8 +34,13 @@
|
||||
- [x] if web3 proxy gets an http error back, retry another node
|
||||
- [x] endpoint for health checks. if no synced servers, give a 502 error
|
||||
- [x] rpc errors propagate too far. one subscription failing ends the app. isolate the providers more (might already be fixed)
|
||||
- [ ] incoming rate limiting (by ip)
|
||||
- [x] incoming rate limiting (by ip)
|
||||
- [ ] connection pool for redis
|
||||
- [ ] automatically route to archive server when necessary
|
||||
- [ ] handle log subscriptions
|
||||
- [ ] basic request method stats
|
||||
- [ ] http servers should check block at the very start
|
||||
- [ ] Got warning: "WARN subscribe_new_heads:send_block: web3_proxy::connection: unable to get block from https://rpc.ethermine.org: Deserialization Error: expected value at line 1 column 1. Response: error code: 1015". this is cloudflare rate limiting on fetching a block, but this is a private rpc. why is there a block subscription?
|
||||
|
||||
## V1
|
||||
|
||||
@ -43,8 +48,8 @@
|
||||
- create the app without applying any config to it
|
||||
- have a blocking future watching the config file and calling app.apply_config() on first load and on change
|
||||
- work started on this in the "config_reloads" branch. because of how we pass channels around during spawn, this requires a larger refactor.
|
||||
- [ ] interval for http subscriptions should be based on block time. load from config is easy, but
|
||||
- [ ] some things that are cached locally should probably be in shared redis caches
|
||||
- [ ] interval for http subscriptions should be based on block time. load from config is easy, but better to query
|
||||
- [ ] most things that are cached locally should probably be in shared redis caches
|
||||
- [ ] stats when forks are resolved (and what chain they were on?)
|
||||
- [ ] incoming rate limiting (by api key)
|
||||
- [ ] failsafe. if no blocks or transactions in the last second, warn and reset the connection
|
||||
|
@ -1,5 +1,6 @@
|
||||
[shared]
|
||||
chain_id = 1
|
||||
public_rate_limit_per_minute = 60_000
|
||||
|
||||
[balanced_rpcs]
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
[shared]
|
||||
chain_id = 1
|
||||
# in prod, do `rate_limit_redis = "redis://redis:6379/"`
|
||||
rate_limit_redis = "redis://dev-redis:6379/"
|
||||
public_rate_limit_per_minute = 60_000
|
||||
|
||||
[balanced_rpcs]
|
||||
|
||||
|
@ -6,4 +6,4 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.58"
|
||||
redis = { version = "0.21.5", features = ["aio", "tokio", "tokio-comp"] }
|
||||
bb8-redis = "0.11.0"
|
||||
|
@ -1,85 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use redis::aio::MultiplexedConnection;
|
||||
|
||||
// TODO: take this as an argument to open?
|
||||
const KEY_PREFIX: &str = "rate-limit";
|
||||
|
||||
pub struct RedisCellClient {
|
||||
conn: MultiplexedConnection,
|
||||
key: String,
|
||||
max_burst: u32,
|
||||
count_per_period: u32,
|
||||
period: u32,
|
||||
}
|
||||
|
||||
impl RedisCellClient {
|
||||
// todo: seems like this could be derived
|
||||
// TODO: take something generic for conn
|
||||
// TODO: use r2d2 for connection pooling?
|
||||
pub fn new(
|
||||
conn: MultiplexedConnection,
|
||||
key: String,
|
||||
max_burst: u32,
|
||||
count_per_period: u32,
|
||||
period: u32,
|
||||
) -> Self {
|
||||
let key = format!("{}:{}", KEY_PREFIX, key);
|
||||
|
||||
Self {
|
||||
conn,
|
||||
key,
|
||||
max_burst,
|
||||
count_per_period,
|
||||
period,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn throttle(&self) -> Result<(), Duration> {
|
||||
self.throttle_quantity(1).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn throttle_quantity(&self, quantity: u32) -> Result<(), Duration> {
|
||||
/*
|
||||
https://github.com/brandur/redis-cell#response
|
||||
|
||||
CL.THROTTLE <key> <max_burst> <count per period> <period> [<quantity>]
|
||||
|
||||
0. Whether the action was limited:
|
||||
0 indicates the action is allowed.
|
||||
1 indicates that the action was limited/blocked.
|
||||
1. The total limit of the key (max_burst + 1). This is equivalent to the common X-RateLimit-Limit HTTP header.
|
||||
2. The remaining limit of the key. Equivalent to X-RateLimit-Remaining.
|
||||
3. The number of seconds until the user should retry, and always -1 if the action was allowed. Equivalent to Retry-After.
|
||||
4. The number of seconds until the limit will reset to its maximum capacity. Equivalent to X-RateLimit-Reset.
|
||||
*/
|
||||
// TODO: don't unwrap. maybe return anyhow::Result<Result<(), Duration>>
|
||||
// TODO: should we return more error info?
|
||||
let x: Vec<isize> = redis::cmd("CL.THROTTLE")
|
||||
.arg(&(
|
||||
&self.key,
|
||||
self.max_burst,
|
||||
self.count_per_period,
|
||||
self.period,
|
||||
quantity,
|
||||
))
|
||||
.query_async(&mut self.conn.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(x.len(), 5);
|
||||
|
||||
// TODO: trace log the result
|
||||
|
||||
// TODO: maybe we should do #4
|
||||
let retry_after = *x.get(3).unwrap();
|
||||
|
||||
if retry_after == -1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Duration::from_secs(retry_after as u64))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,102 @@
|
||||
mod client;
|
||||
use bb8_redis::redis::cmd;
|
||||
|
||||
pub use client::RedisCellClient;
|
||||
pub use redis;
|
||||
// TODO: don't hard code MultiplexedConnection
|
||||
pub use redis::aio::MultiplexedConnection;
|
||||
pub use redis::Client;
|
||||
pub use bb8_redis::{bb8, RedisConnectionManager};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
// TODO: take this as an argument to open?
|
||||
const KEY_PREFIX: &str = "rate-limit";
|
||||
|
||||
pub type RedisClientPool = bb8::Pool<RedisConnectionManager>;
|
||||
|
||||
pub struct RedisCellClient {
|
||||
pool: RedisClientPool,
|
||||
key: String,
|
||||
max_burst: u32,
|
||||
count_per_period: u32,
|
||||
period: u32,
|
||||
}
|
||||
|
||||
impl RedisCellClient {
|
||||
// todo: seems like this could be derived
|
||||
// TODO: take something generic for conn
|
||||
// TODO: use r2d2 for connection pooling?
|
||||
pub fn new(
|
||||
pool: bb8::Pool<RedisConnectionManager>,
|
||||
default_key: String,
|
||||
max_burst: u32,
|
||||
count_per_period: u32,
|
||||
period: u32,
|
||||
) -> Self {
|
||||
let default_key = format!("{}:{}", KEY_PREFIX, default_key);
|
||||
|
||||
Self {
|
||||
pool,
|
||||
key: default_key,
|
||||
max_burst,
|
||||
count_per_period,
|
||||
period,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _throttle(&self, key: &str, quantity: u32) -> Result<(), Duration> {
|
||||
let mut conn = self.pool.get().await.unwrap();
|
||||
|
||||
/*
|
||||
https://github.com/brandur/redis-cell#response
|
||||
|
||||
CL.THROTTLE <key> <max_burst> <count per period> <period> [<quantity>]
|
||||
|
||||
0. Whether the action was limited:
|
||||
0 indicates the action is allowed.
|
||||
1 indicates that the action was limited/blocked.
|
||||
1. The total limit of the key (max_burst + 1). This is equivalent to the common X-RateLimit-Limit HTTP header.
|
||||
2. The remaining limit of the key. Equivalent to X-RateLimit-Remaining.
|
||||
3. The number of seconds until the user should retry, and always -1 if the action was allowed. Equivalent to Retry-After.
|
||||
4. The number of seconds until the limit will reset to its maximum capacity. Equivalent to X-RateLimit-Reset.
|
||||
*/
|
||||
// TODO: don't unwrap. maybe return anyhow::Result<Result<(), Duration>>
|
||||
// TODO: should we return more error info?
|
||||
let x: Vec<isize> = cmd("CL.THROTTLE")
|
||||
.arg(&(
|
||||
key,
|
||||
self.max_burst,
|
||||
self.count_per_period,
|
||||
self.period,
|
||||
quantity,
|
||||
))
|
||||
.query_async(&mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(x.len(), 5);
|
||||
|
||||
// TODO: trace log the result
|
||||
|
||||
let retry_after = *x.get(3).unwrap();
|
||||
|
||||
if retry_after == -1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Duration::from_secs(retry_after as u64))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn throttle(&self) -> Result<(), Duration> {
|
||||
self._throttle(&self.key, 1).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn throttle_key(&self, key: &str) -> Result<(), Duration> {
|
||||
let key = format!("{}:{}", KEY_PREFIX, key);
|
||||
|
||||
self._throttle(key.as_ref(), 1).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn throttle_quantity(&self, quantity: u32) -> Result<(), Duration> {
|
||||
self._throttle(&self.key, quantity).await
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ anyhow = { version = "1.0.58", features = ["backtrace"] }
|
||||
arc-swap = "1.5.0"
|
||||
argh = "0.1.8"
|
||||
axum = { version = "0.5.11", features = ["serde_json", "tokio-tungstenite", "ws"] }
|
||||
axum-client-ip = "0.2.0"
|
||||
counter = "0.5.5"
|
||||
dashmap = "5.3.4"
|
||||
derive_more = "0.99.17"
|
||||
|
@ -10,6 +10,7 @@ use futures::stream::StreamExt;
|
||||
use futures::Future;
|
||||
use linkedhashmap::LinkedHashMap;
|
||||
use parking_lot::RwLock;
|
||||
use redis_cell_client::{bb8, RedisCellClient, RedisClientPool, RedisConnectionManager};
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
@ -20,7 +21,7 @@ use tokio::sync::{broadcast, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
use tokio_stream::wrappers::{BroadcastStream, WatchStream};
|
||||
use tracing::{debug, info, info_span, instrument, trace, warn, Instrument};
|
||||
use tracing::{info, info_span, instrument, trace, warn, Instrument};
|
||||
|
||||
use crate::config::Web3ConnectionConfig;
|
||||
use crate::connections::Web3Connections;
|
||||
@ -94,6 +95,7 @@ pub struct Web3ProxyApp {
|
||||
head_block_receiver: watch::Receiver<Block<TxHash>>,
|
||||
pending_tx_sender: broadcast::Sender<TxState>,
|
||||
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
||||
public_rate_limiter: Option<RedisCellClient>,
|
||||
next_subscription_id: AtomicUsize,
|
||||
}
|
||||
|
||||
@ -109,11 +111,16 @@ impl Web3ProxyApp {
|
||||
&self.pending_transactions
|
||||
}
|
||||
|
||||
pub fn get_public_rate_limiter(&self) -> Option<&RedisCellClient> {
|
||||
self.public_rate_limiter.as_ref()
|
||||
}
|
||||
|
||||
pub async fn spawn(
|
||||
chain_id: usize,
|
||||
redis_address: Option<String>,
|
||||
balanced_rpcs: Vec<Web3ConnectionConfig>,
|
||||
private_rpcs: Vec<Web3ConnectionConfig>,
|
||||
public_rate_limit_per_minute: u32,
|
||||
) -> anyhow::Result<(
|
||||
Arc<Web3ProxyApp>,
|
||||
Pin<Box<dyn Future<Output = anyhow::Result<()>>>>,
|
||||
@ -132,15 +139,14 @@ impl Web3ProxyApp {
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let rate_limiter = match redis_address {
|
||||
let rate_limiter_pool = match redis_address {
|
||||
Some(redis_address) => {
|
||||
info!("Connecting to redis on {}", redis_address);
|
||||
let redis_client = redis_cell_client::Client::open(redis_address)?;
|
||||
|
||||
// TODO: r2d2 connection pool?
|
||||
let redis_conn = redis_client.get_multiplexed_tokio_connection().await?;
|
||||
let manager = RedisConnectionManager::new(redis_address)?;
|
||||
let pool = bb8::Pool::builder().build(manager).await?;
|
||||
|
||||
Some(redis_conn)
|
||||
Some(pool)
|
||||
}
|
||||
None => {
|
||||
info!("No redis address");
|
||||
@ -164,7 +170,7 @@ impl Web3ProxyApp {
|
||||
chain_id,
|
||||
balanced_rpcs,
|
||||
http_client.as_ref(),
|
||||
rate_limiter.as_ref(),
|
||||
rate_limiter_pool.as_ref(),
|
||||
Some(head_block_sender),
|
||||
Some(pending_tx_sender.clone()),
|
||||
pending_transactions.clone(),
|
||||
@ -182,7 +188,7 @@ impl Web3ProxyApp {
|
||||
chain_id,
|
||||
private_rpcs,
|
||||
http_client.as_ref(),
|
||||
rate_limiter.as_ref(),
|
||||
rate_limiter_pool.as_ref(),
|
||||
// subscribing to new heads here won't work well
|
||||
None,
|
||||
// TODO: subscribe to pending transactions on the private rpcs?
|
||||
@ -199,7 +205,20 @@ impl Web3ProxyApp {
|
||||
// TODO: use this? it could listen for confirmed transactions and then clear pending_transactions, but the head_block_sender is doing that
|
||||
drop(pending_tx_receiver);
|
||||
|
||||
let app = Web3ProxyApp {
|
||||
// TODO: how much should we allow?
|
||||
let public_max_burst = public_rate_limit_per_minute / 3;
|
||||
|
||||
let public_rate_limiter = rate_limiter_pool.as_ref().map(|redis_client_pool| {
|
||||
RedisCellClient::new(
|
||||
redis_client_pool.clone(),
|
||||
"public".to_string(),
|
||||
public_max_burst,
|
||||
public_rate_limit_per_minute,
|
||||
60,
|
||||
)
|
||||
});
|
||||
|
||||
let app = Self {
|
||||
balanced_rpcs,
|
||||
private_rpcs,
|
||||
incoming_requests: Default::default(),
|
||||
@ -207,6 +226,7 @@ impl Web3ProxyApp {
|
||||
head_block_receiver,
|
||||
pending_tx_sender,
|
||||
pending_transactions,
|
||||
public_rate_limiter,
|
||||
next_subscription_id: 1.into(),
|
||||
};
|
||||
|
||||
@ -431,7 +451,7 @@ impl Web3ProxyApp {
|
||||
request: JsonRpcRequestEnum,
|
||||
) -> anyhow::Result<JsonRpcForwardedResponseEnum> {
|
||||
// TODO: i don't always see this in the logs. why?
|
||||
debug!("Received request: {:?}", request);
|
||||
trace!("Received request: {:?}", request);
|
||||
|
||||
// even though we have timeouts on the requests to our backend providers,
|
||||
// we need a timeout for the incoming request so that delays from
|
||||
@ -447,7 +467,7 @@ impl Web3ProxyApp {
|
||||
};
|
||||
|
||||
// TODO: i don't always see this in the logs. why?
|
||||
debug!("Forwarding response: {:?}", response);
|
||||
trace!("Forwarding response: {:?}", response);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
@ -40,6 +40,9 @@ pub struct RpcSharedConfig {
|
||||
/// TODO: what type for chain_id? TODO: this isn't at the right level. this is inside a "Config"
|
||||
pub chain_id: usize,
|
||||
pub rate_limit_redis: Option<String>,
|
||||
// TODO: serde default for development?
|
||||
// TODO: allow no limit?
|
||||
pub public_rate_limit_per_minute: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -71,6 +74,7 @@ impl RpcConfig {
|
||||
self.shared.rate_limit_redis,
|
||||
balanced_rpcs,
|
||||
private_rpcs,
|
||||
self.shared.public_rate_limit_per_minute,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -81,14 +85,14 @@ impl Web3ConnectionConfig {
|
||||
// #[instrument(name = "try_build_Web3ConnectionConfig", skip_all)]
|
||||
pub async fn spawn(
|
||||
self,
|
||||
rate_limiter: Option<&redis_cell_client::MultiplexedConnection>,
|
||||
redis_client_pool: Option<&redis_cell_client::RedisClientPool>,
|
||||
chain_id: usize,
|
||||
http_client: Option<&reqwest::Client>,
|
||||
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
||||
block_sender: Option<flume::Sender<(Block<TxHash>, Arc<Web3Connection>)>>,
|
||||
tx_id_sender: Option<flume::Sender<(TxHash, Arc<Web3Connection>)>>,
|
||||
) -> anyhow::Result<(Arc<Web3Connection>, AnyhowJoinHandle<()>)> {
|
||||
let hard_rate_limit = self.hard_limit.map(|x| (x, rate_limiter.unwrap()));
|
||||
let hard_rate_limit = self.hard_limit.map(|x| (x, redis_client_pool.unwrap()));
|
||||
|
||||
Web3Connection::spawn(
|
||||
chain_id,
|
||||
|
@ -131,7 +131,7 @@ impl Web3Connection {
|
||||
// optional because this is only used for http providers. websocket providers don't use it
|
||||
http_client: Option<&reqwest::Client>,
|
||||
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
||||
hard_limit: Option<(u32, &redis_cell_client::MultiplexedConnection)>,
|
||||
hard_limit: Option<(u32, &redis_cell_client::RedisClientPool)>,
|
||||
// TODO: think more about this type
|
||||
soft_limit: u32,
|
||||
block_sender: Option<flume::Sender<(Block<TxHash>, Arc<Self>)>>,
|
||||
@ -356,10 +356,6 @@ impl Web3Connection {
|
||||
let mut last_hash = Default::default();
|
||||
|
||||
loop {
|
||||
// wait for the interval
|
||||
// TODO: if error or rate limit, increase interval?
|
||||
http_interval_receiver.recv().await.unwrap();
|
||||
|
||||
match self.try_request_handle().await {
|
||||
Ok(active_request_handle) => {
|
||||
// TODO: i feel like this should be easier. there is a provider.getBlock, but i don't know how to give it "latest"
|
||||
@ -384,6 +380,10 @@ impl Web3Connection {
|
||||
warn!("Failed getting latest block from {}: {:?}", self, e);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for the interval
|
||||
// TODO: if error or rate limit, increase interval?
|
||||
http_interval_receiver.recv().await.unwrap();
|
||||
}
|
||||
}
|
||||
Web3Provider::Ws(provider) => {
|
||||
@ -447,10 +447,6 @@ impl Web3Connection {
|
||||
// TODO: create a filter
|
||||
|
||||
loop {
|
||||
// wait for the interval
|
||||
// TODO: if error or rate limit, increase interval?
|
||||
interval.tick().await;
|
||||
|
||||
// TODO: actually do something here
|
||||
/*
|
||||
match self.try_request_handle().await {
|
||||
@ -463,6 +459,10 @@ impl Web3Connection {
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// wait for the interval
|
||||
// TODO: if error or rate limit, increase interval?
|
||||
interval.tick().await;
|
||||
}
|
||||
}
|
||||
Web3Provider::Ws(provider) => {
|
||||
|
@ -89,7 +89,7 @@ impl Web3Connections {
|
||||
chain_id: usize,
|
||||
server_configs: Vec<Web3ConnectionConfig>,
|
||||
http_client: Option<&reqwest::Client>,
|
||||
rate_limiter: Option<&redis_cell_client::MultiplexedConnection>,
|
||||
redis_client_pool: Option<&redis_cell_client::RedisClientPool>,
|
||||
head_block_sender: Option<watch::Sender<Block<TxHash>>>,
|
||||
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
||||
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
||||
@ -141,7 +141,7 @@ impl Web3Connections {
|
||||
for server_config in server_configs.into_iter() {
|
||||
match server_config
|
||||
.spawn(
|
||||
rate_limiter,
|
||||
redis_client_pool,
|
||||
chain_id,
|
||||
http_client,
|
||||
http_interval_sender.clone(),
|
||||
|
@ -7,15 +7,15 @@ use crate::jsonrpc::JsonRpcForwardedResponse;
|
||||
pub async fn handler_404() -> impl IntoResponse {
|
||||
let err = anyhow::anyhow!("nothing to see here");
|
||||
|
||||
handle_anyhow_error(err, Some(StatusCode::NOT_FOUND)).await
|
||||
handle_anyhow_error(Some(StatusCode::NOT_FOUND), err).await
|
||||
}
|
||||
|
||||
/// handle errors by converting them into something that implements `IntoResponse`
|
||||
/// TODO: use this. i can't get https://docs.rs/axum/latest/axum/error_handling/index.html to work
|
||||
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
|
||||
pub async fn handle_anyhow_error(
|
||||
err: anyhow::Error,
|
||||
code: Option<StatusCode>,
|
||||
err: anyhow::Error,
|
||||
) -> impl IntoResponse {
|
||||
let id = RawValue::from_string("null".to_string()).unwrap();
|
||||
|
||||
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use crate::app::Web3ProxyApp;
|
||||
|
||||
/// Health check page for load balancers to use
|
||||
pub async fn health(app: Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
pub async fn health(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
if app.get_balanced_rpcs().has_synced_rpcs() {
|
||||
(StatusCode::OK, "OK")
|
||||
} else {
|
||||
@ -14,7 +14,7 @@ pub async fn health(app: Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
/// Very basic status page
|
||||
pub async fn status(app: Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
pub async fn status(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
|
||||
// TODO: what else should we include? uptime? prometheus?
|
||||
let balanced_rpcs = app.get_balanced_rpcs();
|
||||
let private_rpcs = app.get_private_rpcs();
|
||||
|
@ -1,15 +1,34 @@
|
||||
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
|
||||
use axum_client_ip::ClientIp;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::errors::handle_anyhow_error;
|
||||
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
||||
|
||||
pub async fn proxy_web3_rpc(
|
||||
payload: Json<JsonRpcRequestEnum>,
|
||||
app: Extension<Arc<Web3ProxyApp>>,
|
||||
Json(payload): Json<JsonRpcRequestEnum>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
) -> impl IntoResponse {
|
||||
match app.proxy_web3_rpc(payload.0).await {
|
||||
if let Some(rate_limiter) = app.get_public_rate_limiter() {
|
||||
let rate_limiter_key = format!("{}", ip);
|
||||
|
||||
if rate_limiter.throttle_key(&rate_limiter_key).await.is_err() {
|
||||
// TODO: set headers so they know when they can retry
|
||||
// warn!(?ip, "public rate limit exceeded");
|
||||
return handle_anyhow_error(
|
||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
||||
anyhow::anyhow!("too many requests"),
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
}
|
||||
} else {
|
||||
// TODO: if no redis, rate limit with a local cache?
|
||||
}
|
||||
|
||||
match app.proxy_web3_rpc(payload).await {
|
||||
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
||||
Err(err) => handle_anyhow_error(err, None).await.into_response(),
|
||||
Err(err) => handle_anyhow_error(None, err).await.into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ mod errors;
|
||||
mod http;
|
||||
mod http_proxy;
|
||||
mod ws_proxy;
|
||||
|
||||
use axum::{
|
||||
handler::Handler,
|
||||
routing::{get, post},
|
||||
@ -35,8 +36,9 @@ pub async fn run(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()>
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
debug!("listening on port {}", port);
|
||||
// TODO: into_make_service is enough if we always run behind a proxy. make into_make_service_with_connect_info optional?
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use hashbrown::HashMap;
|
||||
use serde_json::value::RawValue;
|
||||
use std::str::from_utf8_mut;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
use crate::{
|
||||
app::Web3ProxyApp,
|
||||
@ -20,13 +20,13 @@ use crate::{
|
||||
};
|
||||
|
||||
pub async fn websocket_handler(
|
||||
app: Extension<Arc<Web3ProxyApp>>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| proxy_web3_socket(app, socket))
|
||||
}
|
||||
|
||||
async fn proxy_web3_socket(app: Extension<Arc<Web3ProxyApp>>, socket: WebSocket) {
|
||||
async fn proxy_web3_socket(app: Arc<Web3ProxyApp>, socket: WebSocket) {
|
||||
// split the websocket so we can read and write concurrently
|
||||
let (ws_tx, ws_rx) = socket.split();
|
||||
|
||||
@ -109,7 +109,7 @@ async fn handle_socket_payload(
|
||||
}
|
||||
|
||||
async fn read_web3_socket(
|
||||
app: Extension<Arc<Web3ProxyApp>>,
|
||||
app: Arc<Web3ProxyApp>,
|
||||
mut ws_rx: SplitStream<WebSocket>,
|
||||
response_tx: flume::Sender<Message>,
|
||||
) {
|
||||
@ -119,12 +119,11 @@ async fn read_web3_socket(
|
||||
// new message from our client. forward to a backend and then send it through response_tx
|
||||
let response_msg = match msg {
|
||||
Message::Text(payload) => {
|
||||
handle_socket_payload(app.0.clone(), &payload, &response_tx, &mut subscriptions)
|
||||
.await
|
||||
handle_socket_payload(app.clone(), &payload, &response_tx, &mut subscriptions).await
|
||||
}
|
||||
Message::Ping(x) => Message::Pong(x),
|
||||
Message::Pong(x) => {
|
||||
debug!("pong: {:?}", x);
|
||||
trace!("pong: {:?}", x);
|
||||
continue;
|
||||
}
|
||||
Message::Close(_) => {
|
||||
@ -134,8 +133,7 @@ async fn read_web3_socket(
|
||||
Message::Binary(mut payload) => {
|
||||
let payload = from_utf8_mut(&mut payload).unwrap();
|
||||
|
||||
handle_socket_payload(app.0.clone(), payload, &response_tx, &mut subscriptions)
|
||||
.await
|
||||
handle_socket_payload(app.clone(), payload, &response_tx, &mut subscriptions).await
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user