use crate::state::{ChannelId, ImState, Seq};
pub fn heartbeat_root_cursors(state: &ImState) -> Vec<RootCursor> {
let mut roots: Vec<RootCursor> = state
.channels
.iter()
.filter(|(_, channel)| !channel.is_terminal())
.map(|(&id, ch)| RootCursor {
channel_id: id,
from_seq: ch.cursor.value(),
})
.collect();
roots.sort_unstable_by(|a, b| a.channel_id.cmp(&b.channel_id));
roots
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RootCursor {
pub channel_id: ChannelId,
pub from_seq: Seq,
}
pub fn all_hash_fnv1a(cursors: &[RootCursor]) -> String {
let mut sorted: Vec<RootCursor> = cursors.to_vec();
sorted.sort_unstable_by(|a, b| {
a.channel_id
.cmp(&b.channel_id)
.then(a.from_seq.0.cmp(&b.from_seq.0))
});
let joined = sorted
.iter()
.map(|rc| format!("{}:{}", rc.channel_id.as_str(), rc.from_seq.0))
.collect::<Vec<_>>()
.join("\n");
let mut h: u64 = 0xcbf2_9ce4_8422_2325; for &b in joined.as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{h:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
fn cid(c: char) -> ChannelId {
ChannelId::from_str(&std::iter::repeat(c).take(26).collect::<String>()).unwrap()
}
#[test]
fn empty_set_hash_is_offset_basis() {
let h = all_hash_fnv1a(&[]);
assert_eq!(h, format!("{:016x}", 0xcbf2_9ce4_8422_2325u64));
}
#[test]
fn all_hash_is_string_form_not_raw_bytes() {
let cursors = [
RootCursor {
channel_id: cid('a'),
from_seq: Seq(5),
},
RootCursor {
channel_id: cid('b'),
from_seq: Seq(10),
},
];
let joined = format!("{}:5\n{}:10", cid('a').as_str(), cid('b').as_str());
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &byte in joined.as_bytes() {
h ^= byte as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
let expected = format!("{h:016x}");
assert_eq!(
all_hash_fnv1a(&cursors),
expected,
"allHash 必须用 \"<channelId>:<seq>\" 字符串形态喂 FNV(现网 byte-exact 契约),非 raw 字节"
);
}
#[test]
fn fnv1a_canonical_anchor_vector() {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &byte in b"a:5\nb:10" {
h ^= byte as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
assert_eq!(format!("{h:016x}"), "87da68614226ef44");
}
#[test]
fn order_independent() {
let a = vec![
RootCursor {
channel_id: cid('a'),
from_seq: Seq(1),
},
RootCursor {
channel_id: cid('b'),
from_seq: Seq(2),
},
];
let mut b = a.clone();
b.reverse();
assert_eq!(all_hash_fnv1a(&a), all_hash_fnv1a(&b));
}
#[test]
fn seq_change_flips_hash() {
let a = [RootCursor {
channel_id: cid('a'),
from_seq: Seq(1),
}];
let b = [RootCursor {
channel_id: cid('a'),
from_seq: Seq(2),
}];
assert_ne!(all_hash_fnv1a(&a), all_hash_fnv1a(&b));
}
#[test]
fn always_16_lower_hex() {
let h = all_hash_fnv1a(&[RootCursor {
channel_id: cid('z'),
from_seq: Seq(0),
}]);
assert_eq!(h.len(), 16);
assert!(h
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn heartbeat_omits_terminal_channels() {
let mut state = ImState::new();
let active = cid('a');
let closed = cid('b');
state
.channels
.insert(active, crate::channel::Channel::new(active, 3));
let mut closed_channel = crate::channel::Channel::new(closed, 4);
closed_channel.mark_projection_terminal(Seq(0));
state.channels.insert(closed, closed_channel);
let roots = heartbeat_root_cursors(&state);
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].channel_id, active);
assert_eq!(roots[0].from_seq, Seq(3));
}
}