feat(transactions): add single-node transaction support with session-aware reads, commits, aborts, and transaction metrics

This commit is contained in:
2026-04-29 22:14:46 +00:00
parent e79fe339aa
commit b72e8ed5e7
19 changed files with 913 additions and 77 deletions
+10
View File
@@ -170,6 +170,16 @@ impl SessionEngine {
}
count
}
/// Number of currently tracked logical sessions.
pub fn len(&self) -> usize {
self.sessions.len()
}
/// Whether there are no tracked logical sessions.
pub fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
}
impl Default for SessionEngine {
+104 -11
View File
@@ -18,7 +18,7 @@ pub enum TransactionStatus {
}
/// Describes a write operation within a transaction.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteOp {
Insert,
Update,
@@ -137,6 +137,25 @@ impl TransactionEngine {
Ok(())
}
/// Remove an active transaction and return its buffered state for an
/// external committer that needs to update secondary indexes and oplogs.
pub fn take_transaction(&self, txn_id: &str) -> TransactionResult<TransactionState> {
let state = self
.transactions
.remove(txn_id)
.map(|(_, s)| s)
.ok_or_else(|| TransactionError::NotFound(txn_id.to_string()))?;
if state.status != TransactionStatus::Active {
return Err(TransactionError::InvalidState(format!(
"transaction {} is {:?}, cannot commit",
txn_id, state.status
)));
}
Ok(state)
}
/// Abort a transaction, discarding all buffered writes.
pub fn abort_transaction(&self, txn_id: &str) -> TransactionResult<()> {
let mut state = self
@@ -191,19 +210,32 @@ impl TransactionEngine {
original: Option<Document>,
) {
if let Some(mut state) = self.transactions.get_mut(txn_id) {
let entry = WriteEntry {
op,
doc,
original_doc: original,
};
state
.write_set
.entry(ns.to_string())
.or_default()
.insert(doc_id.to_string(), entry);
let writes = state.write_set.entry(ns.to_string()).or_default();
if let Some(existing) = writes.remove(doc_id) {
if let Some(merged) = merge_write_entry(existing, op, doc, original) {
writes.insert(doc_id.to_string(), merged);
}
} else {
writes.insert(
doc_id.to_string(),
WriteEntry {
op,
doc,
original_doc: original,
},
);
}
}
}
/// Return true if the transaction already has a base snapshot for a namespace.
pub fn has_snapshot(&self, txn_id: &str, ns: &str) -> bool {
self.transactions
.get(txn_id)
.map(|state| state.snapshots.contains_key(ns))
.unwrap_or(false)
}
/// Get a snapshot of documents for a namespace within a transaction,
/// applying the write overlay (inserts, updates, deletes) on top.
pub fn get_snapshot(&self, txn_id: &str, ns: &str) -> Option<Vec<Document>> {
@@ -270,6 +302,67 @@ impl TransactionEngine {
state.snapshots.insert(ns.to_string(), docs);
}
}
/// Number of currently active transactions.
pub fn len(&self) -> usize {
self.transactions.len()
}
/// Whether there are no active transactions.
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
fn merge_write_entry(
existing: WriteEntry,
next_op: WriteOp,
next_doc: Option<Document>,
next_original: Option<Document>,
) -> Option<WriteEntry> {
match (existing.op, next_op) {
(WriteOp::Insert, WriteOp::Update) => Some(WriteEntry {
op: WriteOp::Insert,
doc: next_doc,
original_doc: None,
}),
(WriteOp::Insert, WriteOp::Delete) => None,
(WriteOp::Insert, WriteOp::Insert) => Some(WriteEntry {
op: WriteOp::Insert,
doc: next_doc,
original_doc: None,
}),
(WriteOp::Update, WriteOp::Update) => Some(WriteEntry {
op: WriteOp::Update,
doc: next_doc,
original_doc: existing.original_doc,
}),
(WriteOp::Update, WriteOp::Delete) => Some(WriteEntry {
op: WriteOp::Delete,
doc: None,
original_doc: existing.original_doc,
}),
(WriteOp::Update, WriteOp::Insert) => Some(WriteEntry {
op: WriteOp::Update,
doc: next_doc,
original_doc: existing.original_doc,
}),
(WriteOp::Delete, WriteOp::Insert) => Some(WriteEntry {
op: if existing.original_doc.is_some() {
WriteOp::Update
} else {
WriteOp::Insert
},
doc: next_doc,
original_doc: existing.original_doc,
}),
(WriteOp::Delete, WriteOp::Update) => Some(WriteEntry {
op: WriteOp::Update,
doc: next_doc,
original_doc: existing.original_doc.or(next_original),
}),
(WriteOp::Delete, WriteOp::Delete) => Some(existing),
}
}
impl Default for TransactionEngine {