use std::sync::{Arc, Mutex, MutexGuard};
use nodedb_types::hlc::{Hlc, HlcClock};
use nodedb_types::temporal::{LsnTimeAnchor, LsnTimeError, LsnTimeMap};
use crate::error::Result;
use crate::record::{RecordType, TimeAnchorPayload, WalRecord};
#[derive(Debug)]
struct State {
map: LsnTimeMap,
last_stamp_ns: u64,
}
#[derive(Debug)]
pub struct TimeAnchors {
clock: Arc<HlcClock>,
state: Mutex<State>,
}
impl TimeAnchors {
pub fn new(clock: Arc<HlcClock>) -> Self {
Self::with_map(clock, LsnTimeMap::new())
}
pub fn with_map(clock: Arc<HlcClock>, map: LsnTimeMap) -> Self {
Self {
clock,
state: Mutex::new(State {
map,
last_stamp_ns: 0,
}),
}
}
pub fn clock(&self) -> &Arc<HlcClock> {
&self.clock
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|p| p.into_inner())
}
pub(crate) fn stamp(&self) -> u64 {
let mut state = self.lock();
let floor = state
.map
.last()
.map_or(0, |a| a.hlc_wall_ns)
.max(state.last_stamp_ns)
.saturating_add(1);
let ns = self.clock.now().wall_ns.max(floor);
state.last_stamp_ns = ns;
ns
}
pub fn record(&self, lsn: u64, hlc_wall_ns: u64) -> std::result::Result<bool, LsnTimeError> {
self.lock().map.push(LsnTimeAnchor::new(lsn, hlc_wall_ns))
}
pub fn absorb_replayed(&self, records: &[WalRecord]) -> Result<()> {
for record in records {
if RecordType::from_raw(record.logical_record_type()) != Some(RecordType::TimeAnchor) {
continue;
}
let payload = TimeAnchorPayload::from_bytes(&record.payload)?;
self.clock.update(Hlc::new(payload.hlc_wall_ns, 0));
if let Err(error) = self.record(record.header.lsn, payload.hlc_wall_ns) {
tracing::warn!(
lsn = record.header.lsn,
hlc_wall_ns = payload.hlc_wall_ns,
%error,
"replayed WAL time anchor skipped"
);
}
}
Ok(())
}
pub fn cover_recovered(&self, last_recovered_lsn: u64) {
let covered = self
.lock()
.map
.last()
.is_some_and(|a| a.lsn >= last_recovered_lsn);
if covered {
return;
}
let ns = self.stamp();
if let Err(error) = self.record(last_recovered_lsn, ns) {
tracing::warn!(
lsn = last_recovered_lsn,
%error,
"recovered WAL tail left without a time anchor"
);
}
}
pub fn lsn_at_or_before(&self, target_ns: u64) -> std::result::Result<u64, LsnTimeError> {
self.lock().map.lsn_at_or_before(target_ns)
}
pub fn lsn_at_or_before_ms(&self, target_ms: i64) -> std::result::Result<u64, LsnTimeError> {
self.lock().map.lsn_at_or_before_ms(target_ms)
}
pub fn commit_ns_of(&self, lsn: u64) -> Option<u64> {
self.lock().map.commit_ns_of(lsn)
}
pub fn anchors(&self) -> Vec<LsnTimeAnchor> {
self.lock().map.anchors().to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::WalRecordArgs;
fn anchor_record(lsn: u64, ns: u64) -> WalRecord {
WalRecord::new(WalRecordArgs {
record_type: RecordType::TimeAnchor as u32,
lsn,
tenant_id: 0,
vshard_id: 0,
database_id: 0,
payload: TimeAnchorPayload::new(ns).to_bytes().to_vec(),
encryption_key: None,
preamble_bytes: None,
})
.unwrap()
}
#[test]
fn stamps_strictly_increase_past_recorded_anchors() {
let anchors = TimeAnchors::new(Arc::new(HlcClock::new()));
let far_future = u64::MAX / 2;
anchors.record(1, far_future).unwrap();
let a = anchors.stamp();
let b = anchors.stamp();
assert!(a > far_future);
assert!(b > a);
}
#[test]
fn replay_folds_anchor_time_into_the_clock() {
let clock = Arc::new(HlcClock::new());
let anchors = TimeAnchors::new(Arc::clone(&clock));
let far_future = u64::MAX / 2;
anchors
.absorb_replayed(&[
anchor_record(3, far_future - 10),
anchor_record(7, far_future),
])
.unwrap();
assert!(clock.peek().wall_ns >= far_future);
assert_eq!(anchors.lsn_at_or_before(far_future - 1).unwrap(), 3);
assert_eq!(anchors.lsn_at_or_before(far_future).unwrap(), 7);
assert!(anchors.stamp() > far_future);
}
#[test]
fn empty_log_resolves_to_lsn_zero_after_open() {
let anchors = TimeAnchors::new(Arc::new(HlcClock::new()));
anchors.cover_recovered(0);
let opened = anchors.anchors()[0];
assert_eq!(opened.lsn, 0);
assert_eq!(anchors.lsn_at_or_before(opened.hlc_wall_ns).unwrap(), 0);
assert_eq!(
anchors.lsn_at_or_before(opened.hlc_wall_ns - 1).unwrap(),
0,
"nothing had committed before the log opened"
);
}
#[test]
fn recovered_tail_is_covered_once() {
let anchors = TimeAnchors::new(Arc::new(HlcClock::new()));
anchors.absorb_replayed(&[anchor_record(4, 1_000)]).unwrap();
anchors.cover_recovered(9);
anchors.cover_recovered(9);
let held = anchors.anchors();
assert_eq!(held.len(), 2);
assert_eq!(held[1].lsn, 9);
assert!(held[1].hlc_wall_ns > 1_000);
}
}