mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-28 09:08:58 +00:00
1171 lines
41 KiB
Rust
1171 lines
41 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use tokio::{sync::mpsc::Sender, time::sleep};
|
|
use tracing::{info, warn};
|
|
|
|
use crate::{lock_args::LockArgs, LockApi, Locker};
|
|
|
|
const DRW_MUTEX_REFRESH_INTERVAL: Duration = Duration::from_secs(10);
|
|
const LOCK_RETRY_MIN_INTERVAL: Duration = Duration::from_millis(250);
|
|
|
|
#[derive(Debug)]
|
|
pub struct DRWMutex {
|
|
owner: String,
|
|
names: Vec<String>,
|
|
write_locks: Vec<String>,
|
|
read_locks: Vec<String>,
|
|
cancel_refresh_sender: Option<Sender<bool>>,
|
|
// rng: ThreadRng,
|
|
lockers: Vec<LockApi>,
|
|
refresh_interval: Duration,
|
|
lock_retry_min_interval: Duration,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct Granted {
|
|
index: usize,
|
|
lock_uid: String,
|
|
}
|
|
|
|
impl Granted {
|
|
fn is_locked(&self) -> bool {
|
|
is_locked(&self.lock_uid)
|
|
}
|
|
}
|
|
|
|
fn is_locked(uid: &str) -> bool {
|
|
!uid.is_empty()
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Options {
|
|
pub timeout: Duration,
|
|
pub retry_interval: Duration,
|
|
}
|
|
|
|
impl DRWMutex {
|
|
pub fn new(owner: String, names: Vec<String>, lockers: Vec<LockApi>) -> Self {
|
|
let mut names = names;
|
|
names.sort();
|
|
Self {
|
|
owner,
|
|
names,
|
|
write_locks: vec![String::new(); lockers.len()],
|
|
read_locks: vec![String::new(); lockers.len()],
|
|
cancel_refresh_sender: None,
|
|
// rng: rand::thread_rng(),
|
|
lockers,
|
|
refresh_interval: DRW_MUTEX_REFRESH_INTERVAL,
|
|
lock_retry_min_interval: LOCK_RETRY_MIN_INTERVAL,
|
|
}
|
|
}
|
|
|
|
fn is_locked(&self) -> bool {
|
|
self.write_locks.iter().any(|w_lock| is_locked(w_lock))
|
|
}
|
|
|
|
fn is_r_locked(&self) -> bool {
|
|
self.read_locks.iter().any(|r_lock| is_locked(r_lock))
|
|
}
|
|
}
|
|
|
|
impl DRWMutex {
|
|
pub async fn lock(&mut self, id: &String, source: &String) {
|
|
let is_read_lock = false;
|
|
let opts = Options {
|
|
timeout: Duration::from_secs(10),
|
|
retry_interval: Duration::from_millis(50),
|
|
};
|
|
self.lock_blocking(id, source, is_read_lock, &opts).await;
|
|
}
|
|
|
|
pub async fn get_lock(&mut self, id: &String, source: &String, opts: &Options) -> bool {
|
|
let is_read_lock = false;
|
|
self.lock_blocking(id, source, is_read_lock, opts).await
|
|
}
|
|
|
|
pub async fn r_lock(&mut self, id: &String, source: &String) {
|
|
let is_read_lock = true;
|
|
let opts = Options {
|
|
timeout: Duration::from_secs(10),
|
|
retry_interval: Duration::from_millis(50),
|
|
};
|
|
self.lock_blocking(id, source, is_read_lock, &opts).await;
|
|
}
|
|
|
|
pub async fn get_r_lock(&mut self, id: &String, source: &String, opts: &Options) -> bool {
|
|
let is_read_lock = true;
|
|
self.lock_blocking(id, source, is_read_lock, opts).await
|
|
}
|
|
|
|
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 {
|
|
// In situations for write locks, as a special case
|
|
// to avoid split brains we make sure to acquire
|
|
// quorum + 1 when tolerance is exactly half of the
|
|
// total locker clients.
|
|
if quorum == tolerance {
|
|
quorum += 1;
|
|
}
|
|
}
|
|
info!("lockBlocking {}/{} for {:?}: lockType readLock({}), additional opts: {:?}, quorum: {}, tolerance: {}, lockClients: {}\n", id, source, self.names, is_read_lock, opts, quorum, tolerance, locker_len);
|
|
|
|
// 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()];
|
|
|
|
loop {
|
|
if self.inner_lock(&mut locks, id, source, is_read_lock, tolerance, quorum).await {
|
|
if is_read_lock {
|
|
self.read_locks = locks;
|
|
} else {
|
|
self.write_locks = locks;
|
|
}
|
|
|
|
info!("lock_blocking {}/{} for {:?}: granted", id, source, self.names);
|
|
|
|
return true;
|
|
}
|
|
|
|
attempt += 1;
|
|
if attempt >= 10 {
|
|
break;
|
|
}
|
|
sleep(opts.retry_interval).await;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
async fn inner_lock(
|
|
&mut self,
|
|
locks: &mut [String],
|
|
id: &String,
|
|
source: &String,
|
|
is_read_lock: bool,
|
|
tolerance: usize,
|
|
quorum: usize,
|
|
) -> bool {
|
|
locks.iter_mut().for_each(|lock| *lock = "".to_string());
|
|
|
|
let mut granteds = Vec::with_capacity(self.lockers.len());
|
|
let args = LockArgs {
|
|
uid: id.to_string(),
|
|
resources: self.names.clone(),
|
|
owner: self.owner.clone(),
|
|
source: source.to_string(),
|
|
quorum,
|
|
};
|
|
|
|
for (index, locker) in self.lockers.iter_mut().enumerate() {
|
|
let mut granted = Granted {
|
|
index,
|
|
..Default::default()
|
|
};
|
|
|
|
if is_read_lock {
|
|
match locker.rlock(&args).await {
|
|
Ok(locked) => {
|
|
if locked {
|
|
granted.lock_uid = id.to_string();
|
|
}
|
|
}
|
|
Err(err) => {
|
|
warn!("Unable to call RLock failed with {} for {} at {:?}", err, args, locker);
|
|
}
|
|
}
|
|
} else {
|
|
match locker.lock(&args).await {
|
|
Ok(locked) => {
|
|
if locked {
|
|
granted.lock_uid = id.to_string();
|
|
}
|
|
}
|
|
Err(err) => {
|
|
warn!("Unable to call Lock failed with {} for {} at {:?}", err, args, locker);
|
|
}
|
|
}
|
|
}
|
|
|
|
granteds.push(granted);
|
|
}
|
|
|
|
granteds.iter().for_each(|granted| {
|
|
locks[granted.index] = granted.lock_uid.clone();
|
|
});
|
|
|
|
let quorum_locked = check_quorum_locked(locks, quorum);
|
|
if !quorum_locked {
|
|
info!("Unable to acquire lock in quorum, {}", args);
|
|
if !self.release_all(tolerance, locks, is_read_lock).await {
|
|
info!("Unable to release acquired locks, these locks will expire automatically {}", args);
|
|
}
|
|
}
|
|
|
|
quorum_locked
|
|
}
|
|
|
|
pub async fn un_lock(&mut self) {
|
|
if self.write_locks.is_empty() || !self.is_locked() {
|
|
warn!("Trying to un_lock() while no lock() is active, write_locks: {:?}", self.write_locks)
|
|
}
|
|
|
|
let tolerance = self.lockers.len() / 2;
|
|
let is_read_lock = false;
|
|
let mut locks = std::mem::take(&mut self.write_locks);
|
|
let start = Instant::now();
|
|
loop {
|
|
if self.release_all(tolerance, &mut locks, is_read_lock).await {
|
|
return;
|
|
}
|
|
|
|
sleep(self.lock_retry_min_interval).await;
|
|
if Instant::now().duration_since(start) > Duration::from_secs(30) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn un_r_lock(&mut self) {
|
|
if self.read_locks.is_empty() || !self.is_r_locked() {
|
|
warn!("Trying to un_r_lock() while no r_lock() is active, read_locks: {:?}", self.read_locks)
|
|
}
|
|
|
|
let tolerance = self.lockers.len() / 2;
|
|
let is_read_lock = true;
|
|
let mut locks = std::mem::take(&mut self.read_locks);
|
|
let start = Instant::now();
|
|
loop {
|
|
if self.release_all(tolerance, &mut locks, is_read_lock).await {
|
|
return;
|
|
}
|
|
|
|
sleep(self.lock_retry_min_interval).await;
|
|
if Instant::now().duration_since(start) > Duration::from_secs(30) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn release_all(&mut self, tolerance: usize, locks: &mut [String], is_read_lock: bool) -> bool {
|
|
for (index, locker) in self.lockers.iter_mut().enumerate() {
|
|
if send_release(locker, &locks[index], &self.owner, &self.names, is_read_lock).await {
|
|
locks[index] = "".to_string();
|
|
}
|
|
}
|
|
|
|
!check_failed_unlocks(locks, tolerance)
|
|
}
|
|
}
|
|
|
|
// async fn start_continuous_lock_refresh(lockers: &Vec<&mut LockApi>, id: &String, source: &String, quorum: usize, refresh_interval: Duration, mut cancel_refresh_receiver: Receiver<bool>) {
|
|
// let uid = id.to_string();
|
|
// tokio::spawn(async move {
|
|
// let mut ticker = interval(refresh_interval);
|
|
// let args = LockArgs {
|
|
// uid,
|
|
// ..Default::default()
|
|
// };
|
|
|
|
// loop {
|
|
// select! {
|
|
// _ = ticker.tick() => {
|
|
// for (index, locker) in lockers.iter().enumerate() {
|
|
|
|
// }
|
|
// },
|
|
// _ = cancel_refresh_receiver.recv() => {
|
|
// return;
|
|
// }
|
|
// }
|
|
// }
|
|
// });
|
|
// }
|
|
|
|
fn check_failed_unlocks(locks: &[String], tolerance: usize) -> bool {
|
|
let mut un_locks_failed = 0;
|
|
locks.iter().for_each(|lock| {
|
|
if is_locked(lock) {
|
|
un_locks_failed += 1;
|
|
}
|
|
});
|
|
|
|
// 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
|
|
}
|
|
|
|
async fn send_release(locker: &mut LockApi, uid: &String, owner: &str, names: &[String], is_read_lock: bool) -> bool {
|
|
if uid.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
let args = LockArgs {
|
|
uid: uid.to_string(),
|
|
owner: owner.to_owned(),
|
|
resources: names.to_owned(),
|
|
..Default::default()
|
|
};
|
|
|
|
if is_read_lock {
|
|
match locker.runlock(&args).await {
|
|
Ok(locked) => {
|
|
if !locked {
|
|
warn!("Unable to release runlock, args: {}", args);
|
|
return false;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
warn!("Unable to call RLock failed with {} for {} at {:?}", err, args, locker);
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
match locker.unlock(&args).await {
|
|
Ok(locked) => {
|
|
if !locked {
|
|
warn!("Unable to release unlock, args: {}", args);
|
|
return false;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
warn!("Unable to call Lock failed with {} for {} at {:?}", err, args, locker);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn check_quorum_locked(locks: &[String], quorum: usize) -> bool {
|
|
let mut count = 0;
|
|
locks.iter().for_each(|lock| {
|
|
if is_locked(lock) {
|
|
count += 1;
|
|
}
|
|
});
|
|
|
|
count >= quorum
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use async_trait::async_trait;
|
|
use common::error::{Error, Result};
|
|
use crate::local_locker::LocalLocker;
|
|
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 || !result); // This is always true, so removed
|
|
|
|
// 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 || !result); // This is always true, so removed
|
|
|
|
// 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 || !result); // This is always true, so removed
|
|
}
|
|
|
|
#[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 || !result); // This is always true, so removed
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_drw_mutex_concurrent_read_locks() {
|
|
// Clear global state before test to avoid interference from other tests
|
|
{
|
|
let mut global_server = crate::GLOBAL_LOCAL_SERVER.write().await;
|
|
*global_server = LocalLocker::new();
|
|
}
|
|
|
|
// Use a single mutex with one resource for simplicity
|
|
let names = vec!["test-resource".to_string()];
|
|
let lockers = create_mock_lockers(1);
|
|
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
|
|
|
|
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(5),
|
|
retry_interval: Duration::from_millis(50),
|
|
};
|
|
|
|
// First acquire a read lock
|
|
let result1 = mutex.get_r_lock(&id1, &source, &opts).await;
|
|
assert!(result1, "First read lock should succeed");
|
|
|
|
// Release the first read lock
|
|
mutex.un_r_lock().await;
|
|
|
|
// Then acquire another read lock with different ID - this should succeed
|
|
let result2 = mutex.get_r_lock(&id2, &source, &opts).await;
|
|
assert!(result2, "Second read lock should succeed after first is released");
|
|
|
|
// Clean up
|
|
mutex.un_r_lock().await;
|
|
}
|
|
|
|
#[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 - just ensure it doesn't panic
|
|
let _ = result; // Suppress unused variable warning
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|
|
}
|