#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use bytes::Bytes;
use futures::future::BoxFuture;
use crate::LogicalLayoutHandle;
use crate::object::{LockFileContent, ObjectBlockOps, ObjectLockManager};
use crate::offload::{
BoxFuture as PolicyBoxFuture, EvalContext, ObjectLockPresenceFilter, OffloadPolicy,
PendingTracker,
};
use crate::{BlockId, G2, SequenceHash};
fn test_hash(n: u64) -> SequenceHash {
SequenceHash::new(n, None, 0)
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn timestamp_to_string(secs: u64) -> String {
format!("{}", secs)
}
fn parse_timestamp(s: &str) -> Option<u64> {
s.parse().ok()
}
fn is_expired_timestamp(deadline_str: &str) -> bool {
if let Some(deadline) = parse_timestamp(deadline_str) {
now_secs() > deadline
} else {
true }
}
#[derive(Debug, Default)]
struct MockObjectStorage {
objects: RwLock<HashMap<String, Bytes>>,
}
impl MockObjectStorage {
fn new() -> Self {
Self {
objects: RwLock::new(HashMap::new()),
}
}
fn has_object(&self, key: &str) -> bool {
self.objects.read().unwrap().contains_key(key)
}
fn get_object(&self, key: &str) -> Option<Bytes> {
self.objects.read().unwrap().get(key).cloned()
}
fn put_object(&self, key: &str, data: Bytes) {
self.objects.write().unwrap().insert(key.to_string(), data);
}
fn delete_object(&self, key: &str) -> bool {
self.objects.write().unwrap().remove(key).is_some()
}
fn put_if_not_exists(&self, key: &str, data: Bytes) -> bool {
let mut objects = self.objects.write().unwrap();
if objects.contains_key(key) {
false
} else {
objects.insert(key.to_string(), data);
true
}
}
#[allow(dead_code)]
fn list_keys(&self) -> Vec<String> {
self.objects.read().unwrap().keys().cloned().collect()
}
}
#[allow(dead_code)]
struct MockObjectBlockClient {
storage: Arc<MockObjectStorage>,
}
#[allow(dead_code)]
impl MockObjectBlockClient {
fn new(storage: Arc<MockObjectStorage>) -> Self {
Self { storage }
}
}
impl ObjectBlockOps for MockObjectBlockClient {
fn has_blocks(
&self,
keys: Vec<SequenceHash>,
) -> BoxFuture<'static, Vec<(SequenceHash, Option<usize>)>> {
let storage = self.storage.clone();
Box::pin(async move {
keys.into_iter()
.map(|hash| {
let key = format!("{:?}", hash);
let size = storage.get_object(&key).map(|b| b.len());
(hash, size)
})
.collect()
})
}
fn put_blocks(
&self,
keys: Vec<SequenceHash>,
_layout: LogicalLayoutHandle,
_block_ids: Vec<BlockId>,
) -> BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
let storage = self.storage.clone();
Box::pin(async move {
keys.into_iter()
.map(|hash| {
let key = format!("{:?}", hash);
storage.put_object(&key, Bytes::from("mock_block_data"));
Ok(hash)
})
.collect()
})
}
fn get_blocks(
&self,
keys: Vec<SequenceHash>,
_layout: LogicalLayoutHandle,
_block_ids: Vec<BlockId>,
) -> BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
let storage = self.storage.clone();
Box::pin(async move {
keys.into_iter()
.map(|hash| {
let key = format!("{:?}", hash);
if storage.has_object(&key) {
Ok(hash)
} else {
Err(hash)
}
})
.collect()
})
}
}
struct MockLockManager {
storage: Arc<MockObjectStorage>,
instance_id: String,
lock_timeout_secs: u64,
}
impl MockLockManager {
fn new(storage: Arc<MockObjectStorage>, instance_id: String) -> Self {
Self {
storage,
instance_id,
lock_timeout_secs: 300,
}
}
fn lock_key(hash: &SequenceHash) -> String {
format!("{:?}.lock", hash)
}
fn meta_key(hash: &SequenceHash) -> String {
format!("{:?}.meta", hash)
}
fn create_lock_content(&self) -> LockFileContent {
LockFileContent {
instance_id: self.instance_id.clone(),
acquired_at: timestamp_to_string(now_secs()),
deadline: timestamp_to_string(now_secs() + self.lock_timeout_secs),
}
}
}
impl ObjectLockManager for MockLockManager {
fn has_meta(&self, hash: SequenceHash) -> PolicyBoxFuture<'static, Result<bool>> {
let storage = self.storage.clone();
Box::pin(async move {
let meta_key = Self::meta_key(&hash);
Ok(storage.has_object(&meta_key))
})
}
fn try_acquire_lock(&self, hash: SequenceHash) -> PolicyBoxFuture<'static, Result<bool>> {
let storage = self.storage.clone();
let lock_content = self.create_lock_content();
let our_instance_id = self.instance_id.clone();
Box::pin(async move {
let lock_key = Self::lock_key(&hash);
let lock_data =
serde_json::to_vec(&lock_content).expect("Failed to serialize lock content");
if storage.put_if_not_exists(&lock_key, Bytes::from(lock_data.clone())) {
return Ok(true); }
if let Some(existing_data) = storage.get_object(&lock_key)
&& let Ok(existing_lock) =
serde_json::from_slice::<LockFileContent>(&existing_data)
{
if existing_lock.instance_id == our_instance_id {
return Ok(true);
}
if is_expired_timestamp(&existing_lock.deadline) {
storage.put_object(&lock_key, Bytes::from(lock_data));
return Ok(true);
}
}
Ok(false) })
}
fn create_meta(&self, hash: SequenceHash) -> PolicyBoxFuture<'static, Result<()>> {
let storage = self.storage.clone();
Box::pin(async move {
let meta_key = Self::meta_key(&hash);
storage.put_object(&meta_key, Bytes::new());
Ok(())
})
}
fn release_lock(&self, hash: SequenceHash) -> PolicyBoxFuture<'static, Result<()>> {
let storage = self.storage.clone();
Box::pin(async move {
let lock_key = Self::lock_key(&hash);
storage.delete_object(&lock_key);
Ok(())
})
}
}
#[tokio::test]
async fn test_lock_manager_acquire_and_release() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager = MockLockManager::new(storage.clone(), "test-instance".to_string());
let hash = test_hash(12345);
assert!(!storage.has_object(&MockLockManager::lock_key(&hash)));
assert!(!storage.has_object(&MockLockManager::meta_key(&hash)));
let acquired = lock_manager.try_acquire_lock(hash).await?;
assert!(acquired, "Should acquire lock");
assert!(storage.has_object(&MockLockManager::lock_key(&hash)));
let lock_data = storage
.get_object(&MockLockManager::lock_key(&hash))
.unwrap();
let lock_content: LockFileContent = serde_json::from_slice(&lock_data)?;
assert_eq!(lock_content.instance_id, "test-instance");
lock_manager.create_meta(hash).await?;
assert!(storage.has_object(&MockLockManager::meta_key(&hash)));
lock_manager.release_lock(hash).await?;
assert!(!storage.has_object(&MockLockManager::lock_key(&hash)));
assert!(storage.has_object(&MockLockManager::meta_key(&hash)));
eprintln!("✓ Lock acquisition and release test passed");
Ok(())
}
#[tokio::test]
async fn test_lock_manager_reacquire_own_lock() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager = MockLockManager::new(storage.clone(), "test-instance".to_string());
let hash = test_hash(12345);
let acquired1 = lock_manager.try_acquire_lock(hash).await?;
assert!(acquired1, "Should acquire lock first time");
let acquired2 = lock_manager.try_acquire_lock(hash).await?;
assert!(
acquired2,
"Same instance should be able to re-acquire its lock"
);
eprintln!("✓ Lock re-acquisition test passed");
Ok(())
}
#[tokio::test]
async fn test_lock_manager_contention() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager1 = MockLockManager::new(storage.clone(), "instance-1".to_string());
let lock_manager2 = MockLockManager::new(storage.clone(), "instance-2".to_string());
let hash = test_hash(12345);
let acquired1 = lock_manager1.try_acquire_lock(hash).await?;
assert!(acquired1, "Instance 1 should acquire lock");
let acquired2 = lock_manager2.try_acquire_lock(hash).await?;
assert!(
!acquired2,
"Instance 2 should NOT acquire lock held by instance 1"
);
let acquired1_again = lock_manager1.try_acquire_lock(hash).await?;
assert!(acquired1_again, "Instance 1 should re-acquire its own lock");
eprintln!("✓ Lock contention test passed");
Ok(())
}
#[tokio::test]
async fn test_lock_manager_expired_lock_overwrite() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager = MockLockManager::new(storage.clone(), "new-instance".to_string());
let hash = test_hash(12345);
let expired_lock = LockFileContent {
instance_id: "old-instance".to_string(),
acquired_at: timestamp_to_string(0), deadline: timestamp_to_string(1), };
let lock_key = MockLockManager::lock_key(&hash);
storage.put_object(&lock_key, Bytes::from(serde_json::to_vec(&expired_lock)?));
let acquired = lock_manager.try_acquire_lock(hash).await?;
assert!(acquired, "Should acquire expired lock");
let lock_data = storage.get_object(&lock_key).unwrap();
let lock_content: LockFileContent = serde_json::from_slice(&lock_data)?;
assert_eq!(lock_content.instance_id, "new-instance");
eprintln!("✓ Expired lock overwrite test passed");
Ok(())
}
#[tokio::test]
async fn test_lock_manager_has_meta() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager = MockLockManager::new(storage.clone(), "test-instance".to_string());
let hash = test_hash(12345);
let has_meta_before = lock_manager.has_meta(hash).await?;
assert!(!has_meta_before, "Should not have meta initially");
lock_manager.create_meta(hash).await?;
let has_meta_after = lock_manager.has_meta(hash).await?;
assert!(has_meta_after, "Should have meta after creation");
eprintln!("✓ Has meta test passed");
Ok(())
}
#[tokio::test]
async fn test_policy_passes_new_blocks() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager: Arc<dyn ObjectLockManager> = Arc::new(MockLockManager::new(
storage.clone(),
"test-instance".to_string(),
));
let filter = ObjectLockPresenceFilter::<G2>::new(lock_manager);
let hash = test_hash(12345);
let ctx = EvalContext::<G2>::from_weak(BlockId::default(), hash);
let result = match filter.evaluate(&ctx) {
futures::future::Either::Left(ready) => ready.await,
futures::future::Either::Right(boxed) => boxed.await,
};
assert!(result?, "Policy should pass for new block");
assert!(storage.has_object(&MockLockManager::lock_key(&hash)));
eprintln!("✓ Policy passes new blocks test passed");
Ok(())
}
#[tokio::test]
async fn test_policy_filters_blocks_with_meta() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let hash = test_hash(12345);
let meta_key = MockLockManager::meta_key(&hash);
storage.put_object(&meta_key, Bytes::new());
let lock_manager: Arc<dyn ObjectLockManager> = Arc::new(MockLockManager::new(
storage.clone(),
"test-instance".to_string(),
));
let filter = ObjectLockPresenceFilter::<G2>::new(lock_manager);
let ctx = EvalContext::<G2>::from_weak(BlockId::default(), hash);
let result = match filter.evaluate(&ctx) {
futures::future::Either::Left(ready) => ready.await,
futures::future::Either::Right(boxed) => boxed.await,
};
assert!(!result?, "Policy should filter block with existing meta");
eprintln!("✓ Policy filters blocks with meta test passed");
Ok(())
}
#[tokio::test]
async fn test_policy_filters_locked_blocks() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let hash = test_hash(12345);
let lock_content = LockFileContent {
instance_id: "other-instance".to_string(),
acquired_at: timestamp_to_string(now_secs()),
deadline: timestamp_to_string(now_secs() + 300), };
let lock_key = MockLockManager::lock_key(&hash);
storage.put_object(&lock_key, Bytes::from(serde_json::to_vec(&lock_content)?));
let lock_manager: Arc<dyn ObjectLockManager> = Arc::new(MockLockManager::new(
storage.clone(),
"test-instance".to_string(),
));
let filter = ObjectLockPresenceFilter::<G2>::new(lock_manager);
let ctx = EvalContext::<G2>::from_weak(BlockId::default(), hash);
let result = match filter.evaluate(&ctx) {
futures::future::Either::Left(ready) => ready.await,
futures::future::Either::Right(boxed) => boxed.await,
};
assert!(
!result?,
"Policy should filter block locked by another instance"
);
eprintln!("✓ Policy filters locked blocks test passed");
Ok(())
}
#[tokio::test]
async fn test_policy_filters_pending_blocks() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let lock_manager: Arc<dyn ObjectLockManager> = Arc::new(MockLockManager::new(
storage.clone(),
"test-instance".to_string(),
));
let pending_tracker = Arc::new(PendingTracker::new());
let filter = ObjectLockPresenceFilter::<G2>::new(lock_manager)
.with_pending_tracker(pending_tracker.clone());
let hash = test_hash(12345);
let _guard = pending_tracker.guard(hash);
let ctx = EvalContext::<G2>::from_weak(BlockId::default(), hash);
let result = match filter.evaluate(&ctx) {
futures::future::Either::Left(ready) => ready.await,
futures::future::Either::Right(boxed) => boxed.await,
};
assert!(!result?, "Policy should filter pending block");
eprintln!("✓ Policy filters pending blocks test passed");
Ok(())
}
#[tokio::test]
async fn test_policy_batch_evaluation() -> Result<()> {
let storage = Arc::new(MockObjectStorage::new());
let hash1 = test_hash(1);
let hash2 = test_hash(2);
let hash3 = test_hash(3);
storage.put_object(&MockLockManager::meta_key(&hash1), Bytes::new());
let lock_manager: Arc<dyn ObjectLockManager> = Arc::new(MockLockManager::new(
storage.clone(),
"test-instance".to_string(),
));
let filter = ObjectLockPresenceFilter::<G2>::new(lock_manager);
let contexts = vec![
EvalContext::<G2>::from_weak(0, hash1), EvalContext::<G2>::from_weak(1, hash2), EvalContext::<G2>::from_weak(2, hash3), ];
let result = match filter.evaluate_batch(&contexts) {
futures::future::Either::Left(ready) => ready.await,
futures::future::Either::Right(boxed) => boxed.await,
};
let results = result?;
assert_eq!(results.len(), 3);
assert!(!results[0], "Block with meta should be filtered");
assert!(results[1], "New block should pass");
assert!(results[2], "New block should pass");
eprintln!("✓ Policy batch evaluation test passed");
Ok(())
}
}