From c25b20687d4415f8231e8ccc1b1adfb91858a63c Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:58:05 +0200 Subject: [PATCH 1/5] fix(sdk): GrpcContextProvider panics when switching sync-async --- packages/rs-sdk/src/mock/provider.rs | 86 +++++++++++++++---- packages/rs-sdk/src/sdk.rs | 13 +-- .../src/errors/consensus/consensus_error.rs | 6 ++ 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index aa5c6f1a666..63f55dd5ba3 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -1,12 +1,17 @@ //! Example ContextProvider that uses the Core gRPC API to fetch data from the platform. +use std::fmt::Debug; use std::hash::Hash; use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; use std::sync::Arc; +use std::thread::sleep; use dpp::prelude::{DataContract, Identifier}; use drive_proof_verifier::error::ContextProviderError; use drive_proof_verifier::ContextProvider; +use std::future::Future; +use tokio::sync::oneshot::error::TryRecvError; use crate::core_client::CoreClient; use crate::platform::Fetch; @@ -24,7 +29,7 @@ pub struct GrpcContextProvider { /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`. /// /// We use [Arc] as we have circular dependencies between Sdk and ContextProvider. - sdk: Option, + sdk: std::sync::RwLock>, /// Data contracts cache. /// @@ -65,7 +70,7 @@ impl GrpcContextProvider { let core_client = CoreClient::new(core_ip, core_port, core_user, core_password)?; Ok(Self { core: core_client, - sdk, + sdk: std::sync::RwLock::new(sdk), data_contracts_cache: Cache::new(data_contracts_cache_size), quorum_public_keys_cache: Cache::new(quorum_public_keys_cache_size), #[cfg(feature = "mocks")] @@ -78,8 +83,10 @@ impl GrpcContextProvider { /// /// Note that if the `sdk` is `None`, the context provider will not be able to fetch data itself and will rely on /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`. - pub fn set_sdk(&mut self, sdk: Option) { - self.sdk = sdk; + pub fn set_sdk(&self, sdk: Option) { + let mut guard = self.sdk.write().expect("lock poisoned"); + let item = guard.deref_mut(); + *item = sdk; } /// Set the directory where to store dumped data. /// @@ -157,8 +164,8 @@ impl ContextProvider for GrpcContextProvider { if let Some(contract) = self.data_contracts_cache.get(data_contract_id) { return Ok(Some(contract)); }; - - let sdk = match &self.sdk { + let sdk_guard = self.sdk.read().expect("lock poisoned"); + let sdk = match &sdk_guard.deref() { Some(sdk) => sdk, None => { tracing::warn!("data contract cache miss and no sdk provided, skipping fetch"); @@ -166,20 +173,14 @@ impl ContextProvider for GrpcContextProvider { } }; - let handle = match tokio::runtime::Handle::try_current() { - Ok(handle) => handle, - // not an error, we rely on the caller to provide a data contract using - Err(e) => { - tracing::warn!( - error = e.to_string(), - "data contract cache miss and no tokio runtime detected, skipping fetch" - ); - return Ok(None); - } - }; + let contract_id = *data_contract_id; + let sdk_cloned = sdk.clone(); - let data_contract = handle - .block_on(DataContract::fetch(sdk, *data_contract_id)) + let data_contract: Option = + spawn_async::, crate::error::Error>, _, _>( + move || async move { DataContract::fetch(&sdk_cloned, contract_id).await }, + ) + .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))? .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))?; if let Some(ref dc) = data_contract { @@ -190,6 +191,53 @@ impl ContextProvider for GrpcContextProvider { } } +// spawn_async is a workaorund to connect sync and async code, as block_on() panics when in async context +fn spawn_async(f: F) -> Result +where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future + Send + 'static, + R: Send + Sync + 'static + Debug, +{ + let tokio_rt = match tokio::runtime::Handle::try_current() { + Ok(handle) => handle, + // not an error, we rely on the caller to provide a data contract using + Err(e) => { + tracing::warn!( + error = e.to_string(), + "data contract cache miss and no tokio runtime detected, skipping fetch" + ); + return Err(Error::ContextProviderError(ContextProviderError::Generic( + e.to_string(), + ))); + } + }; + + let (send, mut recv) = tokio::sync::oneshot::channel(); + + let _join_handle = tokio_rt.spawn(async move { + let result = f().await; + send.send(result).expect("send result"); + }); + + loop { + match recv.try_recv() { + Ok(result) => return Ok(result), + Err(TryRecvError::Empty) => { + tracing::trace!("waiting for data contract to be fetched"); + // FIXME this is just a workaround to connect sync and async code + sleep(std::time::Duration::from_millis(10)); + + continue; + } + Err(e) => { + return Err(Error::ContextProviderError( + ContextProviderError::InvalidDataContract(e.to_string()), + )); + } + } + } +} + /// Thread-safe cache of various objects inside the SDK. /// /// This is used to cache objects that are expensive to fetch from the platform, like data contracts. diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index bf3618fa754..12f1ddbf2e6 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -707,15 +707,18 @@ impl SdkBuilder { tracing::warn!("ContextProvider not set; mocking with Dash Core. \ Please provide your own ContextProvider with SdkBuilder::with_context_provider()."); - let mut context_provider = GrpcContextProvider::new(Some(sdk.clone()), - &self.core_ip, self.core_port, &self.core_user, &self.core_password, - self.data_contract_cache_size, self.quorum_public_keys_cache_size)?; + let mut context_provider = GrpcContextProvider::new(None, + &self.core_ip, self.core_port, &self.core_user, &self.core_password, + self.data_contract_cache_size, self.quorum_public_keys_cache_size)?; #[cfg(feature = "mocks")] if sdk.dump_dir.is_some() { context_provider.set_dump_dir(sdk.dump_dir.clone()); } - - sdk.context_provider.replace(Arc::new(Box::new(context_provider))); + // We have cyclical dependency Sdk <-> GrpcContextProvider, so we just do some + // workaround using additional Arc. + let context_provider=Arc::new(context_provider); + sdk.context_provider.replace(Arc::new(Box::new(context_provider.clone()))); + context_provider.set_sdk(Some(sdk.clone())); } else{ tracing::warn!( "Configure ContextProvider with Sdk::with_context_provider(); otherwise Sdk will fail"); diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 8a8b1b9b894..b1e27fad740 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -149,6 +149,12 @@ pub fn from_consensus_error_ref(e: &DPPConsensusError) -> JsValue { DPPConsensusError::StateError(state_error) => from_state_error(state_error), DPPConsensusError::BasicError(basic_error) => from_basic_error(basic_error), DPPConsensusError::DefaultError => JsError::new("DefaultError").into(), + #[cfg(test)] + e => todo!( + "ConsensusError {} not implemented: {}", + std::any::type_name_of_val(e), + e + ), } } From 923b924eedb38aa50465319a006228526f0bd53d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:19:54 +0200 Subject: [PATCH 2/5] refactor(sdk): cpu-friendly pool in mock provider --- packages/rs-sdk/src/mock/provider.rs | 111 ++++++++++++++------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index 63f55dd5ba3..1a316ddeeec 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -1,21 +1,19 @@ //! Example ContextProvider that uses the Core gRPC API to fetch data from the platform. -use std::fmt::Debug; -use std::hash::Hash; -use std::num::NonZeroUsize; -use std::ops::{Deref, DerefMut}; -use std::sync::Arc; -use std::thread::sleep; - +use crate::core_client::CoreClient; +use crate::platform::Fetch; +use crate::{Error, Sdk}; use dpp::prelude::{DataContract, Identifier}; use drive_proof_verifier::error::ContextProviderError; use drive_proof_verifier::ContextProvider; +use futures::task::ArcWake; +use futures::FutureExt; use std::future::Future; -use tokio::sync::oneshot::error::TryRecvError; - -use crate::core_client::CoreClient; -use crate::platform::Fetch; -use crate::{Error, Sdk}; +use std::hash::Hash; +use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; +use std::task::{Context, Poll}; /// Context provider that uses the Core gRPC API to fetch data from the platform. /// @@ -177,11 +175,8 @@ impl ContextProvider for GrpcContextProvider { let sdk_cloned = sdk.clone(); let data_contract: Option = - spawn_async::, crate::error::Error>, _, _>( - move || async move { DataContract::fetch(&sdk_cloned, contract_id).await }, - ) - .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))? - .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))?; + poll_future(DataContract::fetch(&sdk_cloned, contract_id)) + .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))?; if let Some(ref dc) = data_contract { self.data_contracts_cache.put(*data_contract_id, dc.clone()); @@ -191,49 +186,55 @@ impl ContextProvider for GrpcContextProvider { } } -// spawn_async is a workaorund to connect sync and async code, as block_on() panics when in async context -fn spawn_async(f: F) -> Result -where - F: FnOnce() -> Fut + Send + 'static, - Fut: Future + Send + 'static, - R: Send + Sync + 'static + Debug, -{ - let tokio_rt = match tokio::runtime::Handle::try_current() { - Ok(handle) => handle, - // not an error, we rely on the caller to provide a data contract using - Err(e) => { - tracing::warn!( - error = e.to_string(), - "data contract cache miss and no tokio runtime detected, skipping fetch" - ); - return Err(Error::ContextProviderError(ContextProviderError::Generic( - e.to_string(), - ))); +struct MtxWaker { + mtx: std::sync::Mutex, + cvar: std::sync::Condvar, +} + +impl MtxWaker { + fn new() -> Self { + Self { + mtx: std::sync::Mutex::new(false), + cvar: std::sync::Condvar::new(), + } + } + + fn wait(&self) { + let mut started = self.mtx.lock().expect("lock poisoned"); + while !*started { + started = self.cvar.wait(started).expect("wait failed"); } - }; + } +} - let (send, mut recv) = tokio::sync::oneshot::channel(); +impl ArcWake for MtxWaker { + fn wake_by_ref(arc_self: &Arc) { + let mut started = arc_self.mtx.lock().expect("lock poisoned"); + *started = true; + arc_self.cvar.notify_one(); + } +} + +/// Execute an async function in a sync context, when block_on() is not available. +/// +/// When async code runs some sync code inside a thread used to drive asynchronous tasks, +/// that sync code cannot call block_on(). This function is a workaround for this that allows to run async code in a +/// sync context. +/// +/// Note it uses a busy loop to poll the future, so it's not efficient and should be used only for testing purposes. +fn poll_future(future: F) -> F::Output { + let mtx_waker = Arc::new(MtxWaker::new()); + let waker = futures::task::waker(mtx_waker.clone()); + // let waker = futures::task::noop_waker(); + + let mut context = Context::from_waker(&waker); - let _join_handle = tokio_rt.spawn(async move { - let result = f().await; - send.send(result).expect("send result"); - }); + let mut future = Box::pin(future).fuse(); loop { - match recv.try_recv() { - Ok(result) => return Ok(result), - Err(TryRecvError::Empty) => { - tracing::trace!("waiting for data contract to be fetched"); - // FIXME this is just a workaround to connect sync and async code - sleep(std::time::Duration::from_millis(10)); - - continue; - } - Err(e) => { - return Err(Error::ContextProviderError( - ContextProviderError::InvalidDataContract(e.to_string()), - )); - } + match Future::poll(std::pin::Pin::new(&mut future), &mut context) { + Poll::Ready(value) => return value, + Poll::Pending => mtx_waker.wait(), } } } From 888ecef0c7b9425f811fd4b08005175aa58ff5db Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:37:58 +0200 Subject: [PATCH 3/5] chore(sdk): add some docs --- packages/rs-sdk/src/mock/provider.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index 1a316ddeeec..dd6cf3fbb5b 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -186,6 +186,9 @@ impl ContextProvider for GrpcContextProvider { } } +/// Waker that uses a mutex and a condition variable to wake up a task. +/// +/// See [Condvar] documentation for more details. struct MtxWaker { mtx: std::sync::Mutex, cvar: std::sync::Condvar, @@ -199,6 +202,7 @@ impl MtxWaker { } } + /// Block until the task is woken up. fn wait(&self) { let mut started = self.mtx.lock().expect("lock poisoned"); while !*started { @@ -221,12 +225,10 @@ impl ArcWake for MtxWaker { /// that sync code cannot call block_on(). This function is a workaround for this that allows to run async code in a /// sync context. /// -/// Note it uses a busy loop to poll the future, so it's not efficient and should be used only for testing purposes. +/// Note it's not as efficient as calling `.await` on a future, and should be used only for testing purposes. fn poll_future(future: F) -> F::Output { let mtx_waker = Arc::new(MtxWaker::new()); let waker = futures::task::waker(mtx_waker.clone()); - // let waker = futures::task::noop_waker(); - let mut context = Context::from_waker(&waker); let mut future = Box::pin(future).fuse(); From e34bd3b8c3aadf730f7bbb5b649a0cca9291d116 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:07:41 +0200 Subject: [PATCH 4/5] refactor(sdk): use arc-swap to store sdk in GrpcContextProvider --- Cargo.lock | 1 + packages/rs-sdk/Cargo.toml | 1 + packages/rs-sdk/src/mock/provider.rs | 16 ++++++++-------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c63f23809..efb0d42bb60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1307,6 +1307,7 @@ dependencies = [ name = "dash-sdk" version = "1.0.0-dev.15" dependencies = [ + "arc-swap", "async-trait", "base64 0.22.0", "bincode 2.0.0-rc.3", diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index e514de10925..9d576473b32 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -4,6 +4,7 @@ version = "1.0.0-dev.15" edition = "2021" [dependencies] +arc-swap = { version = "1.7.1" } dpp = { path = "../rs-dpp", default-features = false, features = [ "dash-sdk-features", ] } diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index dd6cf3fbb5b..c15b40d069f 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -3,6 +3,7 @@ use crate::core_client::CoreClient; use crate::platform::Fetch; use crate::{Error, Sdk}; +use arc_swap::ArcSwapAny; use dpp::prelude::{DataContract, Identifier}; use drive_proof_verifier::error::ContextProviderError; use drive_proof_verifier::ContextProvider; @@ -11,7 +12,6 @@ use futures::FutureExt; use std::future::Future; use std::hash::Hash; use std::num::NonZeroUsize; -use std::ops::{Deref, DerefMut}; use std::sync::Arc; use std::task::{Context, Poll}; @@ -27,7 +27,7 @@ pub struct GrpcContextProvider { /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`. /// /// We use [Arc] as we have circular dependencies between Sdk and ContextProvider. - sdk: std::sync::RwLock>, + sdk: ArcSwapAny>>, /// Data contracts cache. /// @@ -68,7 +68,7 @@ impl GrpcContextProvider { let core_client = CoreClient::new(core_ip, core_port, core_user, core_password)?; Ok(Self { core: core_client, - sdk: std::sync::RwLock::new(sdk), + sdk: ArcSwapAny::new(Arc::new(sdk)), data_contracts_cache: Cache::new(data_contracts_cache_size), quorum_public_keys_cache: Cache::new(quorum_public_keys_cache_size), #[cfg(feature = "mocks")] @@ -82,9 +82,7 @@ impl GrpcContextProvider { /// Note that if the `sdk` is `None`, the context provider will not be able to fetch data itself and will rely on /// values set by the user in the caches: `data_contracts_cache`, `quorum_public_keys_cache`. pub fn set_sdk(&self, sdk: Option) { - let mut guard = self.sdk.write().expect("lock poisoned"); - let item = guard.deref_mut(); - *item = sdk; + self.sdk.store(Arc::new(sdk)); } /// Set the directory where to store dumped data. /// @@ -162,8 +160,10 @@ impl ContextProvider for GrpcContextProvider { if let Some(contract) = self.data_contracts_cache.get(data_contract_id) { return Ok(Some(contract)); }; - let sdk_guard = self.sdk.read().expect("lock poisoned"); - let sdk = match &sdk_guard.deref() { + // let sdk_guard = self.sdk.read().expect("lock poisoned"); + let sdk_guard = self.sdk.load(); + + let sdk = match sdk_guard.as_ref() { Some(sdk) => sdk, None => { tracing::warn!("data contract cache miss and no sdk provided, skipping fetch"); From 9d2110fa68c36877d56b0739d6d32b53e07b6f26 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:20:55 +0200 Subject: [PATCH 5/5] refactor(sdk): replace own poller with pollster crate --- Cargo.lock | 7 +++ packages/rs-sdk/Cargo.toml | 2 +- packages/rs-sdk/src/mock/provider.rs | 66 ++-------------------------- 3 files changed, 12 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efb0d42bb60..30e74e18ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1328,6 +1328,7 @@ dependencies = [ "hex", "http 0.2.12", "lru", + "pollster", "rs-dapi-client", "sanitize-filename", "serde", @@ -3658,6 +3659,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "portable-atomic" version = "1.6.0" diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 9d576473b32..1c662f7be62 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -35,7 +35,7 @@ derive_more = { version = "0.99.17" } dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore-rpc", tag = "v0.15.2" } lru = { version = "0.12.3", optional = true } bip37-bloom-filter = { git = "https://github.com/dashpay/rs-bip37-bloom-filter", branch = "develop" } - +pollster = { version = "0.3.0" } [dev-dependencies] tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index c15b40d069f..0efe92986d5 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -7,13 +7,10 @@ use arc_swap::ArcSwapAny; use dpp::prelude::{DataContract, Identifier}; use drive_proof_verifier::error::ContextProviderError; use drive_proof_verifier::ContextProvider; -use futures::task::ArcWake; -use futures::FutureExt; -use std::future::Future; +use pollster::FutureExt; use std::hash::Hash; use std::num::NonZeroUsize; use std::sync::Arc; -use std::task::{Context, Poll}; /// Context provider that uses the Core gRPC API to fetch data from the platform. /// @@ -174,9 +171,9 @@ impl ContextProvider for GrpcContextProvider { let contract_id = *data_contract_id; let sdk_cloned = sdk.clone(); - let data_contract: Option = - poll_future(DataContract::fetch(&sdk_cloned, contract_id)) - .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))?; + let data_contract: Option = DataContract::fetch(&sdk_cloned, contract_id) + .block_on() + .map_err(|e| ContextProviderError::InvalidDataContract(e.to_string()))?; if let Some(ref dc) = data_contract { self.data_contracts_cache.put(*data_contract_id, dc.clone()); @@ -186,61 +183,6 @@ impl ContextProvider for GrpcContextProvider { } } -/// Waker that uses a mutex and a condition variable to wake up a task. -/// -/// See [Condvar] documentation for more details. -struct MtxWaker { - mtx: std::sync::Mutex, - cvar: std::sync::Condvar, -} - -impl MtxWaker { - fn new() -> Self { - Self { - mtx: std::sync::Mutex::new(false), - cvar: std::sync::Condvar::new(), - } - } - - /// Block until the task is woken up. - fn wait(&self) { - let mut started = self.mtx.lock().expect("lock poisoned"); - while !*started { - started = self.cvar.wait(started).expect("wait failed"); - } - } -} - -impl ArcWake for MtxWaker { - fn wake_by_ref(arc_self: &Arc) { - let mut started = arc_self.mtx.lock().expect("lock poisoned"); - *started = true; - arc_self.cvar.notify_one(); - } -} - -/// Execute an async function in a sync context, when block_on() is not available. -/// -/// When async code runs some sync code inside a thread used to drive asynchronous tasks, -/// that sync code cannot call block_on(). This function is a workaround for this that allows to run async code in a -/// sync context. -/// -/// Note it's not as efficient as calling `.await` on a future, and should be used only for testing purposes. -fn poll_future(future: F) -> F::Output { - let mtx_waker = Arc::new(MtxWaker::new()); - let waker = futures::task::waker(mtx_waker.clone()); - let mut context = Context::from_waker(&waker); - - let mut future = Box::pin(future).fuse(); - - loop { - match Future::poll(std::pin::Pin::new(&mut future), &mut context) { - Poll::Ready(value) => return value, - Poll::Pending => mtx_waker.wait(), - } - } -} - /// Thread-safe cache of various objects inside the SDK. /// /// This is used to cache objects that are expensive to fetch from the platform, like data contracts.