use std::collections::HashMap;
use crate::calvin::sequencer::entry::SequencerEntry;
use crate::calvin::sequencer::state_machine::SequencerStateMachine;
use crate::calvin::types::{EpochBatch, SchedulerInput};
pub(crate) fn compute_vshard_txn_counts(batch: &EpochBatch) -> HashMap<u32, u32> {
let mut vshard_txn_counts: HashMap<u32, u32> = HashMap::new();
for txn in &batch.txns {
for vshard_id in txn.tx_class.participating_vshards() {
*vshard_txn_counts.entry(vshard_id.as_u32()).or_insert(0) += 1;
}
}
vshard_txn_counts
}
impl SequencerStateMachine {
pub fn replay_epochs_for_vshard(
&self,
entries: &[nodedb_raft::LogEntry],
vshard_id: u32,
from_epoch: u64,
to_epoch: u64,
) -> Vec<SchedulerInput> {
let mut result: Vec<SchedulerInput> = Vec::new();
for entry in entries {
if entry.data.is_empty() {
continue;
}
let seq_entry: SequencerEntry = match zerompk::from_msgpack(&entry.data) {
Ok(e) => e,
Err(err) => {
tracing::warn!(
raft_index = entry.index,
error = %err,
"calvin replay: failed to decode sequencer entry; skipping"
);
continue;
}
};
match seq_entry {
SequencerEntry::EpochBatch { mut batch } => {
if batch.epoch < from_epoch || batch.epoch > to_epoch {
continue;
}
for txn in &mut batch.txns {
txn.tx_class.restore_derived();
}
let vshard_txn_counts = compute_vshard_txn_counts(&batch);
for txn in &batch.txns {
let participates = txn
.tx_class
.participating_vshards()
.iter()
.any(|v| v.as_u32() == vshard_id);
if !participates {
continue;
}
let mut per_vshard = txn.clone();
per_vshard.epoch_system_ms = batch.epoch_system_ms;
per_vshard.epoch_vshard_txn_count =
vshard_txn_counts.get(&vshard_id).copied().unwrap_or(0);
result.push(SchedulerInput::Txn(per_vshard));
}
}
SequencerEntry::ReserveRead { owner, vshard, key } if vshard == vshard_id => {
result.push(SchedulerInput::Reserve { owner, key });
}
SequencerEntry::ReleaseReservation {
owner,
vshard,
reason,
} if vshard == vshard_id => {
result.push(SchedulerInput::Release { owner, reason });
}
SequencerEntry::ReserveRead { .. } => {}
SequencerEntry::ReleaseReservation { .. } => {}
SequencerEntry::CompletionAck { .. } => {}
SequencerEntry::OllpMismatch { .. } => {}
SequencerEntry::TxnRoutingFailed { .. } => {}
SequencerEntry::Vote { .. } => {}
SequencerEntry::Verdict { .. } => {}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calvin::CalvinCompletionRegistry;
use crate::calvin::types::{
EngineKeySet, EpochBatch, LockKeyWire, ReadWriteSet, ReleaseReason, SequencedTxn,
SortedVec, TxClass, TxnIdWire,
};
use nodedb_types::{
TenantId,
id::{DatabaseId, VShardId},
};
use std::collections::HashMap;
use tokio::sync::mpsc;
fn find_two_distinct_collections() -> (String, String) {
let mut first: Option<(String, u32)> = None;
for i in 0u32..512 {
let name = format!("col_{i}");
let vshard = VShardId::from_collection_in_database(DatabaseId::DEFAULT, &name).as_u32();
if let Some((ref fname, fv)) = first {
if fv != vshard {
return (fname.clone(), name);
}
} else {
first = Some((name, vshard));
}
}
panic!("could not find two distinct-vshard collections in 512 tries");
}
fn make_batch_with_two_vshards() -> (EpochBatch, u32, u32) {
let (col_a, col_b) = find_two_distinct_collections();
let real_va = VShardId::from_collection_in_database(DatabaseId::DEFAULT, &col_a).as_u32();
let real_vb = VShardId::from_collection_in_database(DatabaseId::DEFAULT, &col_b).as_u32();
let write_set = ReadWriteSet::new(vec![
EngineKeySet::Document {
collection: col_a,
surrogates: SortedVec::new(vec![1]),
},
EngineKeySet::Document {
collection: col_b,
surrogates: SortedVec::new(vec![2]),
},
]);
let tx_class = TxClass::new(
ReadWriteSet::new(vec![]),
write_set,
vec![],
TenantId::new(1),
None,
crate::calvin::types::VersionedReadSet::default(),
)
.expect("valid TxClass");
let batch = EpochBatch {
epoch: 0,
txns: vec![SequencedTxn {
epoch: 0,
position: 0,
tx_class,
epoch_system_ms: 1_700_000_000_000,
epoch_vshard_txn_count: 1,
lock_owner: None,
}],
epoch_system_ms: 1_700_000_000_000,
};
(batch, real_va, real_vb)
}
fn encode_entry(entry: &SequencerEntry) -> Vec<u8> {
zerompk::to_msgpack_vec(entry).expect("encode")
}
fn log_entry(index: u64, data: Vec<u8>) -> nodedb_raft::LogEntry {
nodedb_raft::LogEntry {
term: 1,
index,
data,
}
}
#[test]
fn replay_mixed_sequence_matches_live_fanout() {
let (batch, va, vb) = make_batch_with_two_vshards();
let owner = TxnIdWire {
epoch: 0,
position: 0,
};
let entries = [
SequencerEntry::EpochBatch {
batch: batch.clone(),
},
SequencerEntry::ReserveRead {
owner,
vshard: va,
key: LockKeyWire::Kv {
collection: "sessions".to_owned(),
key: b"hot".to_vec(),
},
},
SequencerEntry::ReleaseReservation {
owner,
vshard: va,
reason: ReleaseReason::Commit,
},
SequencerEntry::ReserveRead {
owner,
vshard: vb,
key: LockKeyWire::Kv {
collection: "other".to_owned(),
key: b"cold".to_vec(),
},
},
];
let (tx_a, mut rx_a) = mpsc::channel(64);
let (tx_b, mut _rx_b) = mpsc::channel(64);
let mut senders = HashMap::new();
senders.insert(va, tx_a);
senders.insert(vb, tx_b);
let mut sm = SequencerStateMachine::new(senders, CalvinCompletionRegistry::new_detached());
for (i, entry) in entries.iter().enumerate() {
sm.apply(i as u64, &encode_entry(entry));
}
let mut live: Vec<SchedulerInput> = Vec::new();
while let Ok(input) = rx_a.try_recv() {
live.push(input);
}
let log_entries: Vec<nodedb_raft::LogEntry> = entries
.iter()
.enumerate()
.map(|(i, entry)| log_entry(i as u64, encode_entry(entry)))
.collect();
let replayed = sm.replay_epochs_for_vshard(&log_entries, va, 0, u64::MAX);
assert_eq!(live.len(), 3, "live delivered Txn + Reserve + Release");
assert_eq!(replayed.len(), live.len());
assert_eq!(format!("{live:?}"), format!("{replayed:?}"));
match &replayed[0] {
SchedulerInput::Txn(txn) => {
assert_eq!(txn.epoch_system_ms, batch.epoch_system_ms);
assert_eq!(txn.epoch_vshard_txn_count, 1);
assert_eq!(txn.position, 0);
}
other => panic!("expected Txn first, got {other:?}"),
}
assert!(matches!(replayed[1], SchedulerInput::Reserve { .. }));
assert!(matches!(replayed[2], SchedulerInput::Release { .. }));
}
#[test]
fn replay_filters_epoch_range() {
let (batch, va, _vb) = make_batch_with_two_vshards();
let entry = SequencerEntry::EpochBatch { batch };
let log_entries = vec![log_entry(0, encode_entry(&entry))];
let sm =
SequencerStateMachine::new(HashMap::new(), CalvinCompletionRegistry::new_detached());
let out = sm.replay_epochs_for_vshard(&log_entries, va, 1, u64::MAX);
assert!(out.is_empty());
}
}