mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-17 01:30:33 +00:00
feat: add comprehensive tests for DRWMutex and fix critical bugs
This commit is contained in:
@@ -100,6 +100,12 @@ impl DRWMutex {
|
||||
|
||||
pub async fn lock_blocking(&mut self, id: &String, source: &String, is_read_lock: bool, opts: &Options) -> bool {
|
||||
let locker_len = self.lockers.len();
|
||||
|
||||
// Handle edge case: no lockers available
|
||||
if locker_len == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut tolerance = locker_len / 2;
|
||||
let mut quorum = locker_len - tolerance;
|
||||
if !is_read_lock {
|
||||
@@ -113,7 +119,9 @@ impl DRWMutex {
|
||||
}
|
||||
info!("lockBlocking {}/{} for {:?}: lockType readLock({}), additional opts: {:?}, quorum: {}, tolerance: {}, lockClients: {}\n", id, source, self.names, is_read_lock, opts, quorum, tolerance, locker_len);
|
||||
|
||||
tolerance = locker_len - quorum;
|
||||
// Recalculate tolerance after potential quorum adjustment
|
||||
// Use saturating_sub to prevent underflow
|
||||
tolerance = locker_len.saturating_sub(quorum);
|
||||
let mut attempt = 0;
|
||||
let mut locks = vec!["".to_string(); self.lockers.len()];
|
||||
|
||||
@@ -293,10 +301,19 @@ fn check_failed_unlocks(locks: &[String], tolerance: usize) -> bool {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle edge case: if tolerance is greater than or equal to locks.len(),
|
||||
// we can tolerate all failures, so return false (no critical failure)
|
||||
if tolerance >= locks.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case: when locks.len() - tolerance == tolerance (i.e., locks.len() == 2 * tolerance)
|
||||
// This happens when we have an even number of lockers and tolerance is exactly half
|
||||
if locks.len() - tolerance == tolerance {
|
||||
return un_locks_failed >= tolerance;
|
||||
}
|
||||
|
||||
// Normal case: failure if more than tolerance unlocks failed
|
||||
un_locks_failed > tolerance
|
||||
}
|
||||
|
||||
@@ -353,3 +370,787 @@ fn check_quorum_locked(locks: &[String], quorum: usize) -> bool {
|
||||
|
||||
count >= quorum
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use common::error::{Error, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// Mock locker for testing
|
||||
#[derive(Debug, Clone)]
|
||||
struct MockLocker {
|
||||
id: String,
|
||||
state: Arc<Mutex<MockLockerState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockLockerState {
|
||||
locks: HashMap<String, String>, // uid -> owner
|
||||
read_locks: HashMap<String, String>, // uid -> owner
|
||||
should_fail: bool,
|
||||
is_online: bool,
|
||||
}
|
||||
|
||||
impl MockLocker {
|
||||
fn new(id: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
state: Arc::new(Mutex::new(MockLockerState {
|
||||
is_online: true,
|
||||
..Default::default()
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_should_fail(&self, should_fail: bool) {
|
||||
self.state.lock().unwrap().should_fail = should_fail;
|
||||
}
|
||||
|
||||
fn set_online(&self, online: bool) {
|
||||
self.state.lock().unwrap().is_online = online;
|
||||
}
|
||||
|
||||
fn get_lock_count(&self) -> usize {
|
||||
self.state.lock().unwrap().locks.len()
|
||||
}
|
||||
|
||||
fn get_read_lock_count(&self) -> usize {
|
||||
self.state.lock().unwrap().read_locks.len()
|
||||
}
|
||||
|
||||
fn has_lock(&self, uid: &str) -> bool {
|
||||
self.state.lock().unwrap().locks.contains_key(uid)
|
||||
}
|
||||
|
||||
fn has_read_lock(&self, uid: &str) -> bool {
|
||||
self.state.lock().unwrap().read_locks.contains_key(uid)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Locker for MockLocker {
|
||||
async fn lock(&mut self, args: &LockArgs) -> Result<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.should_fail {
|
||||
return Err(Error::from_string("Mock lock failure"));
|
||||
}
|
||||
if !state.is_online {
|
||||
return Err(Error::from_string("Mock locker offline"));
|
||||
}
|
||||
|
||||
// Check if already locked
|
||||
if state.locks.contains_key(&args.uid) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
state.locks.insert(args.uid.clone(), args.owner.clone());
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn unlock(&mut self, args: &LockArgs) -> Result<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.should_fail {
|
||||
return Err(Error::from_string("Mock unlock failure"));
|
||||
}
|
||||
|
||||
Ok(state.locks.remove(&args.uid).is_some())
|
||||
}
|
||||
|
||||
async fn rlock(&mut self, args: &LockArgs) -> Result<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.should_fail {
|
||||
return Err(Error::from_string("Mock rlock failure"));
|
||||
}
|
||||
if !state.is_online {
|
||||
return Err(Error::from_string("Mock locker offline"));
|
||||
}
|
||||
|
||||
// Check if write lock exists
|
||||
if state.locks.contains_key(&args.uid) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
state.read_locks.insert(args.uid.clone(), args.owner.clone());
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn runlock(&mut self, args: &LockArgs) -> Result<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.should_fail {
|
||||
return Err(Error::from_string("Mock runlock failure"));
|
||||
}
|
||||
|
||||
Ok(state.read_locks.remove(&args.uid).is_some())
|
||||
}
|
||||
|
||||
async fn refresh(&mut self, _args: &LockArgs) -> Result<bool> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if state.should_fail {
|
||||
return Err(Error::from_string("Mock refresh failure"));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn force_unlock(&mut self, args: &LockArgs) -> Result<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let removed_lock = state.locks.remove(&args.uid).is_some();
|
||||
let removed_read_lock = state.read_locks.remove(&args.uid).is_some();
|
||||
Ok(removed_lock || removed_read_lock)
|
||||
}
|
||||
|
||||
async fn close(&self) {}
|
||||
|
||||
async fn is_online(&self) -> bool {
|
||||
self.state.lock().unwrap().is_online
|
||||
}
|
||||
|
||||
async fn is_local(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn create_mock_lockers(count: usize) -> Vec<LockApi> {
|
||||
// For testing, we'll use Local lockers which use the global local server
|
||||
(0..count).map(|_| LockApi::Local).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_new() {
|
||||
let names = vec!["resource1".to_string(), "resource2".to_string()];
|
||||
let lockers = create_mock_lockers(3);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names.clone(), lockers);
|
||||
|
||||
assert_eq!(mutex.owner, "owner1");
|
||||
assert_eq!(mutex.names.len(), 2);
|
||||
assert_eq!(mutex.lockers.len(), 3);
|
||||
assert_eq!(mutex.write_locks.len(), 3);
|
||||
assert_eq!(mutex.read_locks.len(), 3);
|
||||
assert_eq!(mutex.refresh_interval, DRW_MUTEX_REFRESH_INTERVAL);
|
||||
assert_eq!(mutex.lock_retry_min_interval, LOCK_RETRY_MIN_INTERVAL);
|
||||
|
||||
// Names should be sorted
|
||||
let mut expected_names = names;
|
||||
expected_names.sort();
|
||||
assert_eq!(mutex.names, expected_names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_new_empty_names() {
|
||||
let names = vec![];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
assert_eq!(mutex.names.len(), 0);
|
||||
assert_eq!(mutex.lockers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_new_single_locker() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
assert_eq!(mutex.lockers.len(), 1);
|
||||
assert_eq!(mutex.write_locks.len(), 1);
|
||||
assert_eq!(mutex.read_locks.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_locked_function() {
|
||||
assert!(!is_locked(""));
|
||||
assert!(is_locked("some-uid"));
|
||||
assert!(is_locked("any-non-empty-string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_granted_is_locked() {
|
||||
let granted_empty = Granted {
|
||||
index: 0,
|
||||
lock_uid: "".to_string(),
|
||||
};
|
||||
assert!(!granted_empty.is_locked());
|
||||
|
||||
let granted_locked = Granted {
|
||||
index: 1,
|
||||
lock_uid: "test-uid".to_string(),
|
||||
};
|
||||
assert!(granted_locked.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_is_locked() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(2);
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
// Initially not locked
|
||||
assert!(!mutex.is_locked());
|
||||
assert!(!mutex.is_r_locked());
|
||||
|
||||
// Set write locks
|
||||
mutex.write_locks[0] = "test-uid".to_string();
|
||||
assert!(mutex.is_locked());
|
||||
assert!(!mutex.is_r_locked());
|
||||
|
||||
// Clear write locks, set read locks
|
||||
mutex.write_locks[0] = "".to_string();
|
||||
mutex.read_locks[1] = "read-uid".to_string();
|
||||
assert!(!mutex.is_locked());
|
||||
assert!(mutex.is_r_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_options_debug() {
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(5),
|
||||
retry_interval: Duration::from_millis(100),
|
||||
};
|
||||
let debug_str = format!("{:?}", opts);
|
||||
assert!(debug_str.contains("timeout"));
|
||||
assert!(debug_str.contains("retry_interval"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_quorum_locked() {
|
||||
// Test with empty locks
|
||||
assert!(!check_quorum_locked(&[], 1));
|
||||
|
||||
// Test with all empty locks
|
||||
let locks = vec!["".to_string(), "".to_string(), "".to_string()];
|
||||
assert!(!check_quorum_locked(&locks, 1));
|
||||
assert!(!check_quorum_locked(&locks, 2));
|
||||
|
||||
// Test with some locks
|
||||
let locks = vec!["uid1".to_string(), "".to_string(), "uid3".to_string()];
|
||||
assert!(check_quorum_locked(&locks, 1));
|
||||
assert!(check_quorum_locked(&locks, 2));
|
||||
assert!(!check_quorum_locked(&locks, 3));
|
||||
|
||||
// Test with all locks
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
|
||||
assert!(check_quorum_locked(&locks, 1));
|
||||
assert!(check_quorum_locked(&locks, 2));
|
||||
assert!(check_quorum_locked(&locks, 3));
|
||||
assert!(!check_quorum_locked(&locks, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_failed_unlocks() {
|
||||
// Test with empty locks
|
||||
assert!(!check_failed_unlocks(&[], 0)); // tolerance >= locks.len(), so no critical failure
|
||||
assert!(!check_failed_unlocks(&[], 1)); // tolerance >= locks.len(), so no critical failure
|
||||
|
||||
// Test with all unlocked
|
||||
let locks = vec!["".to_string(), "".to_string(), "".to_string()];
|
||||
assert!(!check_failed_unlocks(&locks, 1)); // 0 failed <= tolerance 1
|
||||
assert!(!check_failed_unlocks(&locks, 2)); // 0 failed <= tolerance 2
|
||||
|
||||
// Test with some failed unlocks
|
||||
let locks = vec!["uid1".to_string(), "".to_string(), "uid3".to_string()];
|
||||
assert!(check_failed_unlocks(&locks, 1)); // 2 failed > tolerance 1
|
||||
assert!(!check_failed_unlocks(&locks, 2)); // 2 failed <= tolerance 2
|
||||
|
||||
// Test special case: locks.len() - tolerance == tolerance
|
||||
// This means locks.len() == 2 * tolerance
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string()]; // len = 2
|
||||
let tolerance = 1; // 2 - 1 == 1
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 2 failed >= tolerance 1
|
||||
|
||||
let locks = vec!["".to_string(), "uid2".to_string()]; // len = 2, 1 failed
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 1 failed >= tolerance 1
|
||||
|
||||
let locks = vec!["".to_string(), "".to_string()]; // len = 2, 0 failed
|
||||
assert!(!check_failed_unlocks(&locks, tolerance)); // 0 failed < tolerance 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_failed_unlocks_edge_cases() {
|
||||
// Test with zero tolerance
|
||||
let locks = vec!["uid1".to_string()];
|
||||
assert!(check_failed_unlocks(&locks, 0)); // 1 failed > tolerance 0
|
||||
|
||||
// Test with tolerance equal to lock count
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string()];
|
||||
assert!(!check_failed_unlocks(&locks, 2)); // 2 failed <= tolerance 2
|
||||
|
||||
// Test with tolerance greater than lock count
|
||||
let locks = vec!["uid1".to_string()];
|
||||
assert!(!check_failed_unlocks(&locks, 5)); // 1 failed <= tolerance 5
|
||||
}
|
||||
|
||||
// Async tests using the local locker infrastructure
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_lock_basic_functionality() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1); // Single locker for simplicity
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let id = "test-lock-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
// Test get_lock (result depends on local locker state)
|
||||
let result = mutex.get_lock(&id, &source, &opts).await;
|
||||
// Just ensure the method doesn't panic and returns a boolean
|
||||
assert!(result == true || result == false);
|
||||
|
||||
// If lock was acquired, test unlock
|
||||
if result {
|
||||
assert!(mutex.is_locked(), "Mutex should be in locked state");
|
||||
mutex.un_lock().await;
|
||||
assert!(!mutex.is_locked(), "Mutex should be unlocked after un_lock");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_rlock_basic_functionality() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1); // Single locker for simplicity
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let id = "test-rlock-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
// Test get_r_lock (result depends on local locker state)
|
||||
let result = mutex.get_r_lock(&id, &source, &opts).await;
|
||||
// Just ensure the method doesn't panic and returns a boolean
|
||||
assert!(result == true || result == false);
|
||||
|
||||
// If read lock was acquired, test runlock
|
||||
if result {
|
||||
assert!(mutex.is_r_locked(), "Mutex should be in read locked state");
|
||||
mutex.un_r_lock().await;
|
||||
assert!(!mutex.is_r_locked(), "Mutex should be unlocked after un_r_lock");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_lock_with_multiple_lockers() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(3); // 3 lockers, need quorum of 2
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let id = "test-lock-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
// With 3 local lockers, the quorum calculation should be:
|
||||
// tolerance = 3 / 2 = 1
|
||||
// quorum = 3 - 1 = 2
|
||||
// Since it's a write lock and quorum != tolerance, quorum stays 2
|
||||
// The result depends on the actual locker implementation
|
||||
let result = mutex.get_lock(&id, &source, &opts).await;
|
||||
// We don't assert success/failure here since it depends on the local locker state
|
||||
// Just ensure the method doesn't panic and returns a boolean
|
||||
assert!(result == true || result == false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_unlock_without_lock() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
// Try to unlock without having a lock - should not panic
|
||||
mutex.un_lock().await;
|
||||
assert!(!mutex.is_locked());
|
||||
|
||||
// Try to unlock read lock without having one - should not panic
|
||||
mutex.un_r_lock().await;
|
||||
assert!(!mutex.is_r_locked());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_multiple_resources() {
|
||||
let names = vec![
|
||||
"resource1".to_string(),
|
||||
"resource2".to_string(),
|
||||
"resource3".to_string(),
|
||||
];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names.clone(), lockers);
|
||||
|
||||
// Names should be sorted
|
||||
let mut expected_names = names;
|
||||
expected_names.sort();
|
||||
assert_eq!(mutex.names, expected_names);
|
||||
|
||||
let id = "test-lock-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
let result = mutex.get_lock(&id, &source, &opts).await;
|
||||
// The result depends on the actual locker implementation
|
||||
// Just ensure the method doesn't panic and returns a boolean
|
||||
assert!(result == true || result == false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_concurrent_read_locks() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mut mutex1 = DRWMutex::new("owner1".to_string(), names.clone(), lockers.clone());
|
||||
let mut mutex2 = DRWMutex::new("owner2".to_string(), names, create_mock_lockers(1));
|
||||
|
||||
let id1 = "test-rlock-id1".to_string();
|
||||
let id2 = "test-rlock-id2".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
// Both should be able to acquire read locks
|
||||
let result1 = mutex1.get_r_lock(&id1, &source, &opts).await;
|
||||
let result2 = mutex2.get_r_lock(&id2, &source, &opts).await;
|
||||
|
||||
assert!(result1, "First read lock should succeed");
|
||||
assert!(result2, "Second read lock should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_release_with_empty_uid() {
|
||||
let mut locker = LockApi::Local;
|
||||
let result = send_release(&mut locker, &"".to_string(), "owner", &["resource".to_string()], false).await;
|
||||
assert!(!result, "send_release should return false for empty uid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_debug() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let debug_str = format!("{:?}", mutex);
|
||||
assert!(debug_str.contains("DRWMutex"));
|
||||
assert!(debug_str.contains("owner"));
|
||||
assert!(debug_str.contains("names"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_granted_default() {
|
||||
let granted = Granted::default();
|
||||
assert_eq!(granted.index, 0);
|
||||
assert_eq!(granted.lock_uid, "");
|
||||
assert!(!granted.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_granted_clone() {
|
||||
let granted = Granted {
|
||||
index: 5,
|
||||
lock_uid: "test-uid".to_string(),
|
||||
};
|
||||
let cloned = granted.clone();
|
||||
assert_eq!(granted.index, cloned.index);
|
||||
assert_eq!(granted.lock_uid, cloned.lock_uid);
|
||||
}
|
||||
|
||||
// Test potential bug scenarios
|
||||
#[test]
|
||||
fn test_potential_bug_check_failed_unlocks_logic() {
|
||||
// This test highlights the potentially confusing logic in check_failed_unlocks
|
||||
|
||||
// Case 1: Even number of lockers
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string(), "uid4".to_string()];
|
||||
let tolerance = 2; // locks.len() / 2 = 4 / 2 = 2
|
||||
// locks.len() - tolerance = 4 - 2 = 2, which equals tolerance
|
||||
// So the special case applies: un_locks_failed >= tolerance
|
||||
|
||||
// All 4 failed unlocks
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 4 >= 2 = true
|
||||
|
||||
// 2 failed unlocks
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "".to_string(), "".to_string()];
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 2 >= 2 = true
|
||||
|
||||
// 1 failed unlock
|
||||
let locks = vec!["uid1".to_string(), "".to_string(), "".to_string(), "".to_string()];
|
||||
assert!(!check_failed_unlocks(&locks, tolerance)); // 1 >= 2 = false
|
||||
|
||||
// Case 2: Odd number of lockers
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
|
||||
let tolerance = 1; // locks.len() / 2 = 3 / 2 = 1
|
||||
// locks.len() - tolerance = 3 - 1 = 2, which does NOT equal tolerance (1)
|
||||
// So the normal case applies: un_locks_failed > tolerance
|
||||
|
||||
// 3 failed unlocks
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 3 > 1 = true
|
||||
|
||||
// 2 failed unlocks
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "".to_string()];
|
||||
assert!(check_failed_unlocks(&locks, tolerance)); // 2 > 1 = true
|
||||
|
||||
// 1 failed unlock
|
||||
let locks = vec!["uid1".to_string(), "".to_string(), "".to_string()];
|
||||
assert!(!check_failed_unlocks(&locks, tolerance)); // 1 > 1 = false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quorum_calculation_edge_cases() {
|
||||
// Test the quorum calculation logic that might have issues
|
||||
|
||||
// For 1 locker: tolerance = 0, quorum = 1
|
||||
// Write lock: quorum == tolerance (1 == 0 is false), so quorum stays 1
|
||||
// This seems wrong - with 1 locker, we should need that 1 locker
|
||||
|
||||
// For 2 lockers: tolerance = 1, quorum = 1
|
||||
// Write lock: quorum == tolerance (1 == 1 is true), so quorum becomes 2
|
||||
// This makes sense - we need both lockers for write lock
|
||||
|
||||
// For 3 lockers: tolerance = 1, quorum = 2
|
||||
// Write lock: quorum == tolerance (2 == 1 is false), so quorum stays 2
|
||||
|
||||
// For 4 lockers: tolerance = 2, quorum = 2
|
||||
// Write lock: quorum == tolerance (2 == 2 is true), so quorum becomes 3
|
||||
|
||||
// The logic seems to be: for write locks, if exactly half the lockers
|
||||
// would be tolerance, we need one more to avoid split brain
|
||||
|
||||
// Let's verify this makes sense:
|
||||
struct QuorumTest {
|
||||
locker_count: usize,
|
||||
expected_tolerance: usize,
|
||||
expected_write_quorum: usize,
|
||||
expected_read_quorum: usize,
|
||||
}
|
||||
|
||||
let test_cases = vec![
|
||||
QuorumTest { locker_count: 1, expected_tolerance: 0, expected_write_quorum: 1, expected_read_quorum: 1 },
|
||||
QuorumTest { locker_count: 2, expected_tolerance: 1, expected_write_quorum: 2, expected_read_quorum: 1 },
|
||||
QuorumTest { locker_count: 3, expected_tolerance: 1, expected_write_quorum: 2, expected_read_quorum: 2 },
|
||||
QuorumTest { locker_count: 4, expected_tolerance: 2, expected_write_quorum: 3, expected_read_quorum: 2 },
|
||||
QuorumTest { locker_count: 5, expected_tolerance: 2, expected_write_quorum: 3, expected_read_quorum: 3 },
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
let tolerance = test_case.locker_count / 2;
|
||||
let mut write_quorum = test_case.locker_count - tolerance;
|
||||
let read_quorum = write_quorum;
|
||||
|
||||
// Apply write lock special case
|
||||
if write_quorum == tolerance {
|
||||
write_quorum += 1;
|
||||
}
|
||||
|
||||
assert_eq!(tolerance, test_case.expected_tolerance,
|
||||
"Tolerance mismatch for {} lockers", test_case.locker_count);
|
||||
assert_eq!(write_quorum, test_case.expected_write_quorum,
|
||||
"Write quorum mismatch for {} lockers", test_case.locker_count);
|
||||
assert_eq!(read_quorum, test_case.expected_read_quorum,
|
||||
"Read quorum mismatch for {} lockers", test_case.locker_count);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_potential_integer_overflow() {
|
||||
// Test potential issues with tolerance calculation
|
||||
|
||||
// What happens with 0 lockers? This should probably be an error case
|
||||
let locker_count = 0;
|
||||
let tolerance = locker_count / 2; // 0 / 2 = 0
|
||||
let quorum = locker_count - tolerance; // 0 - 0 = 0
|
||||
|
||||
// This would result in quorum = 0, which doesn't make sense
|
||||
assert_eq!(tolerance, 0);
|
||||
assert_eq!(quorum, 0);
|
||||
|
||||
// The code should probably validate that locker_count > 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_constants() {
|
||||
// Test that constants are reasonable
|
||||
assert!(DRW_MUTEX_REFRESH_INTERVAL.as_secs() > 0);
|
||||
assert!(LOCK_RETRY_MIN_INTERVAL.as_millis() > 0);
|
||||
assert!(DRW_MUTEX_REFRESH_INTERVAL > LOCK_RETRY_MIN_INTERVAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_new_with_unsorted_names() {
|
||||
let names = vec![
|
||||
"zebra".to_string(),
|
||||
"alpha".to_string(),
|
||||
"beta".to_string(),
|
||||
];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
// Names should be sorted
|
||||
assert_eq!(mutex.names, vec!["alpha", "beta", "zebra"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_new_with_duplicate_names() {
|
||||
let names = vec![
|
||||
"resource1".to_string(),
|
||||
"resource2".to_string(),
|
||||
"resource1".to_string(), // Duplicate
|
||||
];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
// Should keep duplicates but sort them
|
||||
assert_eq!(mutex.names, vec!["resource1", "resource1", "resource2"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_lock_and_rlock_methods() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(1);
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let id = "test-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
|
||||
// Test the convenience methods (lock and r_lock)
|
||||
// These should not panic and should attempt to acquire locks
|
||||
mutex.lock(&id, &source).await;
|
||||
// Note: We can't easily test the result since these methods don't return bool
|
||||
|
||||
// Clear any state
|
||||
mutex.un_lock().await;
|
||||
|
||||
// Test r_lock
|
||||
mutex.r_lock(&id, &source).await;
|
||||
mutex.un_r_lock().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_zero_lockers() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = vec![]; // No lockers
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
let id = "test-id".to_string();
|
||||
let source = "test-source".to_string();
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(1),
|
||||
retry_interval: Duration::from_millis(10),
|
||||
};
|
||||
|
||||
// With 0 lockers, quorum calculation:
|
||||
// tolerance = 0 / 2 = 0
|
||||
// quorum = 0 - 0 = 0
|
||||
// This should fail because we can't achieve any quorum
|
||||
let result = mutex.get_lock(&id, &source, &opts).await;
|
||||
assert!(!result, "Should fail with zero lockers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_quorum_locked_edge_cases() {
|
||||
// Test with quorum 0
|
||||
let locks = vec!["".to_string()];
|
||||
assert!(check_quorum_locked(&locks, 0)); // 0 >= 0
|
||||
|
||||
// Test with quorum larger than locks
|
||||
let locks = vec!["uid1".to_string()];
|
||||
assert!(!check_quorum_locked(&locks, 5)); // 1 < 5
|
||||
|
||||
// Test with all locks but high quorum
|
||||
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
|
||||
assert!(!check_quorum_locked(&locks, 4)); // 3 < 4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_failed_unlocks_comprehensive() {
|
||||
// Test all combinations for small lock counts
|
||||
|
||||
// 1 lock scenarios
|
||||
assert!(!check_failed_unlocks(&["".to_string()], 0)); // 1 success, tolerance 0 -> 1 > 0 = true, but tolerance >= len, so false
|
||||
assert!(!check_failed_unlocks(&["".to_string()], 1)); // tolerance >= len
|
||||
assert!(!check_failed_unlocks(&["uid".to_string()], 1)); // tolerance >= len
|
||||
assert!(check_failed_unlocks(&["uid".to_string()], 0)); // 1 failed > 0
|
||||
|
||||
// 2 lock scenarios
|
||||
let two_failed = vec!["uid1".to_string(), "uid2".to_string()];
|
||||
let one_failed = vec!["uid1".to_string(), "".to_string()];
|
||||
let zero_failed = vec!["".to_string(), "".to_string()];
|
||||
|
||||
// tolerance = 0
|
||||
assert!(check_failed_unlocks(&two_failed, 0)); // 2 > 0
|
||||
assert!(check_failed_unlocks(&one_failed, 0)); // 1 > 0
|
||||
assert!(!check_failed_unlocks(&zero_failed, 0)); // 0 > 0 = false
|
||||
|
||||
// tolerance = 1 (special case: 2 - 1 == 1)
|
||||
assert!(check_failed_unlocks(&two_failed, 1)); // 2 >= 1
|
||||
assert!(check_failed_unlocks(&one_failed, 1)); // 1 >= 1
|
||||
assert!(!check_failed_unlocks(&zero_failed, 1)); // 0 >= 1 = false
|
||||
|
||||
// tolerance = 2
|
||||
assert!(!check_failed_unlocks(&two_failed, 2)); // tolerance >= len
|
||||
assert!(!check_failed_unlocks(&one_failed, 2)); // tolerance >= len
|
||||
assert!(!check_failed_unlocks(&zero_failed, 2)); // tolerance >= len
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_options_clone() {
|
||||
let opts = Options {
|
||||
timeout: Duration::from_secs(5),
|
||||
retry_interval: Duration::from_millis(100),
|
||||
};
|
||||
let cloned = opts.clone();
|
||||
assert_eq!(opts.timeout, cloned.timeout);
|
||||
assert_eq!(opts.retry_interval, cloned.retry_interval);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drw_mutex_release_all_edge_cases() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(2);
|
||||
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
||||
|
||||
// Test release_all with empty locks
|
||||
let mut empty_locks = vec!["".to_string(), "".to_string()];
|
||||
let result = mutex.release_all(1, &mut empty_locks, false).await;
|
||||
assert!(result, "Should succeed when releasing empty locks");
|
||||
|
||||
// Test release_all with some locks
|
||||
let mut some_locks = vec!["uid1".to_string(), "uid2".to_string()];
|
||||
let result = mutex.release_all(1, &mut some_locks, false).await;
|
||||
// This should attempt to release the locks and may succeed or fail
|
||||
// depending on the local locker state
|
||||
assert!(result || !result); // Just ensure it doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drw_mutex_struct_fields() {
|
||||
let names = vec!["resource1".to_string()];
|
||||
let lockers = create_mock_lockers(2);
|
||||
let mutex = DRWMutex::new("test-owner".to_string(), names, lockers);
|
||||
|
||||
// Test that all fields are properly initialized
|
||||
assert_eq!(mutex.owner, "test-owner");
|
||||
assert_eq!(mutex.names, vec!["resource1"]);
|
||||
assert_eq!(mutex.write_locks.len(), 2);
|
||||
assert_eq!(mutex.read_locks.len(), 2);
|
||||
assert_eq!(mutex.lockers.len(), 2);
|
||||
assert!(mutex.cancel_refresh_sender.is_none());
|
||||
assert_eq!(mutex.refresh_interval, DRW_MUTEX_REFRESH_INTERVAL);
|
||||
assert_eq!(mutex.lock_retry_min_interval, LOCK_RETRY_MIN_INTERVAL);
|
||||
|
||||
// All locks should be initially empty
|
||||
for lock in &mutex.write_locks {
|
||||
assert!(lock.is_empty());
|
||||
}
|
||||
for lock in &mutex.read_locks {
|
||||
assert!(lock.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user