perf: Arc-wrap SearchResultsDto for zero-copy cache reads

- Change SearchUseCase::search() return type to Arc<SearchResultsDto>
- Replace Cache<u64, SearchResultsDto> with Cache<u64, Arc<SearchResultsDto>>
- Eliminate .clone() on full result set; use Arc::clone() (ptr bump)
- Remove Clone derive from SearchResultsDto (no longer needed)
- Update handlers to deref Arc for JSON serialization
This commit is contained in:
Dionisio
2026-02-25 23:47:51 +01:00
parent 5a1959bf23
commit 5b55056921
5 changed files with 24 additions and 21 deletions
+15 -15
View File
@@ -42,9 +42,9 @@ pub struct SearchService {
folder_repository: Arc<dyn FolderStoragePort>,
/// 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<u64, SearchResultsDto>,
/// Values are `Arc<SearchResultsDto>` so cache insert/hit is a single
/// atomic ref-count increment (~1 ns) instead of cloning thousands of Strings.
search_cache: moka::future::Cache<u64, Arc<SearchResultsDto>>,
}
// ─── Utility functions (pure, no self — computed on the server) ─────────
@@ -134,12 +134,12 @@ impl SearchService {
}
/// Attempts to retrieve results from the cache.
async fn get_from_cache(&self, key: u64) -> Option<SearchResultsDto> {
async fn get_from_cache(&self, key: u64) -> Option<Arc<SearchResultsDto>> {
self.search_cache.get(&key).await
}
/// Stores results in the cache.
async fn store_in_cache(&self, key: u64, results: SearchResultsDto) {
async fn store_in_cache(&self, key: u64, results: Arc<SearchResultsDto>) {
self.search_cache.insert(key, results).await;
}
@@ -272,7 +272,7 @@ impl SearchUseCase for SearchService {
* - Human-readable size formatting
* - Pagination
*/
async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result<SearchResultsDto> {
async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result<Arc<SearchResultsDto>> {
let start = Instant::now();
// Try to get from cache
@@ -359,7 +359,7 @@ impl SearchUseCase for SearchService {
let elapsed_ms = start.elapsed().as_millis() as u64;
let search_results = SearchResultsDto::new(
let search_results = Arc::new(SearchResultsDto::new(
paginated_files,
paginated_folders,
criteria.limit,
@@ -367,9 +367,9 @@ impl SearchUseCase for SearchService {
Some(total_count),
elapsed_ms,
criteria.sort_by.clone(),
);
));
self.store_in_cache(cache_key, search_results.clone()).await;
self.store_in_cache(cache_key, Arc::clone(&search_results)).await;
return Ok(search_results);
}
@@ -443,7 +443,7 @@ impl SearchUseCase for SearchService {
let elapsed_ms = start.elapsed().as_millis() as u64;
let search_results = SearchResultsDto::new(
let search_results = Arc::new(SearchResultsDto::new(
paginated_files,
paginated_folders,
criteria.limit,
@@ -451,10 +451,10 @@ impl SearchUseCase for SearchService {
Some(total_count),
elapsed_ms,
criteria.sort_by.clone(),
);
));
// Store in cache
self.store_in_cache(cache_key, search_results.clone()).await;
// Store in cache — Arc::clone is ~1 ns (atomic increment)
self.store_in_cache(cache_key, Arc::clone(&search_results)).await;
Ok(search_results)
}
@@ -490,8 +490,8 @@ impl SearchService {
&self,
_criteria: SearchCriteriaDto,
_user_id: &str,
) -> Result<SearchResultsDto> {
Ok(SearchResultsDto::empty())
) -> Result<Arc<SearchResultsDto>> {
Ok(Arc::new(SearchResultsDto::empty()))
}
async fn suggest(