use super::lock_manager::{LockManager, LockMode};
use super::wal::TxnId;
use crate::error::{Result, TdbError};
use crate::storage::page::PageId;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictStrategy {
WaitTimeout,
NoWait,
WoundWait,
WaitDie,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeadlockDetection {
Timeout,
WaitForGraph,
Periodic,
}
pub struct ConflictManager {
lock_manager: Arc<LockManager>,
strategy: ConflictStrategy,
detection: DeadlockDetection,
wait_for_graph: RwLock<HashMap<TxnId, HashSet<TxnId>>>,
txn_timestamps: RwLock<HashMap<TxnId, u64>>,
timestamp_counter: RwLock<u64>,
}
impl ConflictManager {
pub fn new(
lock_manager: Arc<LockManager>,
strategy: ConflictStrategy,
detection: DeadlockDetection,
) -> Self {
ConflictManager {
lock_manager,
strategy,
detection,
wait_for_graph: RwLock::new(HashMap::new()),
txn_timestamps: RwLock::new(HashMap::new()),
timestamp_counter: RwLock::new(0),
}
}
pub fn begin_transaction(&self, txn_id: TxnId) -> Result<()> {
let mut counter = self
.timestamp_counter
.write()
.expect("rwlock should not be poisoned");
let timestamp = *counter;
*counter += 1;
let mut timestamps = self
.txn_timestamps
.write()
.expect("rwlock should not be poisoned");
timestamps.insert(txn_id, timestamp);
Ok(())
}
pub fn end_transaction(&self, txn_id: TxnId) -> Result<()> {
let mut wait_for = self
.wait_for_graph
.write()
.expect("rwlock should not be poisoned");
wait_for.remove(&txn_id);
let mut timestamps = self
.txn_timestamps
.write()
.expect("rwlock should not be poisoned");
timestamps.remove(&txn_id);
Ok(())
}
pub fn lock_with_strategy(&self, txn_id: TxnId, page_id: PageId, mode: LockMode) -> Result<()> {
match self.strategy {
ConflictStrategy::WaitTimeout => {
self.lock_manager.lock(txn_id, page_id, mode)
}
ConflictStrategy::NoWait => {
if self.lock_manager.try_lock(txn_id, page_id, mode)? {
Ok(())
} else {
Err(TdbError::Transaction(format!(
"Lock unavailable for page {} (no-wait mode)",
page_id
)))
}
}
ConflictStrategy::WoundWait => {
self.wound_wait_lock(txn_id, page_id, mode)
}
ConflictStrategy::WaitDie => {
self.wait_die_lock(txn_id, page_id, mode)
}
}
}
fn wound_wait_lock(&self, txn_id: TxnId, page_id: PageId, mode: LockMode) -> Result<()> {
if self.lock_manager.try_lock(txn_id, page_id, mode)? {
return Ok(());
}
let my_timestamp = self
.txn_timestamps
.read()
.expect("rwlock should not be poisoned")
.get(&txn_id)
.copied()
.unwrap_or(u64::MAX);
let holders = self.lock_manager.get_lock_holders(page_id);
let timestamps = self
.txn_timestamps
.read()
.expect("rwlock should not be poisoned");
let should_wound = holders.iter().any(|(holder_txn, _)| {
let holder_timestamp = timestamps.get(holder_txn).copied().unwrap_or(0);
my_timestamp < holder_timestamp });
if should_wound {
log::info!(
"Transaction {} (ts={}) wounds younger holders for page {}",
txn_id.as_u64(),
my_timestamp,
page_id
);
self.lock_manager.lock(txn_id, page_id, mode)
} else {
self.lock_manager.lock(txn_id, page_id, mode)
}
}
fn wait_die_lock(&self, txn_id: TxnId, page_id: PageId, mode: LockMode) -> Result<()> {
if self.lock_manager.try_lock(txn_id, page_id, mode)? {
return Ok(());
}
let my_timestamp = self
.txn_timestamps
.read()
.expect("rwlock should not be poisoned")
.get(&txn_id)
.copied()
.unwrap_or(u64::MAX);
let holders = self.lock_manager.get_lock_holders(page_id);
if holders.is_empty() {
return self.lock_manager.lock(txn_id, page_id, mode);
}
let timestamps = self
.txn_timestamps
.read()
.expect("rwlock should not be poisoned");
let i_am_older = holders.iter().all(|(holder_txn, _)| {
let holder_timestamp = timestamps.get(holder_txn).copied().unwrap_or(u64::MAX);
my_timestamp < holder_timestamp });
if i_am_older {
log::debug!(
"Transaction {} (ts={}) waiting for locks on page {}",
txn_id.as_u64(),
my_timestamp,
page_id
);
self.lock_manager.lock(txn_id, page_id, mode)
} else {
log::info!(
"Transaction {} (ts={}) dies (aborts) due to conflict on page {}",
txn_id.as_u64(),
my_timestamp,
page_id
);
Err(TdbError::Transaction(format!(
"Transaction {} aborted by wait-die strategy",
txn_id.as_u64()
)))
}
}
pub fn detect_deadlock(&self) -> Result<Vec<DeadlockCycle>> {
match self.detection {
DeadlockDetection::Timeout => {
Ok(Vec::new())
}
DeadlockDetection::WaitForGraph => {
self.detect_deadlock_cycles()
}
DeadlockDetection::Periodic => {
self.detect_deadlock_cycles()
}
}
}
fn detect_deadlock_cycles(&self) -> Result<Vec<DeadlockCycle>> {
let wait_for = self
.wait_for_graph
.read()
.expect("rwlock should not be poisoned");
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut path = Vec::new();
for &txn in wait_for.keys() {
if !visited.contains(&txn) {
if let Some(cycle) = self.find_cycle(&wait_for, txn, &mut visited, &mut path) {
cycles.push(cycle);
}
}
}
Ok(cycles)
}
#[allow(clippy::only_used_in_recursion)]
fn find_cycle(
&self,
wait_for: &HashMap<TxnId, HashSet<TxnId>>,
start_txn: TxnId,
visited: &mut HashSet<TxnId>,
path: &mut Vec<TxnId>,
) -> Option<DeadlockCycle> {
visited.insert(start_txn);
path.push(start_txn);
if let Some(waiting_for) = wait_for.get(&start_txn) {
for &next_txn in waiting_for {
if let Some(pos) = path.iter().position(|&t| t == next_txn) {
let cycle_txns = path[pos..].to_vec();
return Some(DeadlockCycle {
transactions: cycle_txns,
});
}
if !visited.contains(&next_txn) {
if let Some(cycle) = self.find_cycle(wait_for, next_txn, visited, path) {
return Some(cycle);
}
}
}
}
path.pop();
None
}
pub fn add_wait_for(&self, waiter: TxnId, holder: TxnId) -> Result<()> {
let mut wait_for = self
.wait_for_graph
.write()
.expect("rwlock should not be poisoned");
wait_for.entry(waiter).or_default().insert(holder);
Ok(())
}
pub fn remove_wait_for(&self, waiter: TxnId, holder: TxnId) -> Result<()> {
let mut wait_for = self
.wait_for_graph
.write()
.expect("rwlock should not be poisoned");
if let Some(holders) = wait_for.get_mut(&waiter) {
holders.remove(&holder);
if holders.is_empty() {
wait_for.remove(&waiter);
}
}
Ok(())
}
pub fn conflict_stats(&self) -> ConflictStats {
let wait_for = self
.wait_for_graph
.read()
.expect("rwlock should not be poisoned");
ConflictStats {
waiting_transactions: wait_for.len(),
total_wait_edges: wait_for.values().map(|s| s.len()).sum(),
active_transactions: self
.txn_timestamps
.read()
.expect("rwlock should not be poisoned")
.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct DeadlockCycle {
pub transactions: Vec<TxnId>,
}
impl DeadlockCycle {
pub fn youngest_transaction(&self, timestamps: &HashMap<TxnId, u64>) -> Option<TxnId> {
self.transactions
.iter()
.max_by_key(|&&txn| timestamps.get(&txn).copied().unwrap_or(0))
.copied()
}
pub fn oldest_transaction(&self, timestamps: &HashMap<TxnId, u64>) -> Option<TxnId> {
self.transactions
.iter()
.min_by_key(|&&txn| timestamps.get(&txn).copied().unwrap_or(u64::MAX))
.copied()
}
}
#[derive(Debug, Clone)]
pub struct ConflictStats {
pub waiting_transactions: usize,
pub total_wait_edges: usize,
pub active_transactions: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conflict_manager_creation() {
let lock_manager = Arc::new(LockManager::new());
let _conflict = ConflictManager::new(
lock_manager,
ConflictStrategy::WaitTimeout,
DeadlockDetection::WaitForGraph,
);
}
#[test]
fn test_transaction_timestamps() {
let lock_manager = Arc::new(LockManager::new());
let conflict = ConflictManager::new(
lock_manager,
ConflictStrategy::WaitTimeout,
DeadlockDetection::WaitForGraph,
);
let txn1 = TxnId::new(1);
let txn2 = TxnId::new(2);
conflict.begin_transaction(txn1).unwrap();
conflict.begin_transaction(txn2).unwrap();
let timestamps = conflict
.txn_timestamps
.read()
.expect("rwlock should not be poisoned");
let ts1 = timestamps.get(&txn1).copied().unwrap();
let ts2 = timestamps.get(&txn2).copied().unwrap();
assert!(ts1 < ts2);
drop(timestamps);
conflict.end_transaction(txn1).unwrap();
conflict.end_transaction(txn2).unwrap();
}
#[test]
fn test_wait_for_graph() {
let lock_manager = Arc::new(LockManager::new());
let conflict = ConflictManager::new(
lock_manager,
ConflictStrategy::WaitTimeout,
DeadlockDetection::WaitForGraph,
);
let txn1 = TxnId::new(1);
let txn2 = TxnId::new(2);
conflict.add_wait_for(txn1, txn2).unwrap();
let stats = conflict.conflict_stats();
assert_eq!(stats.waiting_transactions, 1);
assert_eq!(stats.total_wait_edges, 1);
conflict.remove_wait_for(txn1, txn2).unwrap();
let stats = conflict.conflict_stats();
assert_eq!(stats.waiting_transactions, 0);
}
#[test]
fn test_deadlock_cycle_detection() {
let lock_manager = Arc::new(LockManager::new());
let conflict = ConflictManager::new(
lock_manager,
ConflictStrategy::WaitTimeout,
DeadlockDetection::WaitForGraph,
);
let txn1 = TxnId::new(1);
let txn2 = TxnId::new(2);
let txn3 = TxnId::new(3);
conflict.add_wait_for(txn1, txn2).unwrap();
conflict.add_wait_for(txn2, txn3).unwrap();
conflict.add_wait_for(txn3, txn1).unwrap();
let cycles = conflict.detect_deadlock().unwrap();
assert!(!cycles.is_empty(), "Should detect at least one cycle");
}
#[test]
fn test_conflict_stats() {
let lock_manager = Arc::new(LockManager::new());
let conflict = ConflictManager::new(
lock_manager,
ConflictStrategy::WaitTimeout,
DeadlockDetection::WaitForGraph,
);
let txn1 = TxnId::new(1);
let txn2 = TxnId::new(2);
conflict.begin_transaction(txn1).unwrap();
conflict.begin_transaction(txn2).unwrap();
let stats = conflict.conflict_stats();
assert_eq!(stats.active_transactions, 2);
conflict.end_transaction(txn1).unwrap();
conflict.end_transaction(txn2).unwrap();
}
}