mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
fix
This commit is contained in:
@@ -186,26 +186,26 @@ pub fn get_concurrency_aware_buffer_size(file_size: i64, base_buffer_size: usize
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn get_advanced_buffer_size(file_size: i64, base_buffer_size: usize, is_sequential: bool) -> usize {
|
pub fn get_advanced_buffer_size(file_size: i64, base_buffer_size: usize, is_sequential: bool) -> usize {
|
||||||
let concurrent_requests = ACTIVE_GET_REQUESTS.load(Ordering::Relaxed);
|
let concurrent_requests = ACTIVE_GET_REQUESTS.load(Ordering::Relaxed);
|
||||||
|
|
||||||
// For very small files, use smaller buffers regardless of concurrency
|
// For very small files, use smaller buffers regardless of concurrency
|
||||||
if file_size > 0 && file_size < 256 * KI_B as i64 {
|
if file_size > 0 && file_size < 256 * KI_B as i64 {
|
||||||
return (file_size as usize / 4).max(16 * KI_B).min(64 * KI_B);
|
return (file_size as usize / 4).max(16 * KI_B).min(64 * KI_B);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base calculation from standard function
|
// Base calculation from standard function
|
||||||
let standard_size = get_concurrency_aware_buffer_size(file_size, base_buffer_size);
|
let standard_size = get_concurrency_aware_buffer_size(file_size, base_buffer_size);
|
||||||
|
|
||||||
// For sequential reads, we can be more aggressive with buffer sizes
|
// For sequential reads, we can be more aggressive with buffer sizes
|
||||||
if is_sequential && concurrent_requests <= MEDIUM_CONCURRENCY_THRESHOLD {
|
if is_sequential && concurrent_requests <= MEDIUM_CONCURRENCY_THRESHOLD {
|
||||||
return ((standard_size as f64 * 1.5) as usize).min(2 * MI_B);
|
return ((standard_size as f64 * 1.5) as usize).min(2 * MI_B);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For high concurrency with large files, optimize for parallel processing
|
// For high concurrency with large files, optimize for parallel processing
|
||||||
if concurrent_requests > HIGH_CONCURRENCY_THRESHOLD && file_size > 10 * MI_B as i64 {
|
if concurrent_requests > HIGH_CONCURRENCY_THRESHOLD && file_size > 10 * MI_B as i64 {
|
||||||
// Use smaller, more numerous buffers for better parallelism
|
// Use smaller, more numerous buffers for better parallelism
|
||||||
return (standard_size as f64 * 0.8) as usize;
|
return (standard_size as f64 * 0.8) as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
standard_size
|
standard_size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,18 +261,18 @@ impl HotObjectCache {
|
|||||||
// Clone the data reference while holding read lock
|
// Clone the data reference while holding read lock
|
||||||
let data = Arc::clone(&cached.data);
|
let data = Arc::clone(&cached.data);
|
||||||
drop(cache);
|
drop(cache);
|
||||||
|
|
||||||
// Now acquire write lock to promote in LRU and update stats
|
// Now acquire write lock to promote in LRU and update stats
|
||||||
let mut cache_write = self.cache.write().await;
|
let mut cache_write = self.cache.write().await;
|
||||||
if let Some(cached) = cache_write.get(key) {
|
if let Some(cached) = cache_write.get(key) {
|
||||||
cached.hit_count.fetch_add(1, Ordering::Relaxed);
|
cached.hit_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
{
|
{
|
||||||
use metrics::counter;
|
use metrics::counter;
|
||||||
counter!("rustfs_object_cache_hits").increment(1);
|
counter!("rustfs_object_cache_hits").increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(data);
|
return Some(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,13 +346,13 @@ impl HotObjectCache {
|
|||||||
max_object_size: self.max_object_size,
|
max_object_size: self.max_object_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a key exists in cache without promoting it in LRU
|
/// Check if a key exists in cache without promoting it in LRU
|
||||||
async fn contains(&self, key: &str) -> bool {
|
async fn contains(&self, key: &str) -> bool {
|
||||||
let cache = self.cache.read().await;
|
let cache = self.cache.read().await;
|
||||||
cache.peek(key).is_some()
|
cache.peek(key).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get multiple objects from cache in a single operation
|
/// Get multiple objects from cache in a single operation
|
||||||
///
|
///
|
||||||
/// This is more efficient than calling get() multiple times as it acquires
|
/// This is more efficient than calling get() multiple times as it acquires
|
||||||
@@ -360,12 +360,12 @@ impl HotObjectCache {
|
|||||||
async fn get_batch(&self, keys: &[String]) -> Vec<Option<Arc<Vec<u8>>>> {
|
async fn get_batch(&self, keys: &[String]) -> Vec<Option<Arc<Vec<u8>>>> {
|
||||||
let mut cache = self.cache.write().await;
|
let mut cache = self.cache.write().await;
|
||||||
let mut results = Vec::with_capacity(keys.len());
|
let mut results = Vec::with_capacity(keys.len());
|
||||||
|
|
||||||
for key in keys {
|
for key in keys {
|
||||||
if let Some(cached) = cache.get(key) {
|
if let Some(cached) = cache.get(key) {
|
||||||
cached.hit_count.fetch_add(1, Ordering::Relaxed);
|
cached.hit_count.fetch_add(1, Ordering::Relaxed);
|
||||||
results.push(Some(Arc::clone(&cached.data)));
|
results.push(Some(Arc::clone(&cached.data)));
|
||||||
|
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
{
|
{
|
||||||
use metrics::counter;
|
use metrics::counter;
|
||||||
@@ -373,7 +373,7 @@ impl HotObjectCache {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
results.push(None);
|
results.push(None);
|
||||||
|
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
{
|
{
|
||||||
use metrics::counter;
|
use metrics::counter;
|
||||||
@@ -381,24 +381,25 @@ impl HotObjectCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a specific key from cache
|
/// Remove a specific key from cache
|
||||||
///
|
///
|
||||||
/// Returns true if the key was found and removed, false otherwise.
|
/// Returns true if the key was found and removed, false otherwise.
|
||||||
async fn remove(&self, key: &str) -> bool {
|
async fn remove(&self, key: &str) -> bool {
|
||||||
let mut cache = self.cache.write().await;
|
let mut cache = self.cache.write().await;
|
||||||
if let Some((_, cached)) = cache.pop(key) {
|
if let Some(cached) = cache.pop(key) {
|
||||||
let current = self.current_size.load(Ordering::Relaxed);
|
let current = self.current_size.load(Ordering::Relaxed);
|
||||||
self.current_size.store(current.saturating_sub(cached.size), Ordering::Relaxed);
|
self.current_size
|
||||||
|
.store(current.saturating_sub(cached.size), Ordering::Relaxed);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the most frequently accessed keys
|
/// Get the most frequently accessed keys
|
||||||
///
|
///
|
||||||
/// Returns up to `limit` keys sorted by hit count in descending order.
|
/// Returns up to `limit` keys sorted by hit count in descending order.
|
||||||
@@ -408,7 +409,7 @@ impl HotObjectCache {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (k.clone(), v.hit_count.load(Ordering::Relaxed)))
|
.map(|(k, v)| (k.clone(), v.hit_count.load(Ordering::Relaxed)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
keys_with_hits.sort_by(|a, b| b.1.cmp(&a.1));
|
keys_with_hits.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
keys_with_hits.truncate(limit);
|
keys_with_hits.truncate(limit);
|
||||||
keys_with_hits
|
keys_with_hits
|
||||||
@@ -491,35 +492,35 @@ impl ConcurrencyManager {
|
|||||||
pub async fn clear_cache(&self) {
|
pub async fn clear_cache(&self) {
|
||||||
self.cache.clear().await
|
self.cache.clear().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a key exists in cache
|
/// Check if a key exists in cache
|
||||||
///
|
///
|
||||||
/// This is a lightweight check that doesn't promote the key in LRU order.
|
/// This is a lightweight check that doesn't promote the key in LRU order.
|
||||||
pub async fn is_cached(&self, key: &str) -> bool {
|
pub async fn is_cached(&self, key: &str) -> bool {
|
||||||
self.cache.contains(key).await
|
self.cache.contains(key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get multiple objects from cache in batch
|
/// Get multiple objects from cache in batch
|
||||||
///
|
///
|
||||||
/// More efficient than individual get_cached() calls when fetching multiple objects.
|
/// More efficient than individual get_cached() calls when fetching multiple objects.
|
||||||
pub async fn get_cached_batch(&self, keys: &[String]) -> Vec<Option<Arc<Vec<u8>>>> {
|
pub async fn get_cached_batch(&self, keys: &[String]) -> Vec<Option<Arc<Vec<u8>>>> {
|
||||||
self.cache.get_batch(keys).await
|
self.cache.get_batch(keys).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a specific object from cache
|
/// Remove a specific object from cache
|
||||||
///
|
///
|
||||||
/// Returns true if the object was cached and removed.
|
/// Returns true if the object was cached and removed.
|
||||||
pub async fn remove_cached(&self, key: &str) -> bool {
|
pub async fn remove_cached(&self, key: &str) -> bool {
|
||||||
self.cache.remove(key).await
|
self.cache.remove(key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the most frequently accessed keys
|
/// Get the most frequently accessed keys
|
||||||
///
|
///
|
||||||
/// Useful for identifying hot objects and optimizing cache strategies.
|
/// Useful for identifying hot objects and optimizing cache strategies.
|
||||||
pub async fn get_hot_keys(&self, limit: usize) -> Vec<(String, usize)> {
|
pub async fn get_hot_keys(&self, limit: usize) -> Vec<(String, usize)> {
|
||||||
self.cache.get_hot_keys(limit).await
|
self.cache.get_hot_keys(limit).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Warm up cache with frequently accessed objects
|
/// Warm up cache with frequently accessed objects
|
||||||
///
|
///
|
||||||
/// This can be called during server startup or maintenance windows
|
/// This can be called during server startup or maintenance windows
|
||||||
|
|||||||
@@ -24,8 +24,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::storage::concurrency::{
|
use crate::storage::concurrency::{
|
||||||
ConcurrencyManager, GetObjectGuard,
|
ConcurrencyManager, GetObjectGuard, get_advanced_buffer_size, get_concurrency_aware_buffer_size,
|
||||||
get_concurrency_aware_buffer_size, get_advanced_buffer_size
|
|
||||||
};
|
};
|
||||||
use rustfs_config::{KI_B, MI_B};
|
use rustfs_config::{KI_B, MI_B};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -289,24 +288,24 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cache_batch_operations() {
|
async fn test_cache_batch_operations() {
|
||||||
let manager = ConcurrencyManager::new();
|
let manager = ConcurrencyManager::new();
|
||||||
|
|
||||||
// Cache multiple objects
|
// Cache multiple objects
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let key = format!("batch/object{}", i);
|
let key = format!("batch/object{}", i);
|
||||||
let data = vec![i as u8; 100 * KI_B]; // 100KB each
|
let data = vec![i as u8; 100 * KI_B]; // 100KB each
|
||||||
manager.cache_object(key, data).await;
|
manager.cache_object(key, data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test batch get
|
// Test batch get
|
||||||
let keys: Vec<String> = (0..10).map(|i| format!("batch/object{}", i)).collect();
|
let keys: Vec<String> = (0..10).map(|i| format!("batch/object{}", i)).collect();
|
||||||
let results = manager.get_cached_batch(&keys).await;
|
let results = manager.get_cached_batch(&keys).await;
|
||||||
|
|
||||||
assert_eq!(results.len(), 10, "Should return result for each key");
|
assert_eq!(results.len(), 10, "Should return result for each key");
|
||||||
for (i, result) in results.iter().enumerate() {
|
for (i, result) in results.iter().enumerate() {
|
||||||
assert!(result.is_some(), "Object {} should be in cache", i);
|
assert!(result.is_some(), "Object {} should be in cache", i);
|
||||||
assert_eq!(result.as_ref().unwrap().len(), 100 * KI_B);
|
assert_eq!(result.as_ref().unwrap().len(), 100 * KI_B);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test batch get with missing keys
|
// Test batch get with missing keys
|
||||||
let mixed_keys = vec![
|
let mixed_keys = vec![
|
||||||
"batch/object0".to_string(),
|
"batch/object0".to_string(),
|
||||||
@@ -315,7 +314,7 @@ mod tests {
|
|||||||
"missing/key2".to_string(),
|
"missing/key2".to_string(),
|
||||||
];
|
];
|
||||||
let mixed_results = manager.get_cached_batch(&mixed_keys).await;
|
let mixed_results = manager.get_cached_batch(&mixed_keys).await;
|
||||||
|
|
||||||
assert_eq!(mixed_results.len(), 4);
|
assert_eq!(mixed_results.len(), 4);
|
||||||
assert!(mixed_results[0].is_some(), "First key should exist");
|
assert!(mixed_results[0].is_some(), "First key should exist");
|
||||||
assert!(mixed_results[1].is_none(), "Second key should be missing");
|
assert!(mixed_results[1].is_none(), "Second key should be missing");
|
||||||
@@ -327,7 +326,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cache_warming() {
|
async fn test_cache_warming() {
|
||||||
let manager = ConcurrencyManager::new();
|
let manager = ConcurrencyManager::new();
|
||||||
|
|
||||||
// Prepare objects for warming
|
// Prepare objects for warming
|
||||||
let mut warm_objects = Vec::new();
|
let mut warm_objects = Vec::new();
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
@@ -335,14 +334,14 @@ mod tests {
|
|||||||
let data = vec![i as u8; 512 * KI_B]; // 512KB each
|
let data = vec![i as u8; 512 * KI_B]; // 512KB each
|
||||||
warm_objects.push((key, data));
|
warm_objects.push((key, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warm cache
|
// Warm cache
|
||||||
manager.warm_cache(warm_objects).await;
|
manager.warm_cache(warm_objects).await;
|
||||||
|
|
||||||
// Verify all objects are cached
|
// Verify all objects are cached
|
||||||
let stats = manager.cache_stats().await;
|
let stats = manager.cache_stats().await;
|
||||||
assert_eq!(stats.entries, 5, "All objects should be cached");
|
assert_eq!(stats.entries, 5, "All objects should be cached");
|
||||||
|
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
let key = format!("warm/object{}", i);
|
let key = format!("warm/object{}", i);
|
||||||
assert!(manager.is_cached(&key).await, "Object {} should be cached", i);
|
assert!(manager.is_cached(&key).await, "Object {} should be cached", i);
|
||||||
@@ -353,14 +352,14 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_hot_keys_tracking() {
|
async fn test_hot_keys_tracking() {
|
||||||
let manager = ConcurrencyManager::new();
|
let manager = ConcurrencyManager::new();
|
||||||
|
|
||||||
// Cache objects with different access patterns
|
// Cache objects with different access patterns
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
let key = format!("hot/object{}", i);
|
let key = format!("hot/object{}", i);
|
||||||
let data = vec![i as u8; 100 * KI_B];
|
let data = vec![i as u8; 100 * KI_B];
|
||||||
manager.cache_object(key, data).await;
|
manager.cache_object(key, data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate access patterns (object 0 and 1 are hot)
|
// Simulate access patterns (object 0 and 1 are hot)
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let _ = manager.get_cached("hot/object0").await;
|
let _ = manager.get_cached("hot/object0").await;
|
||||||
@@ -371,10 +370,10 @@ mod tests {
|
|||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let _ = manager.get_cached("hot/object2").await;
|
let _ = manager.get_cached("hot/object2").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get hot keys
|
// Get hot keys
|
||||||
let hot_keys = manager.get_hot_keys(3).await;
|
let hot_keys = manager.get_hot_keys(3).await;
|
||||||
|
|
||||||
assert_eq!(hot_keys.len(), 3, "Should return top 3 keys");
|
assert_eq!(hot_keys.len(), 3, "Should return top 3 keys");
|
||||||
assert_eq!(hot_keys[0].0, "hot/object0", "Most accessed should be first");
|
assert_eq!(hot_keys[0].0, "hot/object0", "Most accessed should be first");
|
||||||
assert!(hot_keys[0].1 >= 10, "Object 0 should have at least 10 hits");
|
assert!(hot_keys[0].1 >= 10, "Object 0 should have at least 10 hits");
|
||||||
@@ -386,22 +385,22 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cache_removal() {
|
async fn test_cache_removal() {
|
||||||
let manager = ConcurrencyManager::new();
|
let manager = ConcurrencyManager::new();
|
||||||
|
|
||||||
// Cache an object
|
// Cache an object
|
||||||
let key = "remove/test".to_string();
|
let key = "remove/test".to_string();
|
||||||
let data = vec![1u8; 100 * KI_B];
|
let data = vec![1u8; 100 * KI_B];
|
||||||
manager.cache_object(key.clone(), data).await;
|
manager.cache_object(key.clone(), data).await;
|
||||||
|
|
||||||
// Verify it's cached
|
// Verify it's cached
|
||||||
assert!(manager.is_cached(&key).await, "Object should be cached");
|
assert!(manager.is_cached(&key).await, "Object should be cached");
|
||||||
|
|
||||||
// Remove it
|
// Remove it
|
||||||
let removed = manager.remove_cached(&key).await;
|
let removed = manager.remove_cached(&key).await;
|
||||||
assert!(removed, "Should successfully remove cached object");
|
assert!(removed, "Should successfully remove cached object");
|
||||||
|
|
||||||
// Verify it's gone
|
// Verify it's gone
|
||||||
assert!(!manager.is_cached(&key).await, "Object should no longer be cached");
|
assert!(!manager.is_cached(&key).await, "Object should no longer be cached");
|
||||||
|
|
||||||
// Try to remove non-existent key
|
// Try to remove non-existent key
|
||||||
let not_removed = manager.remove_cached("nonexistent").await;
|
let not_removed = manager.remove_cached("nonexistent").await;
|
||||||
assert!(!not_removed, "Should return false for non-existent key");
|
assert!(!not_removed, "Should return false for non-existent key");
|
||||||
@@ -411,37 +410,34 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_advanced_buffer_sizing() {
|
async fn test_advanced_buffer_sizing() {
|
||||||
let base_buffer = 256 * KI_B; // 256KB base
|
let base_buffer = 256 * KI_B; // 256KB base
|
||||||
|
|
||||||
// Test small file optimization
|
// Test small file optimization
|
||||||
let small_size = get_advanced_buffer_size(128 * KI_B as i64, base_buffer, false);
|
let small_size = get_advanced_buffer_size(128 * KI_B as i64, base_buffer, false);
|
||||||
assert!(small_size < base_buffer, "Small files should use smaller buffers");
|
assert!(small_size < base_buffer, "Small files should use smaller buffers");
|
||||||
assert!(small_size >= 16 * KI_B, "Should not go below minimum");
|
assert!(small_size >= 16 * KI_B, "Should not go below minimum");
|
||||||
|
|
||||||
// Test sequential read optimization
|
// Test sequential read optimization
|
||||||
let _guard = ConcurrencyManager::track_request();
|
let _guard = ConcurrencyManager::track_request();
|
||||||
let sequential_size = get_advanced_buffer_size(10 * MI_B as i64, base_buffer, true);
|
let sequential_size = get_advanced_buffer_size(10 * MI_B as i64, base_buffer, true);
|
||||||
let random_size = get_advanced_buffer_size(10 * MI_B as i64, base_buffer, false);
|
let random_size = get_advanced_buffer_size(10 * MI_B as i64, base_buffer, false);
|
||||||
|
|
||||||
// Sequential reads should get larger buffers at low concurrency
|
// Sequential reads should get larger buffers at low concurrency
|
||||||
assert!(
|
assert!(
|
||||||
sequential_size >= random_size,
|
sequential_size >= random_size,
|
||||||
"Sequential reads should have equal or larger buffers at low concurrency"
|
"Sequential reads should have equal or larger buffers at low concurrency"
|
||||||
);
|
);
|
||||||
|
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
|
|
||||||
// Test high concurrency with large files
|
// Test high concurrency with large files
|
||||||
let mut guards = Vec::new();
|
let mut guards = Vec::new();
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
guards.push(ConcurrencyManager::track_request());
|
guards.push(ConcurrencyManager::track_request());
|
||||||
}
|
}
|
||||||
|
|
||||||
let high_concurrency_size = get_advanced_buffer_size(50 * MI_B as i64, base_buffer, false);
|
let high_concurrency_size = get_advanced_buffer_size(50 * MI_B as i64, base_buffer, false);
|
||||||
assert!(
|
assert!(high_concurrency_size <= base_buffer, "High concurrency should reduce buffer size");
|
||||||
high_concurrency_size <= base_buffer,
|
|
||||||
"High concurrency should reduce buffer size"
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(guards);
|
drop(guards);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,14 +445,14 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_concurrent_cache_access() {
|
async fn test_concurrent_cache_access() {
|
||||||
let manager = Arc::new(ConcurrencyManager::new());
|
let manager = Arc::new(ConcurrencyManager::new());
|
||||||
|
|
||||||
// Pre-populate cache
|
// Pre-populate cache
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
let key = format!("concurrent/object{}", i);
|
let key = format!("concurrent/object{}", i);
|
||||||
let data = vec![i as u8; 100 * KI_B];
|
let data = vec![i as u8; 100 * KI_B];
|
||||||
manager.cache_object(key, data).await;
|
manager.cache_object(key, data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn multiple concurrent readers
|
// Spawn multiple concurrent readers
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
for worker_id in 0..10 {
|
for worker_id in 0..10 {
|
||||||
@@ -473,7 +469,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all workers to complete
|
// Wait for all workers to complete
|
||||||
let mut total_hits = 0;
|
let mut total_hits = 0;
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
@@ -481,7 +477,7 @@ mod tests {
|
|||||||
println!("Worker {} got {} cache hits", worker_id, hits);
|
println!("Worker {} got {} cache hits", worker_id, hits);
|
||||||
total_hits += hits;
|
total_hits += hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All workers should get hits for all cached objects
|
// All workers should get hits for all cached objects
|
||||||
assert_eq!(total_hits, 500, "Should have 500 total hits (10 workers * 50 objects)");
|
assert_eq!(total_hits, 500, "Should have 500 total hits (10 workers * 50 objects)");
|
||||||
}
|
}
|
||||||
@@ -490,23 +486,23 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_is_cached_no_promotion() {
|
async fn test_is_cached_no_promotion() {
|
||||||
let manager = ConcurrencyManager::new();
|
let manager = ConcurrencyManager::new();
|
||||||
|
|
||||||
// Cache two objects
|
// Cache two objects
|
||||||
manager.cache_object("first".to_string(), vec![1u8; 100 * KI_B]).await;
|
manager.cache_object("first".to_string(), vec![1u8; 100 * KI_B]).await;
|
||||||
manager.cache_object("second".to_string(), vec![2u8; 100 * KI_B]).await;
|
manager.cache_object("second".to_string(), vec![2u8; 100 * KI_B]).await;
|
||||||
|
|
||||||
// Check first without accessing it
|
// Check first without accessing it
|
||||||
assert!(manager.is_cached("first").await);
|
assert!(manager.is_cached("first").await);
|
||||||
|
|
||||||
// Access second multiple times
|
// Access second multiple times
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
let _ = manager.get_cached("second").await;
|
let _ = manager.get_cached("second").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both should still be cached
|
// Both should still be cached
|
||||||
assert!(manager.is_cached("first").await);
|
assert!(manager.is_cached("first").await);
|
||||||
assert!(manager.is_cached("second").await);
|
assert!(manager.is_cached("second").await);
|
||||||
|
|
||||||
// Get hot keys - second should be hotter
|
// Get hot keys - second should be hotter
|
||||||
let hot_keys = manager.get_hot_keys(2).await;
|
let hot_keys = manager.get_hot_keys(2).await;
|
||||||
assert_eq!(hot_keys[0].0, "second", "Second should be hottest");
|
assert_eq!(hot_keys[0].0, "second", "Second should be hottest");
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ use rustfs_utils::{
|
|||||||
use rustfs_zip::CompressionFormat;
|
use rustfs_zip::CompressionFormat;
|
||||||
use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH};
|
use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH};
|
||||||
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@@ -132,7 +133,6 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
use std::convert::Infallible;
|
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
use tokio::{io::AsyncRead, sync::mpsc};
|
use tokio::{io::AsyncRead, sync::mpsc};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
@@ -1645,9 +1645,10 @@ impl S3 for FS {
|
|||||||
if let Some(cached_data) = manager.get_cached(&cache_key).await {
|
if let Some(cached_data) = manager.get_cached(&cache_key).await {
|
||||||
debug!("Serving object from cache: {}", cache_key);
|
debug!("Serving object from cache: {}", cache_key);
|
||||||
|
|
||||||
|
let value = cached_data.clone();
|
||||||
// Build response from cached data
|
// Build response from cached data
|
||||||
let body = Some(StreamingBlob::wrap::<_, Infallible>(futures::stream::once(async move {
|
let body = Some(StreamingBlob::wrap::<_, Infallible>(futures::stream::once(async move {
|
||||||
Ok(bytes::Bytes::from((*cached_data).clone()))
|
Ok(bytes::Bytes::from((*value).clone()))
|
||||||
})));
|
})));
|
||||||
|
|
||||||
let output = GetObjectOutput {
|
let output = GetObjectOutput {
|
||||||
|
|||||||
Reference in New Issue
Block a user