use crate::error::{GraphError, Result};
use crate::graph::{Id, Node, Relationship};
use crate::transaction::{IsolationLevel, TransactionId, TransactionState};
use parking_lot::RwLock;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
pub type Timestamp = u64;
#[derive(Debug, Clone)]
pub struct VersionedValue<T> {
pub value: T,
pub created_by: TransactionId,
pub created_at: Timestamp,
pub deleted_by: Option<TransactionId>,
pub deleted_at: Option<Timestamp>,
}
impl<T> VersionedValue<T> {
pub fn new(value: T, tx_id: TransactionId, timestamp: Timestamp) -> Self {
VersionedValue {
value,
created_by: tx_id,
created_at: timestamp,
deleted_by: None,
deleted_at: None,
}
}
pub fn is_visible_at(
&self,
timestamp: Timestamp,
committed_txs: &HashSet<TransactionId>,
) -> bool {
let created_visible =
self.created_at <= timestamp && committed_txs.contains(&self.created_by);
let not_deleted = match (self.deleted_at, self.deleted_by) {
(Some(del_ts), Some(del_tx)) => del_ts > timestamp || !committed_txs.contains(&del_tx),
_ => true,
};
created_visible && not_deleted
}
pub fn mark_deleted(&mut self, tx_id: TransactionId, timestamp: Timestamp) {
self.deleted_by = Some(tx_id);
self.deleted_at = Some(timestamp);
}
}
#[derive(Debug)]
pub struct VersionChain<T> {
versions: BTreeMap<Timestamp, VersionedValue<T>>,
}
impl<T: Clone> VersionChain<T> {
pub fn new() -> Self {
VersionChain {
versions: BTreeMap::new(),
}
}
pub fn add_version(&mut self, version: VersionedValue<T>) {
self.versions.insert(version.created_at, version);
}
pub fn find_visible_version(
&self,
timestamp: Timestamp,
committed_txs: &HashSet<TransactionId>,
) -> Option<&VersionedValue<T>> {
self.versions
.iter()
.rev()
.map(|(_, version)| version)
.find(|&version| version.is_visible_at(timestamp, committed_txs))
}
pub fn latest_version(&self) -> Option<&VersionedValue<T>> {
self.versions.values().next_back()
}
pub fn find_version_by_tx(&self, tx_id: TransactionId) -> Option<&VersionedValue<T>> {
self.versions
.values()
.rev()
.find(|v| v.created_by == tx_id && v.deleted_at.is_none())
}
pub fn mark_latest_deleted(&mut self, tx_id: TransactionId, timestamp: Timestamp) -> bool {
if let Some((_, version)) = self.versions.iter_mut().next_back() {
version.mark_deleted(tx_id, timestamp);
true
} else {
false
}
}
pub fn gc(&mut self, oldest_active_timestamp: Timestamp) {
self.versions.retain(|_, version| {
version.created_at >= oldest_active_timestamp
|| version
.deleted_at
.map(|t| t >= oldest_active_timestamp)
.unwrap_or(true)
});
}
}
impl<T: Clone> Default for VersionChain<T> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct MvccStore {
nodes: RwLock<HashMap<Id, VersionChain<Node>>>,
relationships: RwLock<HashMap<Id, VersionChain<Relationship>>>,
committed_txs: RwLock<HashSet<TransactionId>>,
oldest_active: AtomicU64,
}
impl MvccStore {
pub fn new() -> Self {
let mut committed = HashSet::new();
committed.insert(0);
MvccStore {
nodes: RwLock::new(HashMap::new()),
relationships: RwLock::new(HashMap::new()),
committed_txs: RwLock::new(committed),
oldest_active: AtomicU64::new(0),
}
}
pub fn read_node(
&self,
id: Id,
timestamp: Timestamp,
current_tx_id: Option<TransactionId>,
) -> Option<Node> {
let nodes = self.nodes.read();
let committed = self.committed_txs.read();
nodes.get(&id).and_then(|chain| {
if let Some(tx_id) = current_tx_id {
if let Some(version) = chain.find_version_by_tx(tx_id) {
return Some(version.value.clone());
}
}
chain
.find_visible_version(timestamp, &committed)
.map(|v| v.value.clone())
})
}
pub fn read_relationship(
&self,
id: Id,
timestamp: Timestamp,
current_tx_id: Option<TransactionId>,
) -> Option<Relationship> {
let relationships = self.relationships.read();
let committed = self.committed_txs.read();
relationships.get(&id).and_then(|chain| {
if let Some(tx_id) = current_tx_id {
if let Some(version) = chain.find_version_by_tx(tx_id) {
return Some(version.value.clone());
}
}
chain
.find_visible_version(timestamp, &committed)
.map(|v| v.value.clone())
})
}
pub fn write_node(&self, node: Node, tx_id: TransactionId, timestamp: Timestamp) {
let mut nodes = self.nodes.write();
let id = node.id;
let version = VersionedValue::new(node, tx_id, timestamp);
nodes.entry(id).or_default().add_version(version);
}
pub fn write_relationship(
&self,
rel: Relationship,
tx_id: TransactionId,
timestamp: Timestamp,
) {
let mut relationships = self.relationships.write();
let id = rel.id;
let version = VersionedValue::new(rel, tx_id, timestamp);
relationships.entry(id).or_default().add_version(version);
}
pub fn delete_node(&self, id: Id, tx_id: TransactionId, timestamp: Timestamp) -> bool {
let mut nodes = self.nodes.write();
if let Some(chain) = nodes.get_mut(&id) {
chain.mark_latest_deleted(tx_id, timestamp)
} else {
false
}
}
pub fn delete_relationship(&self, id: Id, tx_id: TransactionId, timestamp: Timestamp) -> bool {
let mut relationships = self.relationships.write();
if let Some(chain) = relationships.get_mut(&id) {
chain.mark_latest_deleted(tx_id, timestamp)
} else {
false
}
}
pub fn commit_transaction(&self, tx_id: TransactionId) {
self.committed_txs.write().insert(tx_id);
}
pub fn is_committed(&self, tx_id: TransactionId) -> bool {
self.committed_txs.read().contains(&tx_id)
}
pub fn update_oldest_active(&self, timestamp: Timestamp) {
self.oldest_active.store(timestamp, Ordering::SeqCst);
}
pub fn gc(&self) {
let oldest = self.oldest_active.load(Ordering::SeqCst);
{
let mut nodes = self.nodes.write();
for chain in nodes.values_mut() {
chain.gc(oldest);
}
}
{
let mut relationships = self.relationships.write();
for chain in relationships.values_mut() {
chain.gc(oldest);
}
}
}
}
impl Default for MvccStore {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WriteSetEntry {
Node(Id),
Relationship(Id),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ReadSetEntry {
Node(Id),
Relationship(Id),
}
#[derive(Debug)]
pub struct MvccTransaction {
pub id: TransactionId,
pub isolation_level: IsolationLevel,
pub state: TransactionState,
pub start_timestamp: Timestamp,
pub commit_timestamp: Option<Timestamp>,
write_set: HashSet<WriteSetEntry>,
read_set: HashSet<ReadSetEntry>,
}
impl MvccTransaction {
pub fn new(
id: TransactionId,
isolation_level: IsolationLevel,
start_timestamp: Timestamp,
) -> Self {
MvccTransaction {
id,
isolation_level,
state: TransactionState::Active,
start_timestamp,
commit_timestamp: None,
write_set: HashSet::new(),
read_set: HashSet::new(),
}
}
pub fn record_read_node(&mut self, id: Id) {
if self.isolation_level == IsolationLevel::Serializable {
self.read_set.insert(ReadSetEntry::Node(id));
}
}
pub fn record_read_relationship(&mut self, id: Id) {
if self.isolation_level == IsolationLevel::Serializable {
self.read_set.insert(ReadSetEntry::Relationship(id));
}
}
pub fn record_write_node(&mut self, id: Id) {
self.write_set.insert(WriteSetEntry::Node(id));
}
pub fn record_write_relationship(&mut self, id: Id) {
self.write_set.insert(WriteSetEntry::Relationship(id));
}
pub fn write_set(&self) -> &HashSet<WriteSetEntry> {
&self.write_set
}
pub fn read_set(&self) -> &HashSet<ReadSetEntry> {
&self.read_set
}
pub fn has_writes(&self) -> bool {
!self.write_set.is_empty()
}
}
pub struct MvccManager {
store: Arc<MvccStore>,
next_tx_id: AtomicU64,
timestamp_counter: AtomicU64,
active_txs: RwLock<HashMap<TransactionId, MvccTransaction>>,
recent_commits: RwLock<Vec<(TransactionId, Timestamp, HashSet<WriteSetEntry>)>>,
conflict_window: u64,
}
impl MvccManager {
pub fn new() -> Self {
MvccManager {
store: Arc::new(MvccStore::new()),
next_tx_id: AtomicU64::new(1),
timestamp_counter: AtomicU64::new(1),
active_txs: RwLock::new(HashMap::new()),
recent_commits: RwLock::new(Vec::new()),
conflict_window: 10_000, }
}
pub fn begin(&self, isolation_level: IsolationLevel) -> TransactionId {
let tx_id = self.next_tx_id.fetch_add(1, Ordering::SeqCst);
let timestamp = self.timestamp_counter.fetch_add(1, Ordering::SeqCst);
let tx = MvccTransaction::new(tx_id, isolation_level, timestamp);
self.active_txs.write().insert(tx_id, tx);
tx_id
}
pub fn get_read_timestamp(&self, tx_id: TransactionId) -> Option<Timestamp> {
self.active_txs
.read()
.get(&tx_id)
.map(|tx| tx.start_timestamp)
}
pub fn read_node(&self, tx_id: TransactionId, id: Id) -> Result<Option<Node>> {
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
tx.record_read_node(id);
Ok(self.store.read_node(id, tx.start_timestamp, Some(tx_id)))
}
pub fn read_relationship(&self, tx_id: TransactionId, id: Id) -> Result<Option<Relationship>> {
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
tx.record_read_relationship(id);
Ok(self
.store
.read_relationship(id, tx.start_timestamp, Some(tx_id)))
}
pub fn write_node(&self, tx_id: TransactionId, node: Node) -> Result<()> {
let timestamp = self.timestamp_counter.fetch_add(1, Ordering::SeqCst);
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
let id = node.id;
tx.record_write_node(id);
self.store.write_node(node, tx_id, timestamp);
Ok(())
}
pub fn write_relationship(&self, tx_id: TransactionId, rel: Relationship) -> Result<()> {
let timestamp = self.timestamp_counter.fetch_add(1, Ordering::SeqCst);
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
let id = rel.id;
tx.record_write_relationship(id);
self.store.write_relationship(rel, tx_id, timestamp);
Ok(())
}
pub fn commit(&self, tx_id: TransactionId) -> Result<()> {
let commit_timestamp = self.timestamp_counter.fetch_add(1, Ordering::SeqCst);
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
if tx.has_writes() {
self.detect_conflicts(tx)?;
}
tx.state = TransactionState::Committed;
tx.commit_timestamp = Some(commit_timestamp);
self.store.commit_transaction(tx_id);
if tx.has_writes() {
let write_set = tx.write_set().clone();
self.recent_commits
.write()
.push((tx_id, commit_timestamp, write_set));
}
let _tx = active.remove(&tx_id);
drop(active);
self.update_oldest_active();
self.cleanup_recent_commits(commit_timestamp);
Ok(())
}
pub fn rollback(&self, tx_id: TransactionId) -> Result<()> {
let mut active = self.active_txs.write();
let tx = active
.get_mut(&tx_id)
.ok_or_else(|| GraphError::Transaction(format!("Transaction {tx_id} not found")))?;
if tx.state != TransactionState::Active {
return Err(GraphError::Transaction(
"Transaction not active".to_string(),
));
}
tx.state = TransactionState::RolledBack;
active.remove(&tx_id);
drop(active);
self.update_oldest_active();
Ok(())
}
fn detect_conflicts(&self, tx: &MvccTransaction) -> Result<()> {
let recent = self.recent_commits.read();
for (other_tx_id, commit_ts, write_set) in recent.iter() {
if *other_tx_id == tx.id {
continue;
}
if *commit_ts > tx.start_timestamp {
for entry in tx.write_set() {
if write_set.contains(entry) {
return Err(GraphError::Concurrency(format!(
"Write-write conflict on {entry:?} with transaction {other_tx_id}"
)));
}
}
if tx.isolation_level == IsolationLevel::Serializable {
for entry in tx.read_set() {
let conflicting_write = match entry {
ReadSetEntry::Node(id) => write_set.contains(&WriteSetEntry::Node(*id)),
ReadSetEntry::Relationship(id) => {
write_set.contains(&WriteSetEntry::Relationship(*id))
}
};
if conflicting_write {
return Err(GraphError::Concurrency(format!(
"Read-write conflict on {entry:?} with transaction {other_tx_id}"
)));
}
}
}
}
}
Ok(())
}
fn update_oldest_active(&self) {
let active = self.active_txs.read();
let oldest = active
.values()
.map(|tx| tx.start_timestamp)
.min()
.unwrap_or(self.timestamp_counter.load(Ordering::SeqCst));
self.store.update_oldest_active(oldest);
}
fn cleanup_recent_commits(&self, current_ts: Timestamp) {
let mut recent = self.recent_commits.write();
let cutoff = current_ts.saturating_sub(self.conflict_window);
recent.retain(|(_, ts, _)| *ts >= cutoff);
}
pub fn gc(&self) {
self.store.gc();
}
pub fn get_stats(&self) -> MvccStats {
let active = self.active_txs.read();
let recent = self.recent_commits.read();
MvccStats {
active_transactions: active.len(),
recent_commits: recent.len(),
current_timestamp: self.timestamp_counter.load(Ordering::SeqCst),
}
}
}
impl Default for MvccManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MvccStats {
pub active_transactions: usize,
pub recent_commits: usize,
pub current_timestamp: Timestamp,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn make_node(id: Id) -> Node {
Node {
id,
properties: HashMap::new(),
}
}
#[test]
fn test_basic_transaction() {
let mvcc = MvccManager::new();
let tx_id = mvcc.begin(IsolationLevel::ReadCommitted);
mvcc.write_node(tx_id, make_node(1)).unwrap();
let node = mvcc.read_node(tx_id, 1).unwrap();
assert!(node.is_some());
mvcc.commit(tx_id).unwrap();
}
#[test]
fn test_isolation() {
let mvcc = MvccManager::new();
let tx1 = mvcc.begin(IsolationLevel::RepeatableRead);
mvcc.write_node(tx1, make_node(1)).unwrap();
mvcc.commit(tx1).unwrap();
let tx2 = mvcc.begin(IsolationLevel::RepeatableRead);
let tx3 = mvcc.begin(IsolationLevel::RepeatableRead);
let mut node = make_node(1);
node.properties
.insert("version".to_string(), serde_json::json!(2));
mvcc.write_node(tx3, node).unwrap();
mvcc.commit(tx3).unwrap();
let node = mvcc.read_node(tx2, 1).unwrap().unwrap();
assert!(!node.properties.contains_key("version"));
mvcc.commit(tx2).unwrap();
}
#[test]
fn test_write_write_conflict() {
let mvcc = MvccManager::new();
let setup_tx = mvcc.begin(IsolationLevel::ReadCommitted);
mvcc.write_node(setup_tx, make_node(1)).unwrap();
mvcc.commit(setup_tx).unwrap();
let tx1 = mvcc.begin(IsolationLevel::RepeatableRead);
let tx2 = mvcc.begin(IsolationLevel::RepeatableRead);
let _n1 = mvcc.read_node(tx1, 1).unwrap();
let _n2 = mvcc.read_node(tx2, 1).unwrap();
let mut node1 = make_node(1);
node1
.properties
.insert("writer".to_string(), serde_json::json!("tx1"));
mvcc.write_node(tx1, node1).unwrap();
mvcc.commit(tx1).unwrap();
let mut node2 = make_node(1);
node2
.properties
.insert("writer".to_string(), serde_json::json!("tx2"));
mvcc.write_node(tx2, node2).unwrap();
let result = mvcc.commit(tx2);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("conflict"));
}
#[test]
fn test_serializable_read_write_conflict() {
let mvcc = MvccManager::new();
let setup_tx = mvcc.begin(IsolationLevel::Serializable);
mvcc.write_node(setup_tx, make_node(1)).unwrap();
mvcc.commit(setup_tx).unwrap();
let tx1 = mvcc.begin(IsolationLevel::Serializable);
let _n = mvcc.read_node(tx1, 1).unwrap();
let tx2 = mvcc.begin(IsolationLevel::Serializable);
mvcc.write_node(tx2, make_node(1)).unwrap();
mvcc.commit(tx2).unwrap();
mvcc.write_node(tx1, make_node(2)).unwrap();
let result = mvcc.commit(tx1);
assert!(result.is_err());
}
#[test]
fn test_rollback() {
let mvcc = MvccManager::new();
let tx1 = mvcc.begin(IsolationLevel::ReadCommitted);
mvcc.write_node(tx1, make_node(1)).unwrap();
mvcc.rollback(tx1).unwrap();
let tx2 = mvcc.begin(IsolationLevel::ReadCommitted);
let node = mvcc.read_node(tx2, 1).unwrap();
assert!(node.is_none());
}
}