From cd5b93706535ab1ff3c6e3745780b8f849a4517a Mon Sep 17 00:00:00 2001 From: Dionisio Date: Tue, 24 Feb 2026 12:58:47 +0100 Subject: [PATCH] perf(search): migrate moka::sync::Cache to moka::future::Cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/application/services/search_service.rs | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/application/services/search_service.rs b/src/application/services/search_service.rs index a7c28515..25e3417b 100644 --- a/src/application/services/search_service.rs +++ b/src/application/services/search_service.rs @@ -41,8 +41,10 @@ pub struct SearchService { /// Repository for folder operations folder_repository: Arc, - /// Lock-free concurrent cache with automatic TTL and LRU eviction (moka) - search_cache: moka::sync::Cache, + /// Lock-free concurrent cache with automatic TTL and LRU eviction (moka). + /// 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, } // ─── Utility functions (pure, no self — computed on the server) ───────── @@ -111,7 +113,7 @@ impl SearchService { cache_ttl: u64, max_cache_size: usize, ) -> Self { - let search_cache = moka::sync::Cache::builder() + let search_cache = moka::future::Cache::builder() .max_capacity(max_cache_size as u64) .time_to_live(Duration::from_secs(cache_ttl)) .build(); @@ -132,13 +134,13 @@ impl SearchService { } /// Attempts to retrieve results from the cache. - fn get_from_cache(&self, key: u64) -> Option { - self.search_cache.get(&key) + async fn get_from_cache(&self, key: u64) -> Option { + self.search_cache.get(&key).await } /// Stores results in the cache. - fn store_in_cache(&self, key: u64, results: SearchResultsDto) { - self.search_cache.insert(key, results); + async fn store_in_cache(&self, key: u64, results: SearchResultsDto) { + self.search_cache.insert(key, results).await; } /// Enrich a FileDto → SearchFileResultDto with server-computed metadata. @@ -278,7 +280,7 @@ impl SearchUseCase for SearchService { // Try to get from cache 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); } @@ -382,7 +384,7 @@ impl SearchUseCase for SearchService { 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); } @@ -481,7 +483,7 @@ impl SearchUseCase for SearchService { ); // 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) } @@ -499,6 +501,7 @@ impl SearchUseCase for SearchService { /// Clears the search results cache. async fn clear_search_cache(&self) -> Result<()> { self.search_cache.invalidate_all(); + self.search_cache.run_pending_tasks().await; Ok(()) } }