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:
@@ -156,7 +156,7 @@ pub struct SearchFolderResultDto {
|
|||||||
* both files and folders that match the search criteria, along with pagination
|
* both files and folders that match the search criteria, along with pagination
|
||||||
* information and server-computed metadata.
|
* information and server-computed metadata.
|
||||||
*/
|
*/
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct SearchResultsDto {
|
pub struct SearchResultsDto {
|
||||||
/// Files matching the search criteria (enriched with metadata)
|
/// Files matching the search criteria (enriched with metadata)
|
||||||
pub files: Vec<SearchFileResultDto>,
|
pub files: Vec<SearchFileResultDto>,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::application::dtos::folder_dto::{
|
use crate::application::dtos::folder_dto::{
|
||||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||||
@@ -92,13 +93,15 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
|||||||
pub trait SearchUseCase: Send + Sync + 'static {
|
pub trait SearchUseCase: Send + Sync + 'static {
|
||||||
/// Performs a full search based on the specified criteria.
|
/// Performs a full search based on the specified criteria.
|
||||||
///
|
///
|
||||||
|
/// Returns `Arc<SearchResultsDto>` so the cache and the caller share
|
||||||
|
/// the same allocation — zero-copy on both insert and hit.
|
||||||
/// `user_id` identifies the authenticated user so that SQL queries filter
|
/// `user_id` identifies the authenticated user so that SQL queries filter
|
||||||
/// by owner and the result cache is isolated per tenant.
|
/// by owner and the result cache is isolated per tenant.
|
||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
criteria: SearchCriteriaDto,
|
criteria: SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<SearchResultsDto, DomainError>;
|
) -> Result<Arc<SearchResultsDto>, DomainError>;
|
||||||
|
|
||||||
/// Returns quick suggestions for autocomplete (lightweight, fast).
|
/// Returns quick suggestions for autocomplete (lightweight, fast).
|
||||||
async fn suggest(
|
async fn suggest(
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ pub struct SearchService {
|
|||||||
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).
|
||||||
/// Uses `future::Cache` so `.get()` / `.insert()` never block the Tokio
|
/// Values are `Arc<SearchResultsDto>` so cache insert/hit is a single
|
||||||
/// runtime (maintenance work is deferred to an internal async task).
|
/// atomic ref-count increment (~1 ns) instead of cloning thousands of Strings.
|
||||||
search_cache: moka::future::Cache<u64, SearchResultsDto>,
|
search_cache: moka::future::Cache<u64, Arc<SearchResultsDto>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Utility functions (pure, no self — computed on the server) ─────────
|
// ─── Utility functions (pure, no self — computed on the server) ─────────
|
||||||
@@ -134,12 +134,12 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to retrieve results from the cache.
|
/// 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
|
self.search_cache.get(&key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores results in the cache.
|
/// 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;
|
self.search_cache.insert(key, results).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ impl SearchUseCase for SearchService {
|
|||||||
* - Human-readable size formatting
|
* - Human-readable size formatting
|
||||||
* - Pagination
|
* - 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();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Try to get from cache
|
// Try to get from cache
|
||||||
@@ -359,7 +359,7 @@ impl SearchUseCase for SearchService {
|
|||||||
|
|
||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
let search_results = SearchResultsDto::new(
|
let search_results = Arc::new(SearchResultsDto::new(
|
||||||
paginated_files,
|
paginated_files,
|
||||||
paginated_folders,
|
paginated_folders,
|
||||||
criteria.limit,
|
criteria.limit,
|
||||||
@@ -367,9 +367,9 @@ impl SearchUseCase for SearchService {
|
|||||||
Some(total_count),
|
Some(total_count),
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
criteria.sort_by.clone(),
|
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);
|
return Ok(search_results);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ impl SearchUseCase for SearchService {
|
|||||||
|
|
||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
let search_results = SearchResultsDto::new(
|
let search_results = Arc::new(SearchResultsDto::new(
|
||||||
paginated_files,
|
paginated_files,
|
||||||
paginated_folders,
|
paginated_folders,
|
||||||
criteria.limit,
|
criteria.limit,
|
||||||
@@ -451,10 +451,10 @@ impl SearchUseCase for SearchService {
|
|||||||
Some(total_count),
|
Some(total_count),
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
criteria.sort_by.clone(),
|
criteria.sort_by.clone(),
|
||||||
);
|
));
|
||||||
|
|
||||||
// Store in cache
|
// Store in cache — Arc::clone is ~1 ns (atomic increment)
|
||||||
self.store_in_cache(cache_key, search_results.clone()).await;
|
self.store_in_cache(cache_key, Arc::clone(&search_results)).await;
|
||||||
|
|
||||||
Ok(search_results)
|
Ok(search_results)
|
||||||
}
|
}
|
||||||
@@ -490,8 +490,8 @@ impl SearchService {
|
|||||||
&self,
|
&self,
|
||||||
_criteria: SearchCriteriaDto,
|
_criteria: SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: &str,
|
||||||
) -> Result<SearchResultsDto> {
|
) -> Result<Arc<SearchResultsDto>> {
|
||||||
Ok(SearchResultsDto::empty())
|
Ok(Arc::new(SearchResultsDto::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn suggest(
|
async fn suggest(
|
||||||
|
|||||||
+2
-2
@@ -597,8 +597,8 @@ impl SearchUseCase for StubSearchUseCase {
|
|||||||
&self,
|
&self,
|
||||||
_criteria: SearchCriteriaDto,
|
_criteria: SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: &str,
|
||||||
) -> Result<SearchResultsDto, DomainError> {
|
) -> Result<Arc<SearchResultsDto>, DomainError> {
|
||||||
Ok(SearchResultsDto::empty())
|
Ok(Arc::new(SearchResultsDto::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn suggest(
|
async fn suggest(
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl SearchHandler {
|
|||||||
results.files.len(),
|
results.files.len(),
|
||||||
results.folders.len()
|
results.folders.len()
|
||||||
);
|
);
|
||||||
(StatusCode::OK, Json(results)).into_response()
|
(StatusCode::OK, Json(&*results)).into_response()
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Search error: {}", err);
|
error!("Search error: {}", err);
|
||||||
@@ -108,7 +108,7 @@ impl SearchHandler {
|
|||||||
results.files.len(),
|
results.files.len(),
|
||||||
results.folders.len()
|
results.folders.len()
|
||||||
);
|
);
|
||||||
(StatusCode::OK, Json(results)).into_response()
|
(StatusCode::OK, Json(&*results)).into_response()
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Search error: {}", err);
|
error!("Search error: {}", err);
|
||||||
|
|||||||
Reference in New Issue
Block a user