use crate::error::{GraphError, Result};
use crate::graph::Id;
use crate::transaction::{Transaction, TransactionId};
use parking_lot::{Mutex, RwLock};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LockType {
Read,
Write,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LockableResource {
Node(Id),
Relationship(Id),
Schema,
}
#[derive(Debug, Clone)]
pub struct LockInfo {
pub transaction_id: TransactionId,
pub lock_type: LockType,
pub acquired_at: Instant,
pub resource: LockableResource,
}
pub struct LockManager {
active_locks: Arc<RwLock<HashMap<LockableResource, Vec<LockInfo>>>>,
waiting_locks: Arc<Mutex<Vec<LockRequest>>>,
#[allow(dead_code)]
deadlock_check_interval: Duration,
}
#[derive(Debug, Clone)]
pub struct LockRequest {
pub transaction_id: TransactionId,
pub resource: LockableResource,
pub lock_type: LockType,
pub requested_at: Instant,
}
impl LockManager {
pub fn new() -> Self {
LockManager {
active_locks: Arc::new(RwLock::new(HashMap::new())),
waiting_locks: Arc::new(Mutex::new(Vec::new())),
deadlock_check_interval: Duration::from_millis(100),
}
}
pub fn acquire_lock(
&self,
transaction_id: TransactionId,
resource: LockableResource,
lock_type: LockType,
timeout: Option<Duration>,
) -> Result<bool> {
let start_time = Instant::now();
let timeout = timeout.unwrap_or(Duration::from_secs(30));
loop {
if self.try_acquire_lock(transaction_id, resource, lock_type)? {
return Ok(true);
}
if start_time.elapsed() > timeout {
return Err(GraphError::Concurrency(format!(
"Lock acquisition timeout for transaction {transaction_id} on resource {resource:?}"
)));
}
self.add_to_waiting_queue(transaction_id, resource, lock_type);
if self.detect_deadlock(transaction_id)? {
return Err(GraphError::Concurrency(format!(
"Deadlock detected involving transaction {transaction_id}"
)));
}
std::thread::sleep(Duration::from_millis(10));
}
}
fn try_acquire_lock(
&self,
transaction_id: TransactionId,
resource: LockableResource,
lock_type: LockType,
) -> Result<bool> {
let mut locks = self.active_locks.write();
let existing_locks = locks.get(&resource).cloned().unwrap_or_default();
if self.can_acquire_lock(&existing_locks, lock_type) {
let lock_info = LockInfo {
transaction_id,
lock_type,
acquired_at: Instant::now(),
resource,
};
locks.entry(resource).or_default().push(lock_info);
Ok(true)
} else {
Ok(false)
}
}
fn can_acquire_lock(&self, existing_locks: &[LockInfo], requested_type: LockType) -> bool {
if existing_locks.is_empty() {
return true;
}
match requested_type {
LockType::Read => {
existing_locks
.iter()
.all(|lock| lock.lock_type == LockType::Read)
}
LockType::Write => {
existing_locks.is_empty()
}
}
}
fn add_to_waiting_queue(
&self,
transaction_id: TransactionId,
resource: LockableResource,
lock_type: LockType,
) {
let mut waiting = self.waiting_locks.lock();
if !waiting.iter().any(|req| {
req.transaction_id == transaction_id
&& req.resource == resource
&& req.lock_type == lock_type
}) {
waiting.push(LockRequest {
transaction_id,
resource,
lock_type,
requested_at: Instant::now(),
});
}
}
pub fn release_all_locks(&self, transaction_id: TransactionId) -> Result<()> {
let mut locks = self.active_locks.write();
for (_resource, lock_list) in locks.iter_mut() {
lock_list.retain(|lock| lock.transaction_id != transaction_id);
}
locks.retain(|_resource, lock_list| !lock_list.is_empty());
let mut waiting = self.waiting_locks.lock();
waiting.retain(|req| req.transaction_id != transaction_id);
Ok(())
}
pub fn release_lock(
&self,
transaction_id: TransactionId,
resource: LockableResource,
) -> Result<()> {
let mut locks = self.active_locks.write();
if let Some(lock_list) = locks.get_mut(&resource) {
lock_list.retain(|lock| {
!(lock.transaction_id == transaction_id && lock.resource == resource)
});
if lock_list.is_empty() {
locks.remove(&resource);
}
}
Ok(())
}
fn detect_deadlock(&self, transaction_id: TransactionId) -> Result<bool> {
let locks = self.active_locks.read();
let waiting = self.waiting_locks.lock();
let mut wait_for: HashMap<TransactionId, HashSet<TransactionId>> = HashMap::new();
for request in waiting.iter() {
if let Some(holders) = locks.get(&request.resource) {
let waiting_tx = request.transaction_id;
let holder_txs: HashSet<TransactionId> = holders
.iter()
.map(|lock| lock.transaction_id)
.filter(|&id| id != waiting_tx)
.collect();
if !holder_txs.is_empty() {
wait_for.insert(waiting_tx, holder_txs);
}
}
}
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
self.has_cycle_dfs(transaction_id, &wait_for, &mut visited, &mut rec_stack)
}
#[allow(clippy::only_used_in_recursion)]
fn has_cycle_dfs(
&self,
node: TransactionId,
graph: &HashMap<TransactionId, HashSet<TransactionId>>,
visited: &mut HashSet<TransactionId>,
rec_stack: &mut HashSet<TransactionId>,
) -> Result<bool> {
visited.insert(node);
rec_stack.insert(node);
if let Some(neighbors) = graph.get(&node) {
for &neighbor in neighbors {
if !visited.contains(&neighbor) {
if self.has_cycle_dfs(neighbor, graph, visited, rec_stack)? {
return Ok(true);
}
} else if rec_stack.contains(&neighbor) {
return Ok(true); }
}
}
rec_stack.remove(&node);
Ok(false)
}
pub fn get_lock_statistics(&self) -> LockStatistics {
let locks = self.active_locks.read();
let waiting = self.waiting_locks.lock();
let total_active_locks = locks.values().map(|v| v.len()).sum();
let waiting_requests = waiting.len();
let locked_resources = locks.len();
LockStatistics {
total_active_locks,
waiting_requests,
locked_resources,
}
}
}
impl Default for LockManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LockStatistics {
pub total_active_locks: usize,
pub waiting_requests: usize,
pub locked_resources: usize,
}
pub struct ConcurrentTransaction {
pub transaction: Transaction,
lock_manager: Arc<LockManager>,
held_locks: HashSet<LockableResource>,
}
impl ConcurrentTransaction {
pub fn new(transaction: Transaction, lock_manager: Arc<LockManager>) -> Self {
ConcurrentTransaction {
transaction,
lock_manager,
held_locks: HashSet::new(),
}
}
pub fn read_lock(&mut self, resource: LockableResource) -> Result<()> {
self.lock_manager.acquire_lock(
self.transaction.id(),
resource,
LockType::Read,
Some(Duration::from_secs(30)),
)?;
self.held_locks.insert(resource);
Ok(())
}
pub fn write_lock(&mut self, resource: LockableResource) -> Result<()> {
self.lock_manager.acquire_lock(
self.transaction.id(),
resource,
LockType::Write,
Some(Duration::from_secs(30)),
)?;
self.held_locks.insert(resource);
Ok(())
}
pub fn commit(mut self) -> Result<()> {
self.transaction.commit()?;
self.lock_manager.release_all_locks(self.transaction.id())?;
self.held_locks.clear();
Ok(())
}
pub fn rollback(mut self) -> Result<()> {
self.transaction.rollback()?;
self.lock_manager.release_all_locks(self.transaction.id())?;
self.held_locks.clear();
Ok(())
}
pub fn id(&self) -> TransactionId {
self.transaction.id()
}
pub fn is_active(&self) -> bool {
self.transaction.is_active()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transaction::{IsolationLevel, TransactionManager};
use std::time::Duration;
#[test]
fn test_lock_acquisition() {
let lock_manager = LockManager::new();
let tx_id = 1;
let resource = LockableResource::Node(100);
let result = lock_manager.acquire_lock(
tx_id,
resource,
LockType::Read,
Some(Duration::from_millis(100)),
);
assert!(result.is_ok());
assert!(result.unwrap());
let result = lock_manager.acquire_lock(
2,
resource,
LockType::Read,
Some(Duration::from_millis(100)),
);
assert!(result.is_ok());
assert!(result.unwrap());
let result = lock_manager.acquire_lock(
3,
resource,
LockType::Write,
Some(Duration::from_millis(100)),
);
assert!(result.is_err());
lock_manager.release_all_locks(tx_id).unwrap();
lock_manager.release_all_locks(2).unwrap();
let result = lock_manager.acquire_lock(
3,
resource,
LockType::Write,
Some(Duration::from_millis(100)),
);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_concurrent_transaction() {
let lock_manager = Arc::new(LockManager::new());
let tx_manager = TransactionManager::new(IsolationLevel::ReadCommitted);
let transaction = tx_manager.begin();
let mut concurrent_tx = ConcurrentTransaction::new(transaction, lock_manager);
let resource = LockableResource::Node(200);
assert!(concurrent_tx.read_lock(resource).is_ok());
assert!(concurrent_tx
.write_lock(LockableResource::Node(201))
.is_ok());
assert!(concurrent_tx.commit().is_ok());
}
#[test]
fn test_lock_statistics() {
let lock_manager = LockManager::new();
let stats = lock_manager.get_lock_statistics();
assert_eq!(stats.total_active_locks, 0);
assert_eq!(stats.waiting_requests, 0);
assert_eq!(stats.locked_resources, 0);
lock_manager
.acquire_lock(
1,
LockableResource::Node(100),
LockType::Read,
Some(Duration::from_millis(100)),
)
.unwrap();
lock_manager
.acquire_lock(
2,
LockableResource::Node(101),
LockType::Write,
Some(Duration::from_millis(100)),
)
.unwrap();
let stats = lock_manager.get_lock_statistics();
assert_eq!(stats.total_active_locks, 2);
assert_eq!(stats.locked_resources, 2);
}
}