diff --git a/integration-tests/Cargo.lock b/integration-tests/Cargo.lock index 9dd8e03f5..993d1749c 100644 --- a/integration-tests/Cargo.lock +++ b/integration-tests/Cargo.lock @@ -2666,6 +2666,7 @@ dependencies = [ "axum", "bs58", "config", + "dashmap", "dirs", "futures", "miniscript", diff --git a/miner-apps/Cargo.lock b/miner-apps/Cargo.lock index f0da4ac03..e909eb6d6 100644 --- a/miner-apps/Cargo.lock +++ b/miner-apps/Cargo.lock @@ -2439,6 +2439,7 @@ dependencies = [ "axum", "bs58", "config", + "dashmap", "dirs", "futures", "miniscript", diff --git a/pool-apps/Cargo.lock b/pool-apps/Cargo.lock index c2fe0b3f7..0b7287a31 100644 --- a/pool-apps/Cargo.lock +++ b/pool-apps/Cargo.lock @@ -2446,6 +2446,7 @@ dependencies = [ "axum", "bs58", "config", + "dashmap", "dirs", "futures", "miniscript", diff --git a/stratum-apps/Cargo.toml b/stratum-apps/Cargo.toml index 5850c4b9e..67a8a5e0c 100644 --- a/stratum-apps/Cargo.toml +++ b/stratum-apps/Cargo.toml @@ -55,6 +55,7 @@ utoipa-swagger-ui = { version = "9.0.2", features = ["axum"], optional = true } # Common external dependencies that roles always need ext-config = { version = "0.14.0", features = ["toml"], package = "config" } shellexpand = "3.1.1" +dashmap = "6.1.0" [features] default = ["network", "config", "std"] diff --git a/stratum-apps/src/lib.rs b/stratum-apps/src/lib.rs index 308a9e917..7aa09ac35 100644 --- a/stratum-apps/src/lib.rs +++ b/stratum-apps/src/lib.rs @@ -78,3 +78,6 @@ pub mod coinbase_output_constraints; /// Fallback coordinator pub mod fallback_coordinator; + +/// Share synchronous primitives +pub mod shared; diff --git a/stratum-apps/src/shared.rs b/stratum-apps/src/shared.rs new file mode 100644 index 000000000..6aefb1bad --- /dev/null +++ b/stratum-apps/src/shared.rs @@ -0,0 +1,258 @@ +use std::{ + hash::Hash, + sync::{Arc, RwLock}, +}; + +use dashmap::DashMap; +use std::sync::Mutex; + +/// Thread-safe shared mutable value using `Mutex` for exclusive access. +#[derive(Debug)] +pub struct Shared(Arc>); + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(Arc::clone(&self.0)) + } +} + +impl Shared { + /// Create a new shared value. + pub fn new(v: T) -> Self { + Shared(Arc::new(Mutex::new(v))) + } + + /// Execute a closure with mutable access to the inner value. + pub fn with(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + let mut lock = self.0.lock().unwrap(); + f(&mut *lock) + } + + /// Get a cloned snapshot of the value. + pub fn get(&self) -> T + where + T: Clone, + { + self.with(|v| v.clone()) + } + + /// Replace the inner value. + pub fn set(&self, value: T) { + self.with(|v| *v = value); + } +} + +/// Thread-safe shared value using `RwLock` for concurrent reads and exclusive writes. +#[derive(Debug)] +pub struct SharedRw(Arc>); + +impl Clone for SharedRw { + fn clone(&self) -> Self { + SharedRw(Arc::clone(&self.0)) + } +} + +impl SharedRw { + /// Create a new shared value. + pub fn new(v: T) -> Self { + SharedRw(Arc::new(RwLock::new(v))) + } + + /// Execute a closure with read-only access. + pub fn read(&self, f: F) -> R + where + F: FnOnce(&T) -> R, + { + let guard = self.0.read().unwrap(); + f(&*guard) + } + + /// Execute a closure with mutable access. + pub fn write(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + let mut guard = self.0.write().unwrap(); + f(&mut *guard) + } + + /// Get a cloned snapshot of the value. + pub fn get(&self) -> T + where + T: Clone, + { + self.read(|v| v.clone()) + } + + /// Replace the inner value. + pub fn set(&self, value: T) { + self.write(|v| *v = value); + } +} + +/// Concurrent map wrapper over `DashMap` providing ergonomic scoped access. +pub struct SharedMap(Arc>); + +impl Clone for SharedMap { + fn clone(&self) -> Self { + SharedMap(Arc::clone(&self.0)) + } +} + +impl SharedMap { + /// Create a new concurrent map. + pub fn new() -> Self { + SharedMap(Arc::new(DashMap::new())) + } + + /// Read a value for a key using a closure. + pub fn with(&self, key: &K, f: F) -> Option + where + F: FnOnce(&V) -> R, + { + let guard = self.0.get(key)?; + let result = f(guard.value()); + drop(guard); + Some(result) + } + + /// Mutate a value for a key using a closure. + pub fn with_mut(&self, key: &K, f: F) -> Option + where + F: FnOnce(&mut V) -> R, + { + let mut guard = self.0.get_mut(key)?; + let result = f(guard.value_mut()); + Some(result) + } + + /// Iterate over all entries immutably. + pub fn for_each(&self, mut f: F) + where + F: FnMut(K, &V) -> Ret, + { + for entry in self.0.iter() { + f(entry.key().clone(), entry.value()); + } + } + + /// Iterate over all entries mutably. + pub fn for_each_mut(&self, mut f: F) + where + F: FnMut(K, &mut V) -> Ret, + { + for mut entry in self.0.iter_mut() { + f(entry.key().clone(), entry.value_mut()); + } + } + + /// Fallible mutable iteration over all entries. + pub fn try_for_each_mut(&self, mut f: F) -> Result<(), E> + where + F: FnMut(K, &mut V) -> Result<(), E>, + { + for mut entry in self.0.iter_mut() { + f(entry.key().clone(), entry.value_mut())?; + } + Ok(()) + } + + /// Insert a key-value pair. + pub fn insert(&self, key: K, value: V) -> Option { + self.0.insert(key, value) + } + + /// Remove a key. + pub fn remove(&self, key: &K) -> Option<(K, V)> { + self.0.remove(key) + } + + /// Check if a key exists. + pub fn contains_key(&self, key: &K) -> bool { + self.0.contains_key(key) + } + + /// Retain entries matching predicate. + pub fn retain(&self, f: F) + where + F: FnMut(&K, &mut V) -> bool, + { + self.0.retain(f); + } + + /// Collect all keys. + pub fn keys(&self) -> Vec + where + K: Clone, + { + self.0.iter().map(|e| e.key().clone()).collect() + } + + /// Number of entries. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Default for SharedMap { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shared_basic_usage() { + let v = Shared::new(10); + + v.with(|x| *x += 5); + assert_eq!(v.get(), 15); + + v.set(42); + assert_eq!(v.get(), 42); + } + + #[test] + fn shared_rw_usage() { + let v = SharedRw::new(100); + + let a = v.read(|x| *x); + let b = v.read(|x| *x); + assert_eq!(a, b); + + v.write(|x| *x += 1); + assert_eq!(v.get(), 101); + } + + #[test] + fn shared_map_usage() { + let map = SharedMap::new(); + + map.insert("a", 1); + map.insert("b", 2); + + let val = map.with(&"a", |v| *v).unwrap(); + assert_eq!(val, 1); + + map.with_mut(&"a", |v| *v += 10); + assert_eq!(map.with(&"a", |v| *v).unwrap(), 11); + + let mut sum = 0; + map.for_each(|_, v| sum += v); + assert_eq!(sum, 13); + + map.remove(&"a"); + assert!(!map.contains_key(&"a")); + } +}