use std::collections::BTreeSet;
use nodedb_wal::record::RecordType;
use nodedb_wal::{CalvinAppliedPayload, WalRecord};
use tracing::warn;
use crate::wal::RedoRecord;
use crate::wal::manager::WalManager;
pub const NOT_YET_APPLIED_EPOCH: u64 = u64::MAX;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppliedRecovery {
pub fully_applied_epoch: u64,
pub applied_tail: BTreeSet<(u64, u32)>,
pub max_applied_epoch: u64,
}
pub fn read_applied_recovery(wal: &WalManager, vshard_id: u32) -> crate::Result<AppliedRecovery> {
let records = wal.replay()?;
let mut applied_tail = BTreeSet::new();
let mut max_applied_epoch = NOT_YET_APPLIED_EPOCH;
for record in &records {
match record_type_of(record) {
Some(RecordType::CalvinApplied) => {
match CalvinAppliedPayload::from_bytes(&record.payload) {
Ok(p) if p.vshard_id == vshard_id => {
accumulate(
&mut applied_tail,
&mut max_applied_epoch,
p.epoch,
p.position,
);
}
Ok(_) => {
}
Err(e) => {
warn!(
lsn = record.header.lsn,
error = %e,
"calvin recovery: failed to decode CalvinApplied payload; skipping"
);
}
}
}
Some(RecordType::TransactionRedo) => match RedoRecord::from_bytes(&record.payload) {
Ok(redo) => {
if let Some(stamp) = redo.calvin_stamp
&& stamp.vshard_id == vshard_id
{
accumulate(
&mut applied_tail,
&mut max_applied_epoch,
stamp.epoch,
stamp.position,
);
}
}
Err(e) => {
warn!(
lsn = record.header.lsn,
error = %e,
"calvin recovery: failed to decode TransactionRedo payload; skipping"
);
}
},
_ => continue,
}
}
Ok(AppliedRecovery {
fully_applied_epoch: NOT_YET_APPLIED_EPOCH,
applied_tail,
max_applied_epoch,
})
}
fn record_type_of(record: &WalRecord) -> Option<RecordType> {
let raw_type = record.header.record_type & !nodedb_wal::record::ENCRYPTED_FLAG;
RecordType::from_raw(raw_type)
}
fn accumulate(
applied_tail: &mut BTreeSet<(u64, u32)>,
max_applied_epoch: &mut u64,
epoch: u64,
position: u32,
) {
applied_tail.insert((epoch, position));
if *max_applied_epoch == NOT_YET_APPLIED_EPOCH || epoch > *max_applied_epoch {
*max_applied_epoch = epoch;
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use crate::wal::manager::WalManager;
fn open_wal(dir: &TempDir) -> WalManager {
WalManager::open(dir.path(), false).expect("open wal")
}
#[test]
fn greenfield_returns_sentinel_and_empty_tail() {
let dir = TempDir::new().unwrap();
let wal = open_wal(&dir);
let rec = read_applied_recovery(&wal, 1).unwrap();
assert_eq!(rec.fully_applied_epoch, NOT_YET_APPLIED_EPOCH);
assert_eq!(rec.max_applied_epoch, NOT_YET_APPLIED_EPOCH);
assert!(rec.applied_tail.is_empty());
}
#[test]
fn tail_records_exact_positions_and_max_epoch() {
let dir = TempDir::new().unwrap();
let wal = open_wal(&dir);
use crate::types::VShardId;
wal.append_calvin_applied(VShardId::new(1), 2, 0).unwrap();
wal.append_calvin_applied(VShardId::new(1), 5, 0).unwrap();
wal.append_calvin_applied(VShardId::new(2), 99, 0).unwrap();
wal.sync().unwrap();
let rec = read_applied_recovery(&wal, 1).unwrap();
assert!(rec.applied_tail.contains(&(5, 0)), "(5,0) is applied");
assert!(!rec.applied_tail.contains(&(5, 1)), "(5,1) is NOT applied");
assert!(rec.applied_tail.contains(&(2, 0)));
assert_eq!(rec.max_applied_epoch, 5);
assert_eq!(rec.fully_applied_epoch, NOT_YET_APPLIED_EPOCH);
assert!(rec.fully_applied_epoch == NOT_YET_APPLIED_EPOCH || rec.fully_applied_epoch < 5);
let rec2 = read_applied_recovery(&wal, 2).unwrap();
assert!(rec2.applied_tail.contains(&(99, 0)));
assert_eq!(rec2.max_applied_epoch, 99);
}
#[test]
fn multi_position_epoch_is_not_collapsed() {
let dir = TempDir::new().unwrap();
let wal = open_wal(&dir);
use crate::types::VShardId;
let vshard = 3u32;
wal.append_calvin_applied(VShardId::new(vshard), 7, 0)
.unwrap();
wal.sync().unwrap();
let rec = read_applied_recovery(&wal, vshard).unwrap();
assert!(rec.applied_tail.contains(&(7, 0)));
assert!(
!rec.applied_tail.contains(&(7, 1)),
"position 1 of epoch 7 must be reported as NOT applied so it is \
re-applied on restart rather than lost"
);
}
#[test]
fn transaction_redo_calvin_stamp_unions_with_calvin_applied() {
use crate::types::{DatabaseId, TenantId, VShardId};
use crate::wal::{CalvinStamp, RedoRecord, RedoSubRecord};
let dir = TempDir::new().unwrap();
let wal = open_wal(&dir);
let vshard = 4u32;
wal.append_calvin_applied(VShardId::new(vshard), 1, 0)
.unwrap();
let write_bearing = RedoRecord {
version: 1,
ops: vec![RedoSubRecord {
record_type: nodedb_wal::record::RecordType::Put as u32,
payload: vec![1, 2, 3],
}],
calvin_stamp: Some(CalvinStamp {
epoch: 1,
position: 1,
vshard_id: vshard,
}),
};
wal.append_transaction_redo(
TenantId::new(0),
VShardId::new(vshard),
DatabaseId::DEFAULT,
&write_bearing,
)
.unwrap();
let single_shard = RedoRecord {
version: 1,
ops: vec![RedoSubRecord {
record_type: nodedb_wal::record::RecordType::Put as u32,
payload: vec![9, 9, 9],
}],
calvin_stamp: None,
};
wal.append_transaction_redo(
TenantId::new(0),
VShardId::new(vshard),
DatabaseId::DEFAULT,
&single_shard,
)
.unwrap();
wal.sync().unwrap();
let rec = read_applied_recovery(&wal, vshard).unwrap();
assert!(
rec.applied_tail.contains(&(1, 0)),
"CalvinApplied marker still contributes"
);
assert!(
rec.applied_tail.contains(&(1, 1)),
"TransactionRedo calvin_stamp contributes its (epoch, position) too"
);
assert_eq!(rec.applied_tail.len(), 2);
assert_eq!(rec.max_applied_epoch, 1);
}
}