From 4f07556cdcef09dd31a6f031c412b5e15c03cf3c Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Tue, 22 Feb 2022 15:21:36 +0100 Subject: [PATCH 1/2] feat: cached provider --- Cargo.lock | 96 ++++++++++++++++++++++++++++--- ethers-providers/Cargo.toml | 1 + ethers-providers/src/cache.rs | 98 ++++++++++++++++++++++++++++++++ ethers-providers/src/lib.rs | 2 + ethers-providers/src/provider.rs | 34 ++++++++++- 5 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 ethers-providers/src/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 7b397336c..b4eb0adce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,6 +873,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dashmap" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0834a35a3fce649144119e18da2a4d8ed12ef3862f47183fd46f625d072d96c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", + "parking_lot 0.12.0", + "rayon", + "serde", +] + [[package]] name = "dbl" version = "0.3.2" @@ -1313,6 +1326,7 @@ dependencies = [ "auto_impl", "base64 0.13.0", "bytes", + "dashmap", "ethers-core", "futures-channel", "futures-core", @@ -1320,7 +1334,7 @@ dependencies = [ "futures-util", "hex", "http", - "parking_lot", + "parking_lot 0.11.2", "pin-project", "reqwest", "serde", @@ -2070,9 +2084,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -2415,7 +2429,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.1", ] [[package]] @@ -2432,6 +2456,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "password-hash" version = "0.2.3" @@ -3337,7 +3374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bcc41d18f7a1d50525d080fd3e953be87c4f9f1a974f3c21798ca00d54ec15" dependencies = [ "lazy_static", - "parking_lot", + "parking_lot 0.11.2", "serial_test_derive", ] @@ -3555,7 +3592,7 @@ checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" dependencies = [ "lazy_static", "new_debug_unreachable", - "parking_lot", + "parking_lot 0.11.2", "phf_shared 0.8.0", "precomputed-hash", ] @@ -3774,7 +3811,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.11.2", "pin-project-lite", "signal-hook-registry", "tokio-macros", @@ -4225,7 +4262,7 @@ checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures", "js-sys", - "parking_lot", + "parking_lot 0.11.2", "pin-utils", "wasm-bindgen", "wasm-bindgen-futures", @@ -4304,6 +4341,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "winreg" version = "0.7.0" diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index f119895e9..c9c1bdf55 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -39,6 +39,7 @@ tracing = { version = "0.1.31", default-features = false } tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] } bytes = { version = "1.1.0", default-features = false, optional = true } +dashmap = { version = "5.1.0", features = ["serde", "rayon"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # tokio diff --git a/ethers-providers/src/cache.rs b/ethers-providers/src/cache.rs new file mode 100644 index 000000000..c7499bf5f --- /dev/null +++ b/ethers-providers/src/cache.rs @@ -0,0 +1,98 @@ +use crate::ProviderError; +use dashmap::DashMap; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + fs::File, + io::{BufReader, BufWriter}, + path::PathBuf, +}; + +#[derive(Clone, Debug, Default)] +/// Simple in-memory K-V cache using concurrent dashmap which flushes +/// its state to disk on `Drop`. +pub struct Cache { + path: PathBuf, + // serialized request / response pair + requests: DashMap, +} + +// Helper type for (de)serialization +#[derive(Serialize, Deserialize)] +struct CachedRequest<'a, T> { + method: &'a str, + params: T, +} + +impl Cache { + /// Instantiates a new cache at a file path. + pub fn new(path: PathBuf) -> Result { + // try to read the already existing requests + let reader = + BufReader::new(File::options().write(true).read(true).create(true).open(&path)?); + let requests = serde_json::from_reader(reader).unwrap_or_default(); + Ok(Self { path, requests }) + } + + pub fn get( + &self, + method: &str, + params: &T, + ) -> Result, ProviderError> { + let key = serde_json::to_string(&CachedRequest { method, params })?; + let value = self.requests.get(&key); + value.map(|x| serde_json::from_str(&x).map_err(ProviderError::SerdeJson)).transpose() + } + + pub fn set( + &self, + method: &str, + params: T, + response: R, + ) -> Result<(), ProviderError> { + let key = serde_json::to_string(&CachedRequest { method, params })?; + let value = serde_json::to_string(&response)?; + self.requests.insert(key, value); + Ok(()) + } +} + +impl Drop for Cache { + fn drop(&mut self) { + let file = match File::options().write(true).read(true).create(true).open(&self.path) { + Ok(inner) => BufWriter::new(inner), + Err(err) => { + tracing::error!("could not open cache file {}", err); + return + } + }; + + // overwrite the cache + if let Err(err) = serde_json::to_writer(file, &self.requests) { + tracing::error!("could not write to cache file {}", err); + }; + } +} + +#[cfg(test)] +mod tests { + use crate::{Middleware, Provider}; + use ethers_core::types::{Address, U256}; + + #[tokio::test] + async fn test_cache() { + let tmp = tempfile::tempdir().unwrap(); + let cache = tmp.path().join("cache"); + let (provider, mock) = Provider::mocked(); + let provider = provider.with_cache(cache.clone()); + let addr = Address::random(); + + assert!(provider.cache().unwrap().requests.is_empty()); + + mock.push(U256::from(100u64)).unwrap(); + let res = provider.get_balance(addr, None).await.unwrap(); + assert_eq!(res, 100.into()); + + assert!(!provider.cache().unwrap().requests.is_empty()); + dbg!(&provider.cache()); + } +} diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index d169bcc22..2547d385b 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -8,6 +8,8 @@ pub use transports::*; mod provider; +mod cache; + // ENS support pub mod ens; diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index d2ddf2093..3b136aec7 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1,4 +1,5 @@ use crate::{ + cache::Cache, ens, erc, maybe, pubsub::{PubsubClient, SubscriptionStream}, stream::{FilterWatcher, DEFAULT_POLL_INTERVAL}, @@ -28,7 +29,7 @@ use thiserror::Error; use url::{ParseError, Url}; use futures_util::{lock::Mutex, try_join}; -use std::{convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration}; +use std::{convert::TryFrom, fmt::Debug, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use tracing::trace; use tracing_futures::Instrument; @@ -83,6 +84,7 @@ pub struct Provider

{ ens: Option

, interval: Option, from: Option
, + cache: Option, /// Node client hasn't been checked yet = `None` /// Unsupported node client = `Some(None)` /// Supported node client = `Some(Some(NodeClient))` @@ -132,6 +134,9 @@ pub enum ProviderError { #[error("Attempted to sign a transaction with no available signer. Hint: did you mean to use a SignerMiddleware?")] SignerUnavailable, + + #[error(transparent)] + Io(#[from] std::io::Error), } /// Types of filters supported by the JSON-RPC. @@ -157,9 +162,14 @@ impl Provider

{ interval: None, from: None, _node_client: Arc::new(Mutex::new(None)), + cache: None, } } + pub fn cache(&self) -> Option<&Cache> { + self.cache.as_ref() + } + /// Returns the type of node we're connected to, while also caching the value for use /// in other node-specific API calls, such as the get_block_receipts call. pub async fn node_client(&self) -> Result { @@ -193,9 +203,22 @@ impl Provider

{ tracing::trace_span!("rpc", method = method, params = ?serde_json::to_string(¶ms)?); // https://docs.rs/tracing/0.1.22/tracing/span/struct.Span.html#in-asynchronous-code let res = async move { + // if there's a cache hit, return it + if let Some(ref cache) = self.cache { + if let Some(res) = cache.get(method, ¶ms)? { + return Ok(res) + } + } + trace!("tx"); - let res: R = self.inner.request(method, params).await.map_err(Into::into)?; + let res: R = self.inner.request(method, ¶ms).await.map_err(Into::into)?; trace!(rx = ?serde_json::to_string(&res)?); + + // save the response if there was a cache set + if let Some(ref cache) = self.cache { + cache.set(method, params, &res)?; + } + Ok::<_, ProviderError>(res) } .instrument(span) @@ -1181,6 +1204,13 @@ impl Provider

{ self } + #[must_use] + /// Sets the provider's cache to avoid making redundant network requests. + pub fn with_cache(mut self, cache: PathBuf) -> Self { + self.cache = Some(Cache::new(cache).unwrap()); + self + } + /// Gets the polling interval which the provider currently uses for event filters /// and pending transactions (default: 7 seconds) pub fn get_interval(&self) -> Duration { From 2e05148ecd0b427bd5d27d6a623773a9e6b3b30c Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 24 Feb 2022 23:26:57 +0200 Subject: [PATCH 2/2] chore: silence compiler warning --- ethers-providers/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 2547d385b..a30ad5a38 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] #![allow(clippy::type_complexity)] #![doc = include_str!("../README.md")] mod transports;