use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use fsqlite_types::{
CommitSeq, PageData, PageNumber, Snapshot, TxnEpoch, TxnId, TxnToken, WitnessKey,
};
use crate::core_types::{CommitIndex, InProcessPageLockTable, TransactionMode, TransactionState};
use crate::lifecycle::MvccError;
use crate::ssi_validation::{
ActiveTxnView, CommittedReaderInfo, CommittedWriterInfo, DiscoveredEdge, SsiAbortReason,
discover_incoming_edges, discover_outgoing_edges,
};
pub const MAX_CONCURRENT_WRITERS: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FcwResult {
Clean,
Conflict {
conflicting_pages: Vec<PageNumber>,
conflicting_commit_seq: CommitSeq,
},
}
#[derive(Debug)]
pub struct ConcurrentHandle {
snapshot: Snapshot,
write_set: HashMap<PageNumber, PageData>,
page_locks: HashSet<PageNumber>,
state: TransactionState,
read_set: HashSet<PageNumber>,
txn_token: TxnToken,
has_in_rw: Cell<bool>,
has_out_rw: Cell<bool>,
marked_for_abort: Cell<bool>,
}
impl ConcurrentHandle {
#[must_use]
pub fn new(snapshot: Snapshot, txn_token: TxnToken) -> Self {
Self {
snapshot,
write_set: HashMap::new(),
page_locks: HashSet::new(),
state: TransactionState::Active,
read_set: HashSet::new(),
txn_token,
has_in_rw: Cell::new(false),
has_out_rw: Cell::new(false),
marked_for_abort: Cell::new(false),
}
}
#[must_use]
pub const fn snapshot(&self) -> &Snapshot {
&self.snapshot
}
#[must_use]
pub const fn state(&self) -> TransactionState {
self.state
}
#[must_use]
pub fn write_set_pages(&self) -> Vec<PageNumber> {
self.write_set.keys().copied().collect()
}
#[must_use]
pub fn write_set_len(&self) -> usize {
self.write_set.len()
}
#[must_use]
pub fn held_locks(&self) -> &HashSet<PageNumber> {
&self.page_locks
}
#[must_use]
pub const fn is_active(&self) -> bool {
matches!(self.state, TransactionState::Active)
}
pub fn mark_committed(&mut self) {
self.state = TransactionState::Committed;
}
pub fn mark_aborted(&mut self) {
self.state = TransactionState::Aborted;
}
pub fn record_read(&mut self, page: PageNumber) {
self.read_set.insert(page);
}
#[must_use]
pub fn read_set(&self) -> &HashSet<PageNumber> {
&self.read_set
}
#[must_use]
pub fn read_set_len(&self) -> usize {
self.read_set.len()
}
#[must_use]
pub const fn txn_token(&self) -> TxnToken {
self.txn_token
}
#[must_use]
pub fn read_witness_keys(&self) -> Vec<WitnessKey> {
self.read_set.iter().map(|&p| WitnessKey::Page(p)).collect()
}
#[must_use]
pub fn write_witness_keys(&self) -> Vec<WitnessKey> {
self.write_set
.keys()
.map(|&p| WitnessKey::Page(p))
.collect()
}
#[must_use]
pub fn has_in_rw(&self) -> bool {
self.has_in_rw.get()
}
#[must_use]
pub fn has_out_rw(&self) -> bool {
self.has_out_rw.get()
}
#[must_use]
pub fn is_marked_for_abort(&self) -> bool {
self.marked_for_abort.get()
}
}
impl ActiveTxnView for ConcurrentHandle {
fn token(&self) -> TxnToken {
self.txn_token
}
fn begin_seq(&self) -> CommitSeq {
self.snapshot.high
}
fn is_active(&self) -> bool {
matches!(self.state, TransactionState::Active)
}
fn read_keys(&self) -> &[WitnessKey] {
&[]
}
fn write_keys(&self) -> &[WitnessKey] {
&[]
}
fn has_in_rw(&self) -> bool {
self.has_in_rw.get()
}
fn has_out_rw(&self) -> bool {
self.has_out_rw.get()
}
fn set_has_out_rw(&self, val: bool) {
self.has_out_rw.set(val);
}
fn set_has_in_rw(&self, val: bool) {
self.has_in_rw.set(val);
}
fn set_marked_for_abort(&self, val: bool) {
self.marked_for_abort.set(val);
}
}
#[derive(Debug, Clone)]
pub struct ConcurrentSavepoint {
pub name: String,
write_set_snapshot: HashMap<PageNumber, PageData>,
write_set_len: usize,
}
impl ConcurrentSavepoint {
#[must_use]
pub fn captured_len(&self) -> usize {
self.write_set_len
}
}
#[derive(Debug)]
pub struct ConcurrentRegistry {
active: HashMap<u64, ConcurrentHandle>,
committed_readers: Vec<CommittedReaderInfo>,
committed_writers: Vec<CommittedWriterInfo>,
next_session_id: u64,
epoch_counter: u32,
}
impl ConcurrentRegistry {
#[must_use]
pub fn new() -> Self {
Self {
active: HashMap::new(),
committed_readers: Vec::new(),
committed_writers: Vec::new(),
next_session_id: 1,
epoch_counter: 0,
}
}
#[must_use]
pub fn active_count(&self) -> usize {
self.active.len()
}
pub fn begin_concurrent(&mut self, snapshot: Snapshot) -> Result<u64, MvccError> {
if self.active.len() >= MAX_CONCURRENT_WRITERS {
return Err(MvccError::Busy);
}
let session_id = self.next_session_id;
self.next_session_id = self.next_session_id.wrapping_add(1);
self.epoch_counter = self.epoch_counter.wrapping_add(1);
let txn_id = TxnId::new(session_id).ok_or(MvccError::InvalidState)?;
let txn_token = TxnToken::new(txn_id, TxnEpoch::new(self.epoch_counter));
let handle = ConcurrentHandle::new(snapshot, txn_token);
self.active.insert(session_id, handle);
Ok(session_id)
}
pub fn iter_active(&self) -> impl Iterator<Item = (u64, &ConcurrentHandle)> {
self.active.iter().map(|(&id, h)| (id, h))
}
#[must_use]
pub fn get(&self, session_id: u64) -> Option<&ConcurrentHandle> {
self.active.get(&session_id)
}
pub fn get_mut(&mut self, session_id: u64) -> Option<&mut ConcurrentHandle> {
self.active.get_mut(&session_id)
}
pub fn remove(&mut self, session_id: u64) -> Option<ConcurrentHandle> {
self.active.remove(&session_id)
}
fn prune_committed_conflict_history(&mut self) {
let Some(min_active_begin) = self.gc_horizon() else {
self.committed_readers.clear();
self.committed_writers.clear();
return;
};
self.committed_readers
.retain(|reader| reader.commit_seq > min_active_begin);
self.committed_writers
.retain(|writer| writer.commit_seq > min_active_begin);
}
#[must_use]
pub fn gc_horizon(&self) -> Option<CommitSeq> {
self.active
.values()
.filter(|h| h.is_active())
.map(|h| h.snapshot.high)
.min()
}
}
impl Default for ConcurrentRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn concurrent_write_page(
handle: &mut ConcurrentHandle,
lock_table: &InProcessPageLockTable,
session_id: u64,
page: PageNumber,
data: PageData,
) -> Result<(), MvccError> {
if !handle.is_active() {
return Err(MvccError::InvalidState);
}
let txn_id = TxnId::new(session_id).ok_or(MvccError::InvalidState)?;
if handle.page_locks.insert(page) && lock_table.try_acquire(page, txn_id).is_err() {
handle.page_locks.remove(&page);
return Err(MvccError::Busy);
}
handle.write_set.insert(page, data);
Ok(())
}
#[must_use]
pub fn concurrent_read_page(handle: &ConcurrentHandle, page: PageNumber) -> Option<&PageData> {
handle.write_set.get(&page)
}
pub fn validate_first_committer_wins(
handle: &ConcurrentHandle,
commit_index: &CommitIndex,
) -> FcwResult {
let snapshot_seq = handle.snapshot.high;
let mut conflicting_pages = Vec::new();
let mut max_conflicting_seq = CommitSeq::ZERO;
for &page in handle.write_set.keys() {
if let Some(committed_seq) = commit_index.latest(page) {
if committed_seq > snapshot_seq {
conflicting_pages.push(page);
if committed_seq > max_conflicting_seq {
max_conflicting_seq = committed_seq;
}
}
}
}
if conflicting_pages.is_empty() {
tracing::debug!(
write_set_size = handle.write_set.len(),
snapshot_seq = snapshot_seq.get(),
"fcw_validation: clean (no base drift)"
);
FcwResult::Clean
} else {
conflicting_pages.sort();
tracing::warn!(
conflicting_page_count = conflicting_pages.len(),
max_conflicting_seq = max_conflicting_seq.get(),
snapshot_seq = snapshot_seq.get(),
"fcw_validation: base drift detected"
);
FcwResult::Conflict {
conflicting_pages,
conflicting_commit_seq: max_conflicting_seq,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SsiResult {
Clean,
Abort { reason: SsiAbortReason },
}
#[derive(Debug, Clone)]
pub struct PreparedConcurrentCommit {
session_id: u64,
assigned_commit_seq: CommitSeq,
txn_token: TxnToken,
begin_seq: CommitSeq,
read_pages: Vec<PageNumber>,
write_pages: Vec<PageNumber>,
has_in_rw: bool,
has_out_rw: bool,
incoming_edges: Vec<DiscoveredEdge>,
outgoing_edges: Vec<DiscoveredEdge>,
}
impl PreparedConcurrentCommit {
#[must_use]
pub const fn session_id(&self) -> u64 {
self.session_id
}
#[must_use]
pub const fn assigned_commit_seq(&self) -> CommitSeq {
self.assigned_commit_seq
}
#[must_use]
pub const fn txn_token(&self) -> TxnToken {
self.txn_token
}
#[must_use]
pub const fn begin_seq(&self) -> CommitSeq {
self.begin_seq
}
#[must_use]
pub const fn has_in_rw(&self) -> bool {
self.has_in_rw
}
#[must_use]
pub const fn has_out_rw(&self) -> bool {
self.has_out_rw
}
#[must_use]
pub fn read_pages(&self) -> &[PageNumber] {
&self.read_pages
}
#[must_use]
pub fn write_pages(&self) -> &[PageNumber] {
&self.write_pages
}
}
struct HandleView<'a> {
handle: &'a ConcurrentHandle,
read_keys: Vec<WitnessKey>,
write_keys: Vec<WitnessKey>,
}
impl<'a> HandleView<'a> {
fn new(handle: &'a ConcurrentHandle) -> Self {
Self {
handle,
read_keys: handle.read_witness_keys(),
write_keys: handle.write_witness_keys(),
}
}
}
impl ActiveTxnView for HandleView<'_> {
fn token(&self) -> TxnToken {
self.handle.txn_token
}
fn begin_seq(&self) -> CommitSeq {
self.handle.snapshot.high
}
fn is_active(&self) -> bool {
self.handle.is_active()
}
fn read_keys(&self) -> &[WitnessKey] {
&self.read_keys
}
fn write_keys(&self) -> &[WitnessKey] {
&self.write_keys
}
fn has_in_rw(&self) -> bool {
self.handle.has_in_rw()
}
fn has_out_rw(&self) -> bool {
self.handle.has_out_rw()
}
fn set_has_out_rw(&self, val: bool) {
self.handle.has_out_rw.set(val);
}
fn set_has_in_rw(&self, val: bool) {
self.handle.has_in_rw.set(val);
}
fn set_marked_for_abort(&self, val: bool) {
self.handle.marked_for_abort.set(val);
}
}
pub fn concurrent_commit(
handle: &mut ConcurrentHandle,
commit_index: &CommitIndex,
lock_table: &InProcessPageLockTable,
session_id: u64,
assign_commit_seq: CommitSeq,
) -> Result<CommitSeq, (MvccError, FcwResult)> {
if !handle.is_active() {
return Err((MvccError::InvalidState, FcwResult::Clean));
}
let txn_id = TxnId::new(session_id).ok_or((MvccError::InvalidState, FcwResult::Clean))?;
let fcw_result = validate_first_committer_wins(handle, commit_index);
match &fcw_result {
FcwResult::Clean => {
if handle.is_marked_for_abort() {
tracing::warn!(
txn = %txn_id,
"concurrent_commit: SSI marked_for_abort"
);
lock_table.release_all(txn_id);
handle.mark_aborted();
return Err((MvccError::BusySnapshot, FcwResult::Clean));
}
if handle.has_in_rw() && handle.has_out_rw() {
tracing::warn!(
txn = %txn_id,
"concurrent_commit: SSI pivot (in+out rw edges)"
);
lock_table.release_all(txn_id);
handle.mark_aborted();
return Err((MvccError::BusySnapshot, FcwResult::Clean));
}
for &page in handle.write_set.keys() {
commit_index.update(page, assign_commit_seq);
}
lock_table.release_all(txn_id);
handle.mark_committed();
Ok(assign_commit_seq)
}
FcwResult::Conflict { .. } => {
lock_table.release_all(txn_id);
handle.mark_aborted();
Err((MvccError::BusySnapshot, fcw_result))
}
}
}
#[allow(clippy::too_many_lines)]
pub fn prepare_concurrent_commit_with_ssi(
registry: &mut ConcurrentRegistry,
commit_index: &CommitIndex,
lock_table: &InProcessPageLockTable,
session_id: u64,
assign_commit_seq: CommitSeq,
) -> Result<PreparedConcurrentCommit, (MvccError, FcwResult)> {
let txn_id = TxnId::new(session_id).ok_or((MvccError::InvalidState, FcwResult::Clean))?;
{
let handle = registry
.get(session_id)
.ok_or((MvccError::InvalidState, FcwResult::Clean))?;
if !handle.is_active() {
return Err((MvccError::InvalidState, FcwResult::Clean));
}
let fcw_result = validate_first_committer_wins(handle, commit_index);
if !matches!(fcw_result, FcwResult::Clean) {
lock_table.release_all(txn_id);
if let Some(handle) = registry.get_mut(session_id) {
handle.mark_aborted();
}
return Err((MvccError::BusySnapshot, fcw_result));
}
}
let (txn, begin_seq, read_keys, write_keys, marked_for_abort, mut read_pages, mut write_pages) = {
let handle = registry
.get(session_id)
.ok_or((MvccError::InvalidState, FcwResult::Clean))?;
let mut read_pages: Vec<PageNumber> = handle.read_set().iter().copied().collect();
read_pages.sort_unstable();
let mut write_pages: Vec<PageNumber> = handle.write_set.keys().copied().collect();
write_pages.sort_unstable();
(
handle.txn_token(),
handle.snapshot().high,
handle.read_witness_keys(),
handle.write_witness_keys(),
handle.is_marked_for_abort(),
read_pages,
write_pages,
)
};
let views: Vec<HandleView<'_>> = registry
.iter_active()
.filter(|(_, other)| other.is_active())
.map(|(_, other)| HandleView::new(other))
.collect();
let active_views: Vec<&dyn ActiveTxnView> = views
.iter()
.map(|view| view as &dyn ActiveTxnView)
.collect();
let incoming_edges = discover_incoming_edges(
txn,
begin_seq,
assign_commit_seq,
&write_keys,
&active_views,
®istry.committed_readers,
);
let outgoing_edges = discover_outgoing_edges(
txn,
begin_seq,
assign_commit_seq,
&read_keys,
&active_views,
®istry.committed_writers,
);
let has_in_rw = !incoming_edges.is_empty();
let has_out_rw = !outgoing_edges.is_empty();
if marked_for_abort {
tracing::warn!(
txn = %txn_id,
"prepare_concurrent_commit_with_ssi: marked_for_abort"
);
lock_table.release_all(txn_id);
if let Some(handle) = registry.get_mut(session_id) {
handle.mark_aborted();
}
return Err((MvccError::BusySnapshot, FcwResult::Clean));
}
if has_in_rw && has_out_rw {
tracing::warn!(
txn = %txn_id,
"prepare_concurrent_commit_with_ssi: pivot (in+out rw edges)"
);
lock_table.release_all(txn_id);
if let Some(handle) = registry.get_mut(session_id) {
handle.mark_aborted();
}
return Err((MvccError::BusySnapshot, FcwResult::Clean));
}
let has_committed_reader_pivot = incoming_edges
.iter()
.any(|edge| !edge.source_is_active && edge.source_has_in_rw);
let has_committed_writer_pivot = outgoing_edges
.iter()
.any(|edge| !edge.source_is_active && edge.source_has_in_rw);
if has_committed_reader_pivot || has_committed_writer_pivot {
tracing::warn!(
txn = %txn_id,
committed_reader_pivot = has_committed_reader_pivot,
committed_writer_pivot = has_committed_writer_pivot,
"prepare_concurrent_commit_with_ssi: committed pivot conflict"
);
lock_table.release_all(txn_id);
if let Some(handle) = registry.get_mut(session_id) {
handle.mark_aborted();
}
return Err((MvccError::BusySnapshot, FcwResult::Clean));
}
if let Some(handle) = registry.get_mut(session_id) {
handle.has_in_rw.set(has_in_rw);
handle.has_out_rw.set(has_out_rw);
} else {
return Err((MvccError::InvalidState, FcwResult::Clean));
}
read_pages.sort_unstable();
write_pages.sort_unstable();
Ok(PreparedConcurrentCommit {
session_id,
assigned_commit_seq: assign_commit_seq,
txn_token: txn,
begin_seq,
read_pages,
write_pages,
has_in_rw,
has_out_rw,
incoming_edges,
outgoing_edges,
})
}
#[allow(clippy::too_many_lines)]
pub fn finalize_prepared_concurrent_commit_with_ssi(
registry: &mut ConcurrentRegistry,
commit_index: &CommitIndex,
lock_table: &InProcessPageLockTable,
prepared: &PreparedConcurrentCommit,
committed_seq: CommitSeq,
) {
debug_assert_eq!(
committed_seq, prepared.assigned_commit_seq,
"prepared commit sequence mismatch"
);
let Some(txn_id) = TxnId::new(prepared.session_id) else {
return;
};
let active_views: Vec<HandleView<'_>> = registry
.iter_active()
.filter(|(_, other)| other.is_active())
.map(|(_, other)| HandleView::new(other))
.collect();
let active_refs: Vec<&dyn ActiveTxnView> = active_views
.iter()
.map(|view| view as &dyn ActiveTxnView)
.collect();
let read_keys: Vec<WitnessKey> = prepared
.read_pages
.iter()
.copied()
.map(WitnessKey::Page)
.collect();
let write_keys: Vec<WitnessKey> = prepared
.write_pages
.iter()
.copied()
.map(WitnessKey::Page)
.collect();
let mut incoming_edges = prepared.incoming_edges.clone();
for edge in discover_incoming_edges(
prepared.txn_token,
prepared.begin_seq,
committed_seq,
&write_keys,
&active_refs,
&[],
) {
if incoming_edges
.iter()
.all(|existing| existing.from != edge.from)
{
incoming_edges.push(edge);
}
}
let mut outgoing_edges = prepared.outgoing_edges.clone();
for edge in discover_outgoing_edges(
prepared.txn_token,
prepared.begin_seq,
committed_seq,
&read_keys,
&active_refs,
&[],
) {
if outgoing_edges.iter().all(|existing| existing.to != edge.to) {
outgoing_edges.push(edge);
}
}
let has_in_rw = !incoming_edges.is_empty();
let has_out_rw = !outgoing_edges.is_empty();
for edge in &incoming_edges {
if !edge.source_is_active {
continue;
}
if let Some(reader) = registry
.active
.values_mut()
.find(|reader| reader.is_active() && reader.txn_token() == edge.from)
{
reader.set_has_out_rw(true);
if reader.has_in_rw() {
reader.set_marked_for_abort(true);
}
}
}
for edge in &outgoing_edges {
if !edge.source_is_active {
continue;
}
if let Some(writer) = registry
.active
.values_mut()
.find(|writer| writer.is_active() && writer.txn_token() == edge.to)
{
writer.set_has_in_rw(true);
if writer.has_out_rw() {
writer.set_marked_for_abort(true);
}
}
}
let Some(handle) = registry.get_mut(prepared.session_id) else {
return;
};
if !handle.is_active() {
return;
}
handle.has_in_rw.set(has_in_rw);
handle.has_out_rw.set(has_out_rw);
for &page in &prepared.write_pages {
commit_index.update(page, committed_seq);
}
lock_table.release_all(txn_id);
handle.mark_committed();
if !prepared.read_pages.is_empty() {
registry.committed_readers.push(CommittedReaderInfo {
token: prepared.txn_token,
begin_seq: prepared.begin_seq,
commit_seq: committed_seq,
had_in_rw: has_in_rw,
pages: prepared.read_pages.clone(),
});
}
if !prepared.write_pages.is_empty() {
registry.committed_writers.push(CommittedWriterInfo {
token: prepared.txn_token,
commit_seq: committed_seq,
had_out_rw: has_out_rw,
pages: prepared.write_pages.clone(),
});
}
registry.prune_committed_conflict_history();
}
#[allow(clippy::too_many_lines)]
pub fn concurrent_commit_with_ssi(
registry: &mut ConcurrentRegistry,
commit_index: &CommitIndex,
lock_table: &InProcessPageLockTable,
session_id: u64,
assign_commit_seq: CommitSeq,
) -> Result<CommitSeq, (MvccError, FcwResult)> {
let prepared = prepare_concurrent_commit_with_ssi(
registry,
commit_index,
lock_table,
session_id,
assign_commit_seq,
)?;
finalize_prepared_concurrent_commit_with_ssi(
registry,
commit_index,
lock_table,
&prepared,
assign_commit_seq,
);
Ok(assign_commit_seq)
}
pub fn concurrent_abort(
handle: &mut ConcurrentHandle,
lock_table: &InProcessPageLockTable,
session_id: u64,
) {
if let Some(txn_id) = TxnId::new(session_id) {
lock_table.release_all(txn_id);
}
handle.mark_aborted();
}
pub fn concurrent_savepoint(
handle: &ConcurrentHandle,
name: &str,
) -> Result<ConcurrentSavepoint, MvccError> {
if !handle.is_active() {
return Err(MvccError::InvalidState);
}
Ok(ConcurrentSavepoint {
name: name.to_owned(),
write_set_snapshot: handle.write_set.clone(),
write_set_len: handle.write_set.len(),
})
}
pub fn concurrent_rollback_to_savepoint(
handle: &mut ConcurrentHandle,
savepoint: &ConcurrentSavepoint,
) -> Result<(), MvccError> {
if !handle.is_active() {
return Err(MvccError::InvalidState);
}
handle.write_set.clone_from(&savepoint.write_set_snapshot);
Ok(())
}
#[must_use]
pub const fn is_concurrent_mode(mode: TransactionMode) -> bool {
matches!(mode, TransactionMode::Concurrent)
}
#[cfg(test)]
mod tests {
use fsqlite_types::{CommitSeq, PageData, PageNumber, PageSize, SchemaEpoch, Snapshot};
use crate::core_types::{CommitIndex, InProcessPageLockTable};
use crate::lifecycle::MvccError;
use super::{
ConcurrentRegistry, FcwResult, MAX_CONCURRENT_WRITERS, concurrent_abort, concurrent_commit,
concurrent_read_page, concurrent_rollback_to_savepoint, concurrent_savepoint,
concurrent_write_page, validate_first_committer_wins,
};
fn test_snapshot(high: u64) -> Snapshot {
Snapshot {
high: CommitSeq::new(high),
schema_epoch: SchemaEpoch::ZERO,
}
}
fn test_page(n: u32) -> PageNumber {
PageNumber::new(n).expect("page number must be nonzero")
}
fn test_data() -> PageData {
PageData::zeroed(PageSize::DEFAULT)
}
#[test]
fn test_begin_concurrent_multiple_writers() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 1");
let s2 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 2");
let h1 = registry.get_mut(s1).expect("handle 1");
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data())
.expect("write page 5");
let h2 = registry.get_mut(s2).expect("handle 2");
concurrent_write_page(h2, &lock_table, s2, test_page(10), test_data())
.expect("write page 10");
let h1 = registry.get_mut(s1).expect("handle 1");
let seq1 = concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11))
.expect("commit 1");
assert_eq!(seq1, CommitSeq::new(11));
let h2 = registry.get_mut(s2).expect("handle 2");
let seq2 = concurrent_commit(h2, &commit_index, &lock_table, s2, CommitSeq::new(12))
.expect("commit 2");
assert_eq!(seq2, CommitSeq::new(12));
}
#[test]
fn test_begin_concurrent_page_conflict_busy_snapshot() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 1");
let s2 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 2");
let h1 = registry.get_mut(s1).expect("handle 1");
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data())
.expect("s1 write page 5");
let h1 = registry.get_mut(s1).expect("handle 1");
concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11))
.expect("s1 commits first");
let h2 = registry.get_mut(s2).expect("handle 2");
concurrent_write_page(h2, &lock_table, s2, test_page(5), test_data())
.expect("s2 write page 5");
let h2 = registry.get_mut(s2).expect("handle 2");
let result = concurrent_commit(h2, &commit_index, &lock_table, s2, CommitSeq::new(12));
assert!(result.is_err());
let (err, fcw) = result.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
assert!(matches!(fcw, FcwResult::Conflict { .. }));
}
#[test]
fn test_begin_concurrent_first_committer_wins() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 1");
let s2 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 2");
let s3 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 3");
let h1 = registry.get_mut(s1).expect("h1");
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data()).unwrap();
let h3 = registry.get_mut(s3).expect("h3");
concurrent_write_page(h3, &lock_table, s3, test_page(10), test_data()).unwrap();
let h1 = registry.get_mut(s1).expect("h1");
concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11))
.expect("s1 commits");
let h2 = registry.get_mut(s2).expect("h2");
concurrent_write_page(h2, &lock_table, s2, test_page(5), test_data()).unwrap();
let h2 = registry.get_mut(s2).expect("h2");
let result = concurrent_commit(h2, &commit_index, &lock_table, s2, CommitSeq::new(12));
assert!(result.is_err());
let (err, _) = result.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
let h3 = registry.get_mut(s3).expect("h3");
let seq3 = concurrent_commit(h3, &commit_index, &lock_table, s3, CommitSeq::new(13))
.expect("s3 commits");
assert_eq!(seq3, CommitSeq::new(13));
}
#[test]
fn test_savepoint_within_concurrent() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(1), test_data()).unwrap();
let handle = registry.get(s1).expect("handle");
let sp = concurrent_savepoint(handle, "sp1").unwrap();
assert_eq!(sp.captured_len(), 1);
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(2), test_data()).unwrap();
assert_eq!(handle.write_set_len(), 2);
let handle = registry.get_mut(s1).expect("handle");
concurrent_rollback_to_savepoint(handle, &sp).unwrap();
assert_eq!(handle.write_set_len(), 1);
assert!(handle.held_locks().contains(&test_page(2)));
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(3), test_data()).unwrap();
let handle = registry.get_mut(s1).expect("handle");
let mut pages = handle.write_set_pages();
pages.sort();
assert_eq!(pages, vec![test_page(1), test_page(3)]);
let handle = registry.get_mut(s1).expect("handle");
concurrent_commit(handle, &commit_index, &lock_table, s1, CommitSeq::new(11))
.expect("commit succeeds");
}
#[test]
fn test_concurrent_read_local_vs_mvcc() {
let lock_table = InProcessPageLockTable::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get(s1).expect("handle");
assert!(concurrent_read_page(handle, test_page(5)).is_none());
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(5), test_data()).unwrap();
let handle = registry.get(s1).expect("handle");
assert!(concurrent_read_page(handle, test_page(5)).is_some());
assert!(concurrent_read_page(handle, test_page(6)).is_none());
}
#[test]
fn test_concurrent_abort_releases_locks() {
let lock_table = InProcessPageLockTable::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(5), test_data()).unwrap();
concurrent_write_page(handle, &lock_table, s1, test_page(6), test_data()).unwrap();
assert_eq!(handle.held_locks().len(), 2);
let handle = registry.get_mut(s1).expect("handle");
concurrent_abort(handle, &lock_table, s1);
assert!(!handle.is_active());
let s2 = registry
.begin_concurrent(test_snapshot(10))
.expect("session 2");
let handle2 = registry.get_mut(s2).expect("handle 2");
concurrent_write_page(handle2, &lock_table, s2, test_page(5), test_data())
.expect("lock should be available after abort");
}
#[test]
fn test_registry_max_concurrent_writers() {
let mut registry = ConcurrentRegistry::new();
for _ in 0..MAX_CONCURRENT_WRITERS {
registry
.begin_concurrent(test_snapshot(1))
.expect("should succeed");
}
let result = registry.begin_concurrent(test_snapshot(1));
assert_eq!(result.unwrap_err(), MvccError::Busy);
}
#[test]
fn test_fcw_validation_clean() {
let commit_index = CommitIndex::new();
let lock_table = InProcessPageLockTable::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(5), test_data()).unwrap();
let handle = registry.get(s1).expect("handle");
assert_eq!(
validate_first_committer_wins(handle, &commit_index),
FcwResult::Clean
);
}
#[test]
fn test_fcw_validation_conflict() {
let commit_index = CommitIndex::new();
let lock_table = InProcessPageLockTable::new();
let mut registry = ConcurrentRegistry::new();
commit_index.update(test_page(5), CommitSeq::new(15));
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
concurrent_write_page(handle, &lock_table, s1, test_page(5), test_data()).unwrap();
let handle = registry.get(s1).expect("handle");
let result = validate_first_committer_wins(handle, &commit_index);
match result {
FcwResult::Conflict {
conflicting_pages,
conflicting_commit_seq,
} => {
assert_eq!(conflicting_pages, vec![test_page(5)]);
assert_eq!(conflicting_commit_seq, CommitSeq::new(15));
}
FcwResult::Clean => panic!("expected conflict"),
}
}
#[test]
fn test_busy_snapshot_vs_busy() {
assert_ne!(MvccError::BusySnapshot, MvccError::Busy);
assert_eq!(
format!("{}", MvccError::BusySnapshot),
"SQLITE_BUSY_SNAPSHOT"
);
assert_eq!(format!("{}", MvccError::Busy), "SQLITE_BUSY");
}
#[test]
fn test_concurrent_session_lifecycle() {
let mut registry = ConcurrentRegistry::new();
assert_eq!(registry.active_count(), 0);
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
assert_eq!(registry.active_count(), 1);
let handle = registry.get(s1).expect("handle");
assert!(handle.is_active());
let removed = registry.remove(s1);
assert!(removed.is_some());
assert_eq!(registry.active_count(), 0);
}
#[test]
fn test_operations_on_inactive_handle() {
let lock_table = InProcessPageLockTable::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
concurrent_abort(handle, &lock_table, s1);
let handle = registry.get_mut(s1).expect("handle");
let result = concurrent_write_page(handle, &lock_table, s1, test_page(1), test_data());
assert_eq!(result.unwrap_err(), MvccError::InvalidState);
let handle = registry.get(s1).expect("handle");
let result = concurrent_savepoint(handle, "sp1");
assert_eq!(result.unwrap_err(), MvccError::InvalidState);
}
#[test]
fn test_ssi_read_tracking() {
let mut registry = ConcurrentRegistry::new();
let s1 = registry
.begin_concurrent(test_snapshot(10))
.expect("session");
let handle = registry.get_mut(s1).expect("handle");
assert_eq!(handle.read_set_len(), 0);
handle.record_read(test_page(5));
handle.record_read(test_page(10));
handle.record_read(test_page(5));
assert_eq!(handle.read_set_len(), 2);
assert!(handle.read_set().contains(&test_page(5)));
assert!(handle.read_set().contains(&test_page(10)));
}
#[test]
fn test_ssi_no_conflict_disjoint() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(5));
concurrent_write_page(h1, &lock_table, s1, test_page(10), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(20));
concurrent_write_page(h2, &lock_table, s2, test_page(30), test_data()).unwrap();
}
let seq1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
)
.expect("T1 commits");
assert_eq!(seq1, CommitSeq::new(11));
let seq2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
)
.expect("T2 commits");
assert_eq!(seq2, CommitSeq::new(12));
}
#[test]
fn test_ssi_committed_history_pruned_on_completion() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(5));
concurrent_write_page(h1, &lock_table, s1, test_page(10), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(10));
concurrent_write_page(h2, &lock_table, s2, test_page(20), test_data()).unwrap();
}
concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
)
.expect("first txn commits while second is still active");
assert_eq!(
registry.committed_readers.len(),
1,
"reader history retained while overlapping txn is active"
);
assert_eq!(
registry.committed_writers.len(),
1,
"writer history retained while overlapping txn is active"
);
concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
)
.expect("second txn commits");
assert!(
registry.committed_readers.is_empty(),
"reader history pruned once no active transactions remain"
);
assert!(
registry.committed_writers.is_empty(),
"writer history pruned once no active transactions remain"
);
}
#[test]
fn test_ssi_pivot_abort() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(5)); concurrent_write_page(h1, &lock_table, s1, test_page(10), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(10)); concurrent_write_page(h2, &lock_table, s2, test_page(5), test_data()).unwrap();
}
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
);
assert!(
result1.is_err(),
"T1 should abort as pivot (both in and out edges)"
);
let (err, _) = result1.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
let result2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(11),
);
assert!(result2.is_ok(), "T2 should commit after T1 aborted");
}
#[test]
fn test_ssi_marked_for_abort() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let h1 = registry.get_mut(s1).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data()).unwrap();
h1.marked_for_abort.set(true);
let result = concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11));
assert!(result.is_err());
let (err, _) = result.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
}
#[test]
fn test_ssi_only_incoming_edge_commits() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let h1 = registry.get_mut(s1).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data()).unwrap();
h1.has_in_rw.set(true);
h1.has_out_rw.set(false);
let result = concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11));
assert!(result.is_ok(), "only incoming edge should allow commit");
}
#[test]
fn test_ssi_only_outgoing_edge_commits() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let h1 = registry.get_mut(s1).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data()).unwrap();
h1.has_in_rw.set(false);
h1.has_out_rw.set(true);
let result = concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11));
assert!(result.is_ok(), "only outgoing edge should allow commit");
}
#[test]
fn test_ssi_both_edges_aborts() {
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let h1 = registry.get_mut(s1).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(5), test_data()).unwrap();
h1.has_in_rw.set(true);
h1.has_out_rw.set(true);
let result = concurrent_commit(h1, &commit_index, &lock_table, s1, CommitSeq::new(11));
assert!(result.is_err());
let (err, _) = result.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
}
#[test]
fn test_ssi_witness_keys() {
let mut registry = ConcurrentRegistry::new();
let lock_table = InProcessPageLockTable::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(5));
h1.record_read(test_page(10));
concurrent_write_page(h1, &lock_table, s1, test_page(15), test_data()).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(20), test_data()).unwrap();
let read_keys = h1.read_witness_keys();
let write_keys = h1.write_witness_keys();
assert_eq!(read_keys.len(), 2);
assert_eq!(write_keys.len(), 2);
}
#[test]
fn test_ssi_three_txn_pivot_abort_real_components() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s3 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(40)); concurrent_write_page(h1, &lock_table, s1, test_page(30), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(30)); concurrent_write_page(h2, &lock_table, s2, test_page(40), test_data()).unwrap();
}
{
let h3 = registry.get_mut(s3).unwrap();
h3.record_read(test_page(5)); concurrent_write_page(h3, &lock_table, s3, test_page(10), test_data()).unwrap();
}
let result3 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s3,
CommitSeq::new(11),
);
assert!(result3.is_ok(), "T3 disjoint commit must succeed");
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(12),
);
assert!(
result1.is_err(),
"T1 must abort as pivot (both in+out edges with T2)"
);
let (err, _) = result1.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
let result2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
);
assert!(result2.is_ok(), "T2 must commit after pivot T1 aborted");
}
#[test]
fn test_ssi_marked_for_abort_via_real_edge_detection() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s3 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(10));
h1.record_read(test_page(20));
concurrent_write_page(h1, &lock_table, s1, test_page(30), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(50));
concurrent_write_page(h2, &lock_table, s2, test_page(10), test_data()).unwrap();
}
{
let h3 = registry.get_mut(s3).unwrap();
h3.record_read(test_page(30));
concurrent_write_page(h3, &lock_table, s3, test_page(40), test_data()).unwrap();
}
let result3 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s3,
CommitSeq::new(11),
);
assert!(result3.is_ok(), "T3 commits (only outgoing edge)");
{
let h1 = registry.get(s1).unwrap();
assert!(
h1.has_in_rw(),
"T1 must have has_in_rw: T1 writes 30, T3 reads 30"
);
assert!(!h1.has_out_rw(), "T1 must NOT have has_out_rw yet");
assert!(
!h1.is_marked_for_abort(),
"T1 must NOT be marked_for_abort yet"
);
}
let result2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
);
assert!(
result2.is_ok(),
"T2 commits (only incoming edge, not pivot)"
);
{
let h1 = registry.get(s1).unwrap();
assert!(h1.has_in_rw(), "T1 still has has_in_rw (from T3's commit)");
assert!(
h1.has_out_rw(),
"T1 now has has_out_rw (T2's incoming edge scan set it)"
);
assert!(
h1.is_marked_for_abort(),
"T1 must be marked_for_abort: T2 found incoming edge from T1, \
and T1 already had has_in_rw from T3's commit"
);
}
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(13),
);
assert!(
result1.is_err(),
"T1 must abort: marked_for_abort by T2's commit scan"
);
let (err, _) = result1.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
}
#[test]
fn test_ssi_edge_propagation_sets_flags_automatically() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(100));
concurrent_write_page(h1, &lock_table, s1, test_page(200), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(200));
concurrent_write_page(h2, &lock_table, s2, test_page(300), test_data()).unwrap();
}
{
let h1 = registry.get(s1).unwrap();
let h2 = registry.get(s2).unwrap();
assert!(!h1.has_in_rw());
assert!(!h1.has_out_rw());
assert!(!h2.has_in_rw());
assert!(!h2.has_out_rw());
}
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
);
assert!(result1.is_ok(), "T1 commits (only incoming edge)");
{
let h2 = registry.get(s2).unwrap();
assert!(
h2.has_out_rw(),
"T2.has_out_rw must be set: T2 read page 200 that T1 wrote"
);
assert!(
!h2.has_in_rw(),
"T2.has_in_rw must NOT be set: no outgoing edge from T1 to T2"
);
}
let result2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
);
assert!(
result2.is_ok(),
"T2 commits (only outgoing edge, not pivot)"
);
}
#[test]
fn test_fcw_real_commit_index_conflict() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
concurrent_write_page(h1, &lock_table, s1, test_page(42), test_data()).unwrap();
}
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
);
assert!(result1.is_ok(), "T1 first-committer wins");
{
let h2 = registry.get_mut(s2).unwrap();
concurrent_write_page(h2, &lock_table, s2, test_page(42), test_data()).unwrap();
}
let result2 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s2,
CommitSeq::new(12),
);
assert!(result2.is_err(), "T2 must fail: FCW conflict on page 42");
let (err, fcw) = result2.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
assert!(
matches!(fcw, FcwResult::Conflict { .. }),
"FCW must report conflict"
);
}
#[test]
fn test_fcw_deterministic_tiebreak_lower_txn_id_wins() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let token1 = registry.get(s1).unwrap().txn_token();
let token2 = registry.get(s2).unwrap().txn_token();
let (winner_session, loser_session, winner_token) = if token1.id <= token2.id {
(s1, s2, token1)
} else {
(s2, s1, token2)
};
{
let winner_handle = registry.get_mut(winner_session).unwrap();
concurrent_write_page(
winner_handle,
&lock_table,
winner_session,
test_page(77),
test_data(),
)
.unwrap();
}
let winner = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
winner_session,
CommitSeq::new(11),
);
assert!(
winner.is_ok(),
"lower txn_id should deterministically win tie window"
);
{
let loser_handle = registry.get_mut(loser_session).unwrap();
concurrent_write_page(
loser_handle,
&lock_table,
loser_session,
test_page(77),
test_data(),
)
.unwrap();
}
let loser = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
loser_session,
CommitSeq::new(11),
);
assert!(
loser.is_err(),
"higher txn_id should lose deterministic tie"
);
let (err, fcw) = loser.unwrap_err();
assert_eq!(err, MvccError::BusySnapshot);
assert!(matches!(fcw, FcwResult::Conflict { .. }));
let committed = registry
.committed_writers
.iter()
.find(|writer| writer.pages.contains(&test_page(77)))
.map(|writer| writer.token)
.expect("winning writer should be recorded");
assert_eq!(committed, winner_token);
}
#[test]
fn test_prepare_aborts_on_committed_writer_pivot() {
use super::concurrent_commit_with_ssi;
let lock_table = InProcessPageLockTable::new();
let commit_index = CommitIndex::new();
let mut registry = ConcurrentRegistry::new();
let s1 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s2 = registry.begin_concurrent(test_snapshot(10)).unwrap();
let s3 = registry.begin_concurrent(test_snapshot(10)).unwrap();
{
let h1 = registry.get_mut(s1).unwrap();
h1.record_read(test_page(20));
concurrent_write_page(h1, &lock_table, s1, test_page(10), test_data()).unwrap();
}
{
let h2 = registry.get_mut(s2).unwrap();
h2.record_read(test_page(30));
concurrent_write_page(h2, &lock_table, s2, test_page(20), test_data()).unwrap();
}
let result1 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s1,
CommitSeq::new(11),
);
assert!(
result1.is_ok(),
"T1 should commit with only outgoing rw edge"
);
let t1_writer = registry
.committed_writers
.iter()
.find(|entry| entry.token.id.get() == s1)
.expect("T1 writer history should be present");
assert!(
t1_writer.had_out_rw,
"T1 should be recorded with had_out_rw"
);
{
let h3 = registry.get_mut(s3).unwrap();
h3.record_read(test_page(10));
concurrent_write_page(h3, &lock_table, s3, test_page(40), test_data()).unwrap();
}
let result3 = concurrent_commit_with_ssi(
&mut registry,
&commit_index,
&lock_table,
s3,
CommitSeq::new(12),
);
assert!(
result3.is_err(),
"T3 must abort when it depends on committed writer pivot T1"
);
let (err3, _) = result3.unwrap_err();
assert_eq!(err3, MvccError::BusySnapshot);
}
}