perf(search): migrate moka::sync::Cache to moka::future::Cache

sync::Cache uses an internal Mutex that blocks the Tokio worker thread
during maintenance (eviction, frequency bookkeeping). Under high
concurrency this serialises all tasks scheduled on the same worker.

future::Cache defers maintenance to an internal async task and never
holds a synchronous lock visible to the caller.

Changes:
- Field type: sync::Cache → future::Cache
- get_from_cache / store_in_cache: now async, calls .await
- clear_search_cache: added run_pending_tasks().await after invalidate

Consistent with the pattern already used in thumbnail_service,
image_transcode_service, and file_content_cache.
This commit is contained in:
Dionisio
2026-02-24 12:58:47 +01:00
parent 41af7f0933
commit cd5b937065
+13 -10
View File
@@ -41,8 +41,10 @@ pub struct SearchService {
/// Repository for folder operations /// Repository for folder operations
folder_repository: Arc<dyn FolderStoragePort>, folder_repository: Arc<dyn FolderStoragePort>,
/// Lock-free concurrent cache with automatic TTL and LRU eviction (moka) /// Lock-free concurrent cache with automatic TTL and LRU eviction (moka).
search_cache: moka::sync::Cache<u64, SearchResultsDto>, /// Uses `future::Cache` so `.get()` / `.insert()` never block the Tokio
/// runtime (maintenance work is deferred to an internal async task).
search_cache: moka::future::Cache<u64, SearchResultsDto>,
} }
// ─── Utility functions (pure, no self — computed on the server) ───────── // ─── Utility functions (pure, no self — computed on the server) ─────────
@@ -111,7 +113,7 @@ impl SearchService {
cache_ttl: u64, cache_ttl: u64,
max_cache_size: usize, max_cache_size: usize,
) -> Self { ) -> Self {
let search_cache = moka::sync::Cache::builder() let search_cache = moka::future::Cache::builder()
.max_capacity(max_cache_size as u64) .max_capacity(max_cache_size as u64)
.time_to_live(Duration::from_secs(cache_ttl)) .time_to_live(Duration::from_secs(cache_ttl))
.build(); .build();
@@ -132,13 +134,13 @@ impl SearchService {
} }
/// Attempts to retrieve results from the cache. /// Attempts to retrieve results from the cache.
fn get_from_cache(&self, key: u64) -> Option<SearchResultsDto> { async fn get_from_cache(&self, key: u64) -> Option<SearchResultsDto> {
self.search_cache.get(&key) self.search_cache.get(&key).await
} }
/// Stores results in the cache. /// Stores results in the cache.
fn store_in_cache(&self, key: u64, results: SearchResultsDto) { async fn store_in_cache(&self, key: u64, results: SearchResultsDto) {
self.search_cache.insert(key, results); self.search_cache.insert(key, results).await;
} }
/// Enrich a FileDto → SearchFileResultDto with server-computed metadata. /// Enrich a FileDto → SearchFileResultDto with server-computed metadata.
@@ -278,7 +280,7 @@ impl SearchUseCase for SearchService {
// Try to get from cache // Try to get from cache
let cache_key = Self::create_cache_key(&criteria, user_id); let cache_key = Self::create_cache_key(&criteria, user_id);
if let Some(cached_results) = self.get_from_cache(cache_key) { if let Some(cached_results) = self.get_from_cache(cache_key).await {
return Ok(cached_results); return Ok(cached_results);
} }
@@ -382,7 +384,7 @@ impl SearchUseCase for SearchService {
criteria.sort_by.clone(), criteria.sort_by.clone(),
); );
self.store_in_cache(cache_key, search_results.clone()); self.store_in_cache(cache_key, search_results.clone()).await;
return Ok(search_results); return Ok(search_results);
} }
@@ -481,7 +483,7 @@ impl SearchUseCase for SearchService {
); );
// Store in cache // Store in cache
self.store_in_cache(cache_key, search_results.clone()); self.store_in_cache(cache_key, search_results.clone()).await;
Ok(search_results) Ok(search_results)
} }
@@ -499,6 +501,7 @@ impl SearchUseCase for SearchService {
/// Clears the search results cache. /// Clears the search results cache.
async fn clear_search_cache(&self) -> Result<()> { async fn clear_search_cache(&self) -> Result<()> {
self.search_cache.invalidate_all(); self.search_cache.invalidate_all();
self.search_cache.run_pending_tasks().await;
Ok(()) Ok(())
} }
} }