Browse Source

Move cache TTL to each wallet

Each wallet have their own cache TTL and access to cache freshness status
Cesar Rodas 1 week ago
parent
commit
a28b6ac58c

+ 3 - 2
crates/cdk/src/wallet/builder.rs

@@ -154,7 +154,7 @@ impl WalletBuilder {
                 cache.clone()
                 cache.clone()
             } else {
             } else {
                 // Create a new one
                 // Create a new one
-                Arc::new(MintMetadataCache::new(mint_url.clone(), None))
+                Arc::new(MintMetadataCache::new(mint_url.clone()))
             }
             }
         });
         });
 
 
@@ -207,7 +207,7 @@ impl WalletBuilder {
                 cache.clone()
                 cache.clone()
             } else {
             } else {
                 // Create a new one
                 // Create a new one
-                Arc::new(MintMetadataCache::new(mint_url.clone(), None))
+                Arc::new(MintMetadataCache::new(mint_url.clone()))
             }
             }
         });
         });
 
 
@@ -216,6 +216,7 @@ impl WalletBuilder {
             unit,
             unit,
             localstore,
             localstore,
             metadata_cache,
             metadata_cache,
+            metadata_cache_ttl: None.into(),
             target_proof_count: self.target_proof_count.unwrap_or(3),
             target_proof_count: self.target_proof_count.unwrap_or(3),
             #[cfg(feature = "auth")]
             #[cfg(feature = "auth")]
             auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),
             auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),

+ 25 - 5
crates/cdk/src/wallet/keysets.rs

@@ -15,7 +15,11 @@ impl Wallet {
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
     pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
         self.metadata_cache
         self.metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .keys
             .keys
             .get(&keyset_id)
             .get(&keyset_id)
@@ -38,7 +42,11 @@ impl Wallet {
     pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
     pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
         let keysets = self
         let keysets = self
             .metadata_cache
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .keysets
             .keysets
             .iter()
             .iter()
@@ -69,7 +77,11 @@ impl Wallet {
 
 
         let keysets = self
         let keysets = self
             .metadata_cache
             .metadata_cache
-            .load_from_mint(&self.localstore, &self.client)
+            .load_from_mint(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .keysets
             .keysets
             .iter()
             .iter()
@@ -111,7 +123,11 @@ impl Wallet {
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub async fn get_active_keyset(&self) -> Result<KeySetInfo, Error> {
     pub async fn get_active_keyset(&self) -> Result<KeySetInfo, Error> {
         self.metadata_cache
         self.metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .active_keysets
             .active_keysets
             .iter()
             .iter()
@@ -127,7 +143,11 @@ impl Wallet {
     pub async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Error> {
     pub async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Error> {
         let metadata = self
         let metadata = self
             .metadata_cache
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?;
             .await?;
 
 
         let mut fees = HashMap::new();
         let mut fees = HashMap::new();

+ 40 - 18
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -39,10 +39,10 @@ use cdk_common::{KeySet, MintInfo};
 use tokio::sync::Mutex;
 use tokio::sync::Mutex;
 
 
 use crate::nuts::Id;
 use crate::nuts::Id;
-#[cfg(feature = "auth")]
-use crate::wallet::AuthMintConnector;
 use crate::wallet::MintConnector;
 use crate::wallet::MintConnector;
-use crate::Error;
+#[cfg(feature = "auth")]
+use crate::wallet::{AuthMintConnector, AuthWallet};
+use crate::{Error, Wallet};
 
 
 /// Default TTL
 /// Default TTL
 pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
 pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
@@ -52,12 +52,12 @@ pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
 /// Tracks when data was last fetched and which version is currently cached.
 /// Tracks when data was last fetched and which version is currently cached.
 /// Used to determine if cache is ready and if database sync is needed.
 /// Used to determine if cache is ready and if database sync is needed.
 #[derive(Clone, Debug)]
 #[derive(Clone, Debug)]
-struct FreshnessStatus {
+pub struct FreshnessStatus {
     /// Whether this data has been successfully fetched at least once
     /// Whether this data has been successfully fetched at least once
-    is_populated: bool,
+    pub is_populated: bool,
 
 
     /// A future time when the cache would be considered as staled.
     /// A future time when the cache would be considered as staled.
-    valid_until: Instant,
+    pub updated_at: Instant,
 
 
     /// Monotonically increasing version number (for database sync tracking)
     /// Monotonically increasing version number (for database sync tracking)
     version: usize,
     version: usize,
@@ -67,7 +67,7 @@ impl Default for FreshnessStatus {
     fn default() -> Self {
     fn default() -> Self {
         Self {
         Self {
             is_populated: false,
             is_populated: false,
-            valid_until: Instant::now() + DEFAULT_TTL,
+            updated_at: Instant::now(),
             version: 0,
             version: 0,
         }
         }
     }
     }
@@ -122,8 +122,6 @@ pub struct MintMetadataCache {
     /// The mint server URL this cache manages
     /// The mint server URL this cache manages
     mint_url: MintUrl,
     mint_url: MintUrl,
 
 
-    default_ttl: Duration,
-
     /// Atomically-updated metadata snapshot (lock-free reads)
     /// Atomically-updated metadata snapshot (lock-free reads)
     metadata: Arc<ArcSwap<MintMetadata>>,
     metadata: Arc<ArcSwap<MintMetadata>>,
 
 
@@ -146,6 +144,27 @@ impl std::fmt::Debug for MintMetadataCache {
     }
     }
 }
 }
 
 
+impl Wallet {
+    /// Sets the metadata cache TTL
+    pub fn set_metadata_cache_ttl(&self, ttl: Option<Duration>) {
+        let mut guarded_ttl = self.metadata_cache_ttl.lock();
+        *guarded_ttl = ttl;
+    }
+
+    /// Get information about metada cache info
+    pub fn get_metadata_cache_info(&self) -> FreshnessStatus {
+        self.metadata_cache.metadata.load().status.clone()
+    }
+}
+
+#[cfg(feature = "auth")]
+impl AuthWallet {
+    /// Get information about metada cache info
+    pub fn get_metadata_cache_info(&self) -> FreshnessStatus {
+        self.metadata_cache.metadata.load().auth_status.clone()
+    }
+}
+
 impl MintMetadataCache {
 impl MintMetadataCache {
     /// Compute a unique identifier for an Arc pointer
     /// Compute a unique identifier for an Arc pointer
     ///
     ///
@@ -170,10 +189,9 @@ impl MintMetadataCache {
     /// let cache = MintMetadataCache::new(mint_url, None);
     /// let cache = MintMetadataCache::new(mint_url, None);
     /// // No data loaded yet - call load() to fetch
     /// // No data loaded yet - call load() to fetch
     /// ```
     /// ```
-    pub fn new(mint_url: MintUrl, default_ttl: Option<Duration>) -> Self {
+    pub fn new(mint_url: MintUrl) -> Self {
         Self {
         Self {
             mint_url,
             mint_url,
-            default_ttl: default_ttl.unwrap_or(DEFAULT_TTL),
             metadata: Arc::new(ArcSwap::default()),
             metadata: Arc::new(ArcSwap::default()),
             db_sync_versions: Arc::new(Default::default()),
             db_sync_versions: Arc::new(Default::default()),
             fetch_lock: Arc::new(Mutex::new(())),
             fetch_lock: Arc::new(Mutex::new(())),
@@ -212,15 +230,17 @@ impl MintMetadataCache {
         &self,
         &self,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
+        ttl: Option<Duration>,
     ) -> Result<Arc<MintMetadata>, Error> {
     ) -> Result<Arc<MintMetadata>, Error> {
         // Acquire lock to ensure only one fetch at a time
         // Acquire lock to ensure only one fetch at a time
+        let ttl = ttl.unwrap_or(DEFAULT_TTL);
         let current_version = self.metadata.load().status.version;
         let current_version = self.metadata.load().status.version;
         let _guard = self.fetch_lock.lock().await;
         let _guard = self.fetch_lock.lock().await;
 
 
         // Check if another caller already updated the cache while we waited
         // Check if another caller already updated the cache while we waited
         let current_metadata = self.metadata.load().clone();
         let current_metadata = self.metadata.load().clone();
         if current_metadata.status.is_populated
         if current_metadata.status.is_populated
-            && current_metadata.status.valid_until > Instant::now()
+            && current_metadata.status.updated_at + ttl > Instant::now()
             && current_metadata.status.version > current_version
             && current_metadata.status.version > current_version
         {
         {
             // Cache was just updated by another caller - return it
             // Cache was just updated by another caller - return it
@@ -271,9 +291,11 @@ impl MintMetadataCache {
         &self,
         &self,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
+        ttl: Option<Duration>,
     ) -> Result<Arc<MintMetadata>, Error> {
     ) -> Result<Arc<MintMetadata>, Error> {
         let cached_metadata = self.metadata.load().clone();
         let cached_metadata = self.metadata.load().clone();
         let storage_id = Self::arc_pointer_id(storage);
         let storage_id = Self::arc_pointer_id(storage);
+        let ttl = ttl.unwrap_or(DEFAULT_TTL);
 
 
         // Check what version of cache this database has seen
         // Check what version of cache this database has seen
         let db_synced_version = self
         let db_synced_version = self
@@ -284,7 +306,7 @@ impl MintMetadataCache {
             .unwrap_or_default();
             .unwrap_or_default();
 
 
         if cached_metadata.status.is_populated
         if cached_metadata.status.is_populated
-            && cached_metadata.status.valid_until > Instant::now()
+            && cached_metadata.status.updated_at + ttl > Instant::now()
         {
         {
             // Cache is ready - check if database needs updating
             // Cache is ready - check if database needs updating
             if db_synced_version != cached_metadata.status.version {
             if db_synced_version != cached_metadata.status.version {
@@ -297,7 +319,7 @@ impl MintMetadataCache {
         }
         }
 
 
         // Cache not populated - fetch from mint
         // Cache not populated - fetch from mint
-        self.load_from_mint(storage, client).await
+        self.load_from_mint(storage, client, Some(ttl)).await
     }
     }
 
 
     /// Load auth keysets and keys (auth feature only)
     /// Load auth keysets and keys (auth feature only)
@@ -331,7 +353,7 @@ impl MintMetadataCache {
 
 
         // Check if auth data is populated in cache
         // Check if auth data is populated in cache
         if cached_metadata.auth_status.is_populated
         if cached_metadata.auth_status.is_populated
-            && cached_metadata.auth_status.valid_until > Instant::now()
+            && cached_metadata.auth_status.updated_at > Instant::now()
         {
         {
             if db_synced_version != cached_metadata.status.version {
             if db_synced_version != cached_metadata.status.version {
                 // Database needs updating - spawn background sync
                 // Database needs updating - spawn background sync
@@ -346,7 +368,7 @@ impl MintMetadataCache {
         // Re-check if auth data was updated while waiting for lock
         // Re-check if auth data was updated while waiting for lock
         let current_metadata = self.metadata.load().clone();
         let current_metadata = self.metadata.load().clone();
         if current_metadata.auth_status.is_populated
         if current_metadata.auth_status.is_populated
-            && current_metadata.auth_status.valid_until > Instant::now()
+            && current_metadata.auth_status.updated_at > Instant::now()
         {
         {
             tracing::debug!(
             tracing::debug!(
                 "Auth cache was updated while waiting for fetch lock, returning cached data"
                 "Auth cache was updated while waiting for fetch lock, returning cached data"
@@ -562,14 +584,14 @@ impl MintMetadataCache {
         // Update freshness status based on what was fetched
         // Update freshness status based on what was fetched
         if client.is_some() {
         if client.is_some() {
             new_metadata.status.is_populated = true;
             new_metadata.status.is_populated = true;
-            new_metadata.status.valid_until = Instant::now() + self.default_ttl;
+            new_metadata.status.updated_at = Instant::now();
             new_metadata.status.version += 1;
             new_metadata.status.version += 1;
         }
         }
 
 
         #[cfg(feature = "auth")]
         #[cfg(feature = "auth")]
         if auth_client.is_some() {
         if auth_client.is_some() {
             new_metadata.auth_status.is_populated = true;
             new_metadata.auth_status.is_populated = true;
-            new_metadata.auth_status.valid_until = Instant::now() + self.default_ttl;
+            new_metadata.auth_status.updated_at = Instant::now();
             new_metadata.auth_status.version += 1;
             new_metadata.auth_status.version += 1;
         }
         }
 
 

+ 62 - 2
crates/cdk/src/wallet/mod.rs

@@ -1,12 +1,16 @@
 #![doc = include_str!("./README.md")]
 #![doc = include_str!("./README.md")]
 
 
 use std::collections::HashMap;
 use std::collections::HashMap;
+use std::fmt::Debug;
+use std::ops::Deref;
 use std::str::FromStr;
 use std::str::FromStr;
 use std::sync::atomic::AtomicBool;
 use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 use std::sync::Arc;
+use std::time::Duration;
 
 
 use cdk_common::amount::FeeAndAmounts;
 use cdk_common::amount::FeeAndAmounts;
 use cdk_common::database::{self, WalletDatabase};
 use cdk_common::database::{self, WalletDatabase};
+use cdk_common::parking_lot::Mutex;
 use cdk_common::subscription::WalletParams;
 use cdk_common::subscription::WalletParams;
 use getrandom::getrandom;
 use getrandom::getrandom;
 use subscription::{ActiveSubscription, SubscriptionManager};
 use subscription::{ActiveSubscription, SubscriptionManager};
@@ -75,6 +79,53 @@ pub use types::{MeltQuote, MintQuote, SendKind};
 
 
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::nut00::ProofsMethods;
 
 
+/// Clonable mutex
+///
+/// Each instance is indepent from each other, that's why an Arc is not an option
+#[derive(Debug)]
+pub struct ClonableMutex<T>(Mutex<T>)
+where
+    T: Clone + Debug;
+
+impl<T> From<T> for ClonableMutex<T>
+where
+    T: Clone + Debug,
+{
+    fn from(value: T) -> Self {
+        Self(Mutex::new(value))
+    }
+}
+
+impl<T> ClonableMutex<T>
+where
+    T: Clone + Debug,
+{
+    /// Clone the inner guarded object
+    pub fn clone_inner(&self) -> T {
+        (*self.0.lock().deref()).clone()
+    }
+}
+
+impl<T> Clone for ClonableMutex<T>
+where
+    T: Clone + Debug,
+{
+    fn clone(&self) -> Self {
+        Self(Mutex::new(self.0.lock().clone()))
+    }
+}
+
+impl<T> Deref for ClonableMutex<T>
+where
+    T: Clone + Debug,
+{
+    type Target = Mutex<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 /// CDK Wallet
 /// CDK Wallet
 ///
 ///
 /// The CDK [`Wallet`] is a high level cashu wallet.
 /// The CDK [`Wallet`] is a high level cashu wallet.
@@ -92,6 +143,7 @@ pub struct Wallet {
     pub metadata_cache: Arc<MintMetadataCache>,
     pub metadata_cache: Arc<MintMetadataCache>,
     /// The targeted amount of proofs to have at each size
     /// The targeted amount of proofs to have at each size
     pub target_proof_count: usize,
     pub target_proof_count: usize,
+    metadata_cache_ttl: ClonableMutex<Option<Duration>>,
     #[cfg(feature = "auth")]
     #[cfg(feature = "auth")]
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
     seed: [u8; 64],
     seed: [u8; 64],
@@ -223,7 +275,11 @@ impl Wallet {
         let mut fee_per_keyset = HashMap::new();
         let mut fee_per_keyset = HashMap::new();
         let metadata = self
         let metadata = self
             .metadata_cache
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?;
             .await?;
 
 
         for keyset_id in proofs_per_keyset.keys() {
         for keyset_id in proofs_per_keyset.keys() {
@@ -244,7 +300,11 @@ impl Wallet {
     pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
     pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
         let input_fee_ppk = self
         let input_fee_ppk = self
             .metadata_cache
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .keysets
             .keysets
             .get(keyset_id)
             .get(keyset_id)

+ 5 - 1
crates/cdk/src/wallet/swap.rs

@@ -49,7 +49,11 @@ impl Wallet {
 
 
         let active_keys = self
         let active_keys = self
             .metadata_cache
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(
+                &self.localstore,
+                &self.client,
+                self.metadata_cache_ttl.clone_inner(),
+            )
             .await?
             .await?
             .keys
             .keys
             .get(&active_keyset_id)
             .get(&active_keyset_id)