use std::path::PathBuf;
use std::sync::Arc;
use mongreldb_log::{
CommandEnvelope, CommitLog, CommitReceipt, CommittedEntry, DurabilityLevel, LogError,
LogPosition, LogSnapshot,
};
use mongreldb_types::hlc::{HlcClock, HlcTimestamp};
use mongreldb_types::ids::TransactionId;
use parking_lot::Mutex;
use zeroize::Zeroizing;
use crate::epoch::{Epoch, EpochAuthority};
use crate::txn::GroupCommit;
use crate::wal::{AddedRun, DdlOp, Op, SharedWal};
use crate::MongrelError;
pub const COMMAND_TYPE_TRANSACTION: u32 = 1;
pub fn to_log_control(control: &crate::ExecutionControl) -> mongreldb_log::ExecutionControl {
mongreldb_log::ExecutionControl {
deadline: control.deadline(),
cancellation: None,
}
}
pub(crate) fn fault_as_io(fault: mongreldb_fault::Fault) -> MongrelError {
MongrelError::Io(std::io::Error::other(fault))
}
pub(crate) fn transaction_id_from_txn(txn_id: u64) -> TransactionId {
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&txn_id.to_le_bytes());
TransactionId::from_bytes(bytes)
}
pub(crate) fn commit_nanos(commit_ts: HlcTimestamp) -> u64 {
commit_ts.physical_micros.saturating_mul(1_000)
}
pub struct StandaloneCommitLog {
wal: Arc<Mutex<SharedWal>>,
group: Arc<GroupCommit>,
epoch: Arc<EpochAuthority>,
commit_lock: Arc<Mutex<()>>,
txn_ids: Arc<Mutex<u64>>,
root: PathBuf,
wal_dek: Option<Zeroizing<[u8; 32]>>,
clock: Arc<HlcClock>,
}
impl StandaloneCommitLog {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
wal: Arc<Mutex<SharedWal>>,
group: Arc<GroupCommit>,
epoch: Arc<EpochAuthority>,
commit_lock: Arc<Mutex<()>>,
txn_ids: Arc<Mutex<u64>>,
root: PathBuf,
wal_dek: Option<Zeroizing<[u8; 32]>>,
clock: Arc<HlcClock>,
) -> Self {
Self {
wal,
group,
epoch,
commit_lock,
txn_ids,
root,
wal_dek,
clock,
}
}
pub(crate) fn append_transaction(
&self,
wal: &mut SharedWal,
txn_id: u64,
epoch: Epoch,
commit_ts: HlcTimestamp,
records: Vec<(u64, Op)>,
added_runs: &[AddedRun],
) -> Result<u64, LogError> {
for (table_id, op) in records {
wal.append(txn_id, table_id, op)
.map_err(|error| LogError::Internal(error.to_string()))?;
}
wal.append_commit_at(txn_id, epoch, added_runs, commit_nanos(commit_ts))
.map_err(|error| LogError::Internal(error.to_string()))
}
pub(crate) fn seal_transaction(
&self,
txn_id: u64,
epoch: Epoch,
commit_seq: u64,
commit_ts: Option<HlcTimestamp>,
) -> Result<CommitReceipt, LogError> {
self.group
.await_durable(&self.wal, commit_seq)
.map_err(|error| LogError::Internal(error.to_string()))?;
let commit_ts =
commit_ts.unwrap_or_else(|| self.clock.commit_timestamp(std::iter::empty()));
Ok(CommitReceipt {
transaction_id: transaction_id_from_txn(txn_id),
commit_ts,
log_position: LogPosition {
term: 0,
index: epoch.0,
},
durability: DurabilityLevel::GroupCommit,
})
}
}
impl CommitLog for StandaloneCommitLog {
fn propose(
&self,
command: CommandEnvelope,
control: &mongreldb_log::ExecutionControl,
) -> Result<CommitReceipt, LogError> {
command.verify()?;
control.check()?;
let _commit = self.commit_lock.lock();
let epoch = self.epoch.bump_assigned();
let result = (|| {
let txn_id = crate::txn::allocate_txn_id(&self.txn_ids)
.map_err(|error| LogError::Internal(error.to_string()))?;
let commit_ts = self.clock.commit_timestamp(std::iter::empty());
let record = Op::Ddl(DdlOp::Command {
payload: command.encode(),
});
let commit_seq = {
let mut wal = self.wal.lock();
wal.append(txn_id, crate::database::WAL_TABLE_ID, record)
.and_then(|_| wal.append_commit_at(txn_id, epoch, &[], commit_nanos(commit_ts)))
.map_err(|error| LogError::Internal(error.to_string()))?
};
let receipt = self.seal_transaction(txn_id, epoch, commit_seq, Some(commit_ts))?;
mongreldb_fault::inject("commit.publish.before")
.map_err(|fault| LogError::Internal(fault.to_string()))?;
self.epoch.publish_in_order(epoch);
mongreldb_fault::inject("commit.publish.after")
.map_err(|fault| LogError::Internal(fault.to_string()))?;
Ok(receipt)
})();
if result.is_err() {
self.epoch.abandon(epoch);
}
result
}
fn read_committed(
&self,
after: LogPosition,
limit: usize,
) -> Result<Vec<CommittedEntry>, LogError> {
let _wal = self.wal.lock();
let records = SharedWal::replay_with_dek(&self.root, self.wal_dek.as_ref())
.map_err(|error| LogError::Internal(error.to_string()))?;
let mut commits = std::collections::HashMap::new();
let mut timestamps = std::collections::HashMap::new();
for record in &records {
match record.op {
Op::TxnCommit { epoch, .. } => {
commits.insert(record.txn_id, epoch);
}
Op::CommitTimestamp { unix_nanos } => {
timestamps.insert(record.txn_id, unix_nanos);
}
_ => {}
}
}
let mut entries = Vec::new();
for record in &records {
if entries.len() >= limit {
break;
}
let Op::Ddl(DdlOp::Command { payload }) = &record.op else {
continue;
};
let Some(&epoch) = commits.get(&record.txn_id) else {
continue;
};
if epoch <= after.index {
continue;
}
let envelope = CommandEnvelope::decode(payload)?;
let physical_micros = timestamps.get(&record.txn_id).copied().unwrap_or(0) / 1_000;
entries.push(CommittedEntry {
position: LogPosition {
term: 0,
index: epoch,
},
commit_ts: HlcTimestamp {
physical_micros,
logical: 0,
node_tiebreaker: 0,
},
envelope,
});
}
Ok(entries)
}
fn applied_position(&self) -> LogPosition {
LogPosition {
term: 0,
index: self.wal.lock().durable_seq(),
}
}
fn create_snapshot(&self) -> Result<LogSnapshot, LogError> {
Err(LogError::Unsupported(
"standalone commit log does not create log snapshots; replicated snapshot boundaries arrive in Stage 2",
))
}
fn install_snapshot(&self, _snapshot: LogSnapshot) -> Result<(), LogError> {
Err(LogError::Unsupported(
"standalone commit log does not install log snapshots; replicated snapshot boundaries arrive in Stage 2",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn core_hlc_clock_is_monotonic_across_commit_allocations() {
let clock = HlcClock::new(0, Duration::from_secs(60));
let mut previous = clock.commit_timestamp(std::iter::empty());
for _ in 0..1_000 {
let next = clock.commit_timestamp(std::iter::empty());
assert!(next > previous, "{next} must exceed {previous}");
previous = next;
}
}
#[test]
fn commit_timestamp_exceeds_every_participant_timestamp() {
let clock = HlcClock::new(0, Duration::from_secs(60));
let read_ts = clock.now().unwrap();
let commit_ts = clock.commit_timestamp([read_ts]);
assert!(
commit_ts > read_ts,
"§8.2: commit ts {commit_ts} must exceed read ts {read_ts}"
);
let later = clock.commit_timestamp([read_ts, commit_ts]);
assert!(later > commit_ts);
}
#[test]
fn commit_nanos_carries_the_physical_component() {
let ts = HlcTimestamp {
physical_micros: 1_700_000_000_123_456,
logical: 7,
node_tiebreaker: 2,
};
assert_eq!(commit_nanos(ts), 1_700_000_000_123_456_000);
let saturated = HlcTimestamp {
physical_micros: u64::MAX,
logical: 0,
node_tiebreaker: 0,
};
assert_eq!(commit_nanos(saturated), u64::MAX);
}
#[test]
fn to_log_control_maps_deadline_and_leaves_cancellation_to_checkpoint() {
let control = crate::ExecutionControl::new(None);
let converted = to_log_control(&control);
assert!(converted.deadline.is_none());
assert!(converted.cancellation.is_none());
assert!(converted.check().is_ok());
let deadline = Instant::now() + Duration::from_secs(60);
let control = crate::ExecutionControl::new(Some(deadline));
let converted = to_log_control(&control);
assert_eq!(converted.deadline, Some(deadline));
let expired = crate::ExecutionControl::new(Some(Instant::now()));
let converted = to_log_control(&expired);
assert!(matches!(converted.check(), Err(LogError::DeadlineExceeded)));
}
#[test]
fn transaction_id_mapping_is_injective() {
assert_ne!(transaction_id_from_txn(1), transaction_id_from_txn(2));
assert_eq!(
transaction_id_from_txn(7),
transaction_id_from_txn(7),
"mapping must be deterministic"
);
}
}