feat(transactions): add single-node transaction support with session-aware reads, commits, aborts, and transaction metrics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user