mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-10 02:34:20 +01:00
database: implement missing cases for is_version_visible + tests
Following the Hekaton paper tables, but also taking into account that in iteration 0 we're only interested in snapshot isolation, not serializability.
This commit is contained in:
@@ -147,7 +147,8 @@ impl std::fmt::Display for Transaction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}",
|
||||
"{{ state: {}, id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}",
|
||||
self.state,
|
||||
self.tx_id,
|
||||
self.begin_ts,
|
||||
// FIXME: I'm sorry, we obviously shouldn't be cloning here.
|
||||
@@ -168,10 +169,23 @@ impl std::fmt::Display for Transaction {
|
||||
enum TransactionState {
|
||||
Active,
|
||||
Preparing,
|
||||
Committed,
|
||||
Committed(u64),
|
||||
Aborted,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransactionState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
TransactionState::Active => write!(f, "Active"),
|
||||
TransactionState::Preparing => write!(f, "Preparing"),
|
||||
TransactionState::Committed(ts) => write!(f, "Committed({ts})"),
|
||||
TransactionState::Aborted => write!(f, "Aborted"),
|
||||
TransactionState::Terminated => write!(f, "Terminated"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Database<Clock: LogicalClock> {
|
||||
rows: SkipMap<RowID, RwLock<Vec<RowVersion>>>,
|
||||
@@ -459,6 +473,10 @@ impl<Clock: LogicalClock> Database<Clock> {
|
||||
only if TE commits.
|
||||
"""
|
||||
*/
|
||||
tx.state = TransactionState::Committed(end_ts);
|
||||
tracing::trace!("COMMIT {tx}");
|
||||
// Postprocessing: inserting row versions and logging the transaction to persistent storage.
|
||||
// TODO: we should probably save to persistent storage first, and only then update the in-memory structures.
|
||||
let mut log_record: LogRecord = LogRecord::new(end_ts);
|
||||
for id in &tx.write_set {
|
||||
let id = id.value();
|
||||
@@ -480,8 +498,6 @@ impl<Clock: LogicalClock> Database<Clock> {
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.state = TransactionState::Committed;
|
||||
tracing::trace!("COMMIT {tx}");
|
||||
// We have now updated all the versions with a reference to the
|
||||
// transaction ID to a timestamp and can, therefore, remove the
|
||||
// transaction. Please note that when we move to lockless, the
|
||||
@@ -595,7 +611,7 @@ impl<Clock: LogicalClock> Database<Clock> {
|
||||
|
||||
/// A write-write conflict happens when transaction T_m attempts to update a
|
||||
/// row version that is currently being updated by an active transaction T_n.
|
||||
fn is_write_write_conflict(
|
||||
pub(crate) fn is_write_write_conflict(
|
||||
txs: &SkipMap<TxID, RwLock<Transaction>>,
|
||||
tx: &Transaction,
|
||||
rv: &RowVersion,
|
||||
@@ -607,7 +623,7 @@ fn is_write_write_conflict(
|
||||
match te.state {
|
||||
TransactionState::Active => tx.tx_id != te.tx_id,
|
||||
TransactionState::Preparing => todo!(),
|
||||
TransactionState::Committed => todo!(),
|
||||
TransactionState::Committed(_end_ts) => todo!(),
|
||||
TransactionState::Aborted => todo!(),
|
||||
TransactionState::Terminated => todo!(),
|
||||
}
|
||||
@@ -617,7 +633,7 @@ fn is_write_write_conflict(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_version_visible(
|
||||
pub(crate) fn is_version_visible(
|
||||
txs: &SkipMap<TxID, RwLock<Transaction>>,
|
||||
tx: &Transaction,
|
||||
rv: &RowVersion,
|
||||
@@ -635,13 +651,22 @@ fn is_begin_visible(
|
||||
TxTimestampOrID::TxID(rv_begin) => {
|
||||
let tb = txs.get(&rv_begin).unwrap();
|
||||
let tb = tb.value().read().unwrap();
|
||||
match tb.state {
|
||||
let visible = match tb.state {
|
||||
TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(),
|
||||
TransactionState::Preparing => todo!(),
|
||||
TransactionState::Committed => todo!(),
|
||||
TransactionState::Aborted => todo!(),
|
||||
TransactionState::Terminated => todo!(),
|
||||
}
|
||||
TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable!
|
||||
TransactionState::Committed(committed_ts) => tx.begin_ts >= committed_ts,
|
||||
TransactionState::Aborted => false,
|
||||
TransactionState::Terminated => {
|
||||
tracing::debug!("TODO: should reread rv's end field - it should have updated the timestamp in the row version by now");
|
||||
false
|
||||
}
|
||||
};
|
||||
tracing::trace!(
|
||||
"is_begin_visible: tx={tx}, tb={tb} rv = {:?}-{:?} visible = {visible}",
|
||||
rv.begin,
|
||||
rv.end
|
||||
);
|
||||
visible
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,13 +681,22 @@ fn is_end_visible(
|
||||
Some(TxTimestampOrID::TxID(rv_end)) => {
|
||||
let te = txs.get(&rv_end).unwrap();
|
||||
let te = te.value().read().unwrap();
|
||||
match te.state {
|
||||
let visible = match te.state {
|
||||
TransactionState::Active => tx.tx_id != te.tx_id,
|
||||
TransactionState::Preparing => todo!(),
|
||||
TransactionState::Committed => todo!(),
|
||||
TransactionState::Aborted => todo!(),
|
||||
TransactionState::Terminated => todo!(),
|
||||
}
|
||||
TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable!
|
||||
TransactionState::Committed(committed_ts) => tx.begin_ts < committed_ts,
|
||||
TransactionState::Aborted => false,
|
||||
TransactionState::Terminated => {
|
||||
tracing::debug!("TODO: should reread rv's end field - it should have updated the timestamp in the row version by now");
|
||||
false
|
||||
}
|
||||
};
|
||||
tracing::trace!(
|
||||
"is_end_visible: tx={tx}, te={te} rv = {:?}-{:?} visible = {visible}",
|
||||
rv.begin,
|
||||
rv.end
|
||||
);
|
||||
visible
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -776,3 +776,157 @@ fn test_storage1() {
|
||||
"testme3"
|
||||
);
|
||||
}
|
||||
|
||||
/* States described in the Hekaton paper *for serializability*:
|
||||
|
||||
Table 1: Case analysis of action to take when version V’s
|
||||
Begin field contains the ID of transaction TB
|
||||
------------------------------------------------------------------------------------------------------
|
||||
TB’s state | TB’s end timestamp | Action to take when transaction T checks visibility of version V.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Active | Not set | V is visible only if TB=T and V’s end timestamp equals infinity.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Preparing | TS | V’s begin timestamp will be TS ut V is not yet committed. Use TS
|
||||
| as V’s begin time when testing visibility. If the test is true,
|
||||
| allow T to speculatively read V. Committed TS V’s begin timestamp
|
||||
| will be TS and V is committed. Use TS as V’s begin time to test
|
||||
| visibility.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Committed | TS | V’s begin timestamp will be TS and V is committed. Use TS as V’s
|
||||
| begin time to test visibility.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Aborted | Irrelevant | Ignore V; it’s a garbage version.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Terminated | Irrelevant | Reread V’s Begin field. TB has terminated so it must have finalized
|
||||
or not found | | the timestamp.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
|
||||
Table 2: Case analysis of action to take when V's End field
|
||||
contains a transaction ID TE.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
TE’s state | TE’s end timestamp | Action to take when transaction T checks visibility of a version V
|
||||
| | as of read time RT.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Active | Not set | V is visible only if TE is not T.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Preparing | TS | V’s end timestamp will be TS provided that TE commits. If TS > RT,
|
||||
| V is visible to T. If TS < RT, T speculatively ignores V.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Committed | TS | V’s end timestamp will be TS and V is committed. Use TS as V’s end
|
||||
| timestamp when testing visibility.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Aborted | Irrelevant | V is visible.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
Terminated | Irrelevant | Reread V’s End field. TE has terminated so it must have finalized
|
||||
or not found | | the timestamp.
|
||||
*/
|
||||
|
||||
fn new_tx(tx_id: TxID, begin_ts: u64, state: TransactionState) -> RwLock<Transaction> {
|
||||
RwLock::new(Transaction {
|
||||
state,
|
||||
tx_id,
|
||||
begin_ts,
|
||||
write_set: SkipSet::new(),
|
||||
read_set: SkipSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[test]
|
||||
fn test_snapshot_isolation_tx_visible1() {
|
||||
let txs: SkipMap<TxID, RwLock<Transaction>> = SkipMap::from_iter([
|
||||
(1, new_tx(1, 1, TransactionState::Committed(2))),
|
||||
(2, new_tx(2, 2, TransactionState::Committed(5))),
|
||||
(3, new_tx(3, 3, TransactionState::Aborted)),
|
||||
(5, new_tx(5, 5, TransactionState::Preparing)),
|
||||
(6, new_tx(6, 6, TransactionState::Committed(10))),
|
||||
(7, new_tx(7, 7, TransactionState::Active)),
|
||||
]);
|
||||
|
||||
let current_tx = new_tx(4, 4, TransactionState::Preparing);
|
||||
let current_tx = current_tx.read().unwrap();
|
||||
|
||||
let rv_visible = |begin: TxTimestampOrID, end: Option<TxTimestampOrID>| {
|
||||
let row_version = RowVersion {
|
||||
begin,
|
||||
end,
|
||||
row: Row {
|
||||
id: RowID {
|
||||
table_id: 1,
|
||||
row_id: 1,
|
||||
},
|
||||
data: "testme".to_string(),
|
||||
},
|
||||
};
|
||||
tracing::debug!("Testing visibility of {row_version:?}");
|
||||
is_version_visible(&txs, ¤t_tx, &row_version)
|
||||
};
|
||||
|
||||
// begin visible: transaction committed with ts < current_tx.begin_ts
|
||||
// end visible: inf
|
||||
assert!(rv_visible(TxTimestampOrID::TxID(1), None));
|
||||
|
||||
// begin invisible: transaction committed with ts > current_tx.begin_ts
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(2), None));
|
||||
|
||||
// begin invisible: transaction aborted
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(3), None));
|
||||
|
||||
// begin visible: timestamp < current_tx.begin_ts
|
||||
// end invisible: transaction committed with ts > current_tx.begin_ts
|
||||
assert!(!rv_visible(
|
||||
TxTimestampOrID::Timestamp(0),
|
||||
Some(TxTimestampOrID::TxID(1))
|
||||
));
|
||||
|
||||
// begin visible: timestamp < current_tx.begin_ts
|
||||
// end visible: transaction committed with ts < current_tx.begin_ts
|
||||
assert!(rv_visible(
|
||||
TxTimestampOrID::Timestamp(0),
|
||||
Some(TxTimestampOrID::TxID(2))
|
||||
));
|
||||
|
||||
// begin visible: timestamp < current_tx.begin_ts
|
||||
// end invisible: transaction aborted
|
||||
assert!(!rv_visible(
|
||||
TxTimestampOrID::Timestamp(0),
|
||||
Some(TxTimestampOrID::TxID(3))
|
||||
));
|
||||
|
||||
// begin invisible: transaction preparing
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(5), None));
|
||||
|
||||
// begin invisible: transaction committed with ts > current_tx.begin_ts
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(6), None));
|
||||
|
||||
// begin invisible: transaction active
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(7), None));
|
||||
|
||||
// begin invisible: transaction committed with ts > current_tx.begin_ts
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(6), None));
|
||||
|
||||
// begin invisible: transaction active
|
||||
assert!(!rv_visible(TxTimestampOrID::TxID(7), None));
|
||||
|
||||
// begin visible: timestamp < current_tx.begin_ts
|
||||
// end invisible: transaction preparing
|
||||
assert!(!rv_visible(
|
||||
TxTimestampOrID::Timestamp(0),
|
||||
Some(TxTimestampOrID::TxID(5))
|
||||
));
|
||||
|
||||
// begin invisible: timestamp > current_tx.begin_ts
|
||||
assert!(!rv_visible(
|
||||
TxTimestampOrID::Timestamp(6),
|
||||
Some(TxTimestampOrID::TxID(6))
|
||||
));
|
||||
|
||||
// begin visible: timestamp < current_tx.begin_ts
|
||||
// end visible: some active transaction will eventually overwrite this version,
|
||||
// but that hasn't happened
|
||||
// (this is the https://avi.im/blag/2023/hekaton-paper-typo/ case, I believe!)
|
||||
assert!(rv_visible(
|
||||
TxTimestampOrID::Timestamp(0),
|
||||
Some(TxTimestampOrID::TxID(7))
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user