Эх сурвалжийг харах

Prevent database contention in metadata cache load operations (#1300)

* refactor(cdk/wallet): prevent database contention in metadata cache load operations

Refactors mint metadata cache to eliminate database reads during active
transactions by extracting database loading into a separate `load_from_storage`
method that runs independently at wallet initialization.

Previously, `load()` would read from storage during transaction execution,
causing database contention with concurrent wallet operations. The new
approach:

- Introduces `load_from_storage` method that loads mint info and keysets from
  storage upfront
- Spawns an async task during wallet initialization to populate the cache from
  storage
- Removes all storage reads from the `load()` and `load_auth()` methods
- Ensures storage is only used to sync data back, never for reading during load
  operations

This prevents transaction conflicts and improves concurrent operation
performance by ensuring the metadata cache operates independently of ongoing
database transactions.
C 1 өдөр өмнө
parent
commit
a12fd4dbea

+ 12 - 0
crates/cdk/src/wallet/builder.rs

@@ -4,6 +4,7 @@ use std::time::Duration;
 
 use cdk_common::database;
 use cdk_common::parking_lot::RwLock;
+use cdk_common::task::spawn;
 #[cfg(feature = "auth")]
 use cdk_common::AuthToken;
 #[cfg(feature = "auth")]
@@ -223,6 +224,17 @@ impl WalletBuilder {
             }
         });
 
+        let metadata_for_loader = metadata_cache.clone();
+        let localstore_for_loader = localstore.clone();
+        spawn(async move {
+            let _ = metadata_for_loader
+                .load_from_storage(&localstore_for_loader)
+                .await
+                .inspect_err(|err| {
+                    tracing::warn!("Failed to load mint metadata from storage {err}");
+                });
+        });
+
         Ok(Wallet {
             mint_url,
             unit,

+ 58 - 31
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -243,20 +243,6 @@ impl MintMetadataCache {
             return Ok(current_metadata);
         }
 
-        // Load keys from database before fetching from HTTP
-        // This prevents re-fetching keys we already have and avoids duplicate insertions
-        if let Some(keysets) = storage.get_mint_keysets(self.mint_url.clone()).await? {
-            let mut updated_metadata = (*self.metadata.load().clone()).clone();
-            for keyset_info in keysets {
-                if let Some(keys) = storage.get_keys(&keyset_info.id).await? {
-                    tracing::trace!("Loaded keys for keyset {} from database", keyset_info.id);
-                    updated_metadata.keys.insert(keyset_info.id, Arc::new(keys));
-                }
-            }
-            // Update cache with database keys before HTTP fetch
-            self.metadata.store(Arc::new(updated_metadata));
-        }
-
         // Perform the fetch
         #[cfg(feature = "auth")]
         let metadata = self.fetch_from_http(Some(client), None).await?;
@@ -331,6 +317,64 @@ impl MintMetadataCache {
         self.load_from_mint(storage, client).await
     }
 
+    /// Load mint info and keys from storage.
+    ///
+    /// This function should be called without any competing transaction with the storage.
+    pub async fn load_from_storage(
+        &self,
+        storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+    ) -> Result<(), Error> {
+        // Load keys from database before fetching from HTTP
+        // This prevents re-fetching keys we already have and avoids duplicate insertions
+        let mut updated_metadata = (*self.metadata.load().clone()).clone();
+
+        updated_metadata.mint_info = storage
+            .get_mint(self.mint_url.clone())
+            .await?
+            .ok_or(Error::UnknownKeySet)?;
+
+        let keysets = storage
+            .get_mint_keysets(self.mint_url.clone())
+            .await?
+            .ok_or(Error::UnknownKeySet)?
+            .into_iter()
+            .map(Arc::new)
+            .collect::<Vec<_>>();
+
+        for keyset_info in keysets.iter() {
+            if let Some(keys) = storage.get_keys(&keyset_info.id).await? {
+                tracing::trace!(
+                    "Loaded keys for keyset {} from database (auth)",
+                    keyset_info.id
+                );
+                updated_metadata.keys.insert(keyset_info.id, Arc::new(keys));
+            }
+            if keyset_info.active {
+                updated_metadata.active_keysets.push(keyset_info.clone());
+            }
+        }
+
+        updated_metadata.keysets = keysets
+            .into_iter()
+            .map(|keyset| (keyset.id, keyset))
+            .collect();
+        updated_metadata.status.is_populated = true;
+        updated_metadata.status.version += 1;
+        updated_metadata.status.updated_at = Instant::now();
+
+        #[cfg(feature = "auth")]
+        {
+            updated_metadata.auth_status.is_populated = true;
+            updated_metadata.auth_status.updated_at = Instant::now();
+            updated_metadata.auth_status.version += 1;
+        }
+
+        // Update cache with database keys before HTTP fetch
+        self.metadata.store(Arc::new(updated_metadata));
+
+        Ok(())
+    }
+
     /// Load auth keysets and keys (auth feature only)
     ///
     /// Fetches blind authentication keysets from the mint. Always performs
@@ -385,23 +429,6 @@ impl MintMetadataCache {
             return Ok(current_metadata);
         }
 
-        // Load keys from database before fetching from HTTP
-        // This prevents re-fetching keys we already have and avoids duplicate insertions
-        if let Some(keysets) = storage.get_mint_keysets(self.mint_url.clone()).await? {
-            let mut updated_metadata = (*self.metadata.load().clone()).clone();
-            for keyset_info in keysets {
-                if let Some(keys) = storage.get_keys(&keyset_info.id).await? {
-                    tracing::trace!(
-                        "Loaded keys for keyset {} from database (auth)",
-                        keyset_info.id
-                    );
-                    updated_metadata.keys.insert(keyset_info.id, Arc::new(keys));
-                }
-            }
-            // Update cache with database keys before HTTP fetch
-            self.metadata.store(Arc::new(updated_metadata));
-        }
-
         // Auth data not in cache - fetch from mint
         let metadata = self.fetch_from_http(None, Some(auth_client)).await?;