use crate::l2_cache::{InvalidationBus, InvalidationMessage};
use crate::value::Value;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConsistencyLevel {
#[default]
Eventual,
Strong,
}
pub struct RedisPubSubInvalidationBus {
client: Option<redis::aio::ConnectionManager>,
channel: String,
local_buffer: parking_lot::Mutex<VecDeque<InvalidationMessage>>,
instance_id: String,
}
impl RedisPubSubInvalidationBus {
pub fn new(client: redis::aio::ConnectionManager, instance_id: impl Into<String>) -> Self {
Self {
client: Some(client),
channel: "sz-orm:invalidation".to_string(),
local_buffer: parking_lot::Mutex::new(VecDeque::new()),
instance_id: instance_id.into(),
}
}
pub fn disconnected(instance_id: impl Into<String>) -> Self {
Self {
client: None,
channel: "sz-orm:invalidation".to_string(),
local_buffer: parking_lot::Mutex::new(VecDeque::new()),
instance_id: instance_id.into(),
}
}
pub fn with_channel(mut self, channel: impl Into<String>) -> Self {
self.channel = channel.into();
self
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
fn serialize_message(message: &InvalidationMessage, instance_id: &str) -> String {
let payload = match message {
InvalidationMessage::InvalidateKey(key) => {
serde_json::json!({"type": "key", "key": key, "src": instance_id})
}
InvalidationMessage::InvalidateTable(table) => {
serde_json::json!({"type": "table", "table": table, "src": instance_id})
}
InvalidationMessage::InvalidateAll => {
serde_json::json!({"type": "all", "src": instance_id})
}
};
payload.to_string()
}
#[allow(dead_code)]
fn deserialize_message(json: &str, self_instance_id: &str) -> Option<InvalidationMessage> {
let v: serde_json::Value = serde_json::from_str(json).ok()?;
let src = v.get("src")?.as_str()?;
if src == self_instance_id {
return None;
}
match v.get("type")?.as_str()? {
"key" => {
let key = v.get("key")?.as_str()?;
Some(InvalidationMessage::InvalidateKey(key.to_string()))
}
"table" => {
let table = v.get("table")?.as_str()?;
Some(InvalidationMessage::InvalidateTable(table.to_string()))
}
"all" => Some(InvalidationMessage::InvalidateAll),
_ => None,
}
}
pub fn push_received(&self, message: InvalidationMessage) {
self.local_buffer.lock().push_back(message);
}
}
impl InvalidationBus for RedisPubSubInvalidationBus {
fn publish(&self, message: InvalidationMessage) {
if let Some(client) = &self.client {
let json = Self::serialize_message(&message, &self.instance_id);
let client = client.clone();
let channel = self.channel.clone();
tokio::spawn(async move {
let _: Result<(), _> = redis::cmd("PUBLISH")
.arg(&channel)
.arg(&json)
.query_async(&mut client.clone())
.await;
});
}
}
fn subscribe(&self) -> Box<dyn Iterator<Item = InvalidationMessage> + Send> {
let mut buffer = self.local_buffer.lock();
let drained: Vec<_> = buffer.drain(..).collect();
Box::new(drained.into_iter())
}
}
#[derive(Debug, Clone)]
pub struct NodeAddr {
pub host: String,
pub port: u16,
}
impl NodeAddr {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self {
host: host.into(),
port,
}
}
}
pub struct GossipInvalidationBus {
#[allow(dead_code)]
nodes: Vec<NodeAddr>,
shared_secret: Vec<u8>,
local_buffer: parking_lot::Mutex<VecDeque<InvalidationMessage>>,
seen_messages: parking_lot::RwLock<HashSet<u64>>,
instance_id: String,
sequence: AtomicU64,
}
impl GossipInvalidationBus {
pub fn new(
nodes: Vec<NodeAddr>,
shared_secret: Vec<u8>,
instance_id: impl Into<String>,
) -> Self {
Self {
nodes,
shared_secret,
local_buffer: parking_lot::Mutex::new(VecDeque::new()),
seen_messages: parking_lot::RwLock::new(HashSet::new()),
instance_id: instance_id.into(),
sequence: AtomicU64::new(0),
}
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
fn message_id(&self) -> u64 {
self.sequence.fetch_add(1, Ordering::SeqCst)
}
fn compute_hmac(&self, message: &InvalidationMessage) -> Vec<u8> {
let msg_bytes = format!("{:?}", message);
sz_orm_crypto::hmac_sha256(&self.shared_secret, msg_bytes.as_bytes()).to_vec()
}
fn verify_hmac(&self, message: &InvalidationMessage, tag: &[u8]) -> bool {
let expected = self.compute_hmac(message);
expected == tag
}
pub fn receive(&self, message: InvalidationMessage, msg_id: u64, hmac_tag: &[u8]) -> bool {
if !self.verify_hmac(&message, hmac_tag) {
return false;
}
let mut seen = self.seen_messages.write();
if !seen.insert(msg_id) {
return false; }
drop(seen);
self.local_buffer.lock().push_back(message);
true
}
}
impl InvalidationBus for GossipInvalidationBus {
fn publish(&self, message: InvalidationMessage) {
let msg_id = self.message_id();
let _hmac_tag = self.compute_hmac(&message);
let mut seen = self.seen_messages.write();
seen.insert(msg_id);
drop(seen);
self.local_buffer.lock().push_back(message);
}
fn subscribe(&self) -> Box<dyn Iterator<Item = InvalidationMessage> + Send> {
let mut buffer = self.local_buffer.lock();
let drained: Vec<_> = buffer.drain(..).collect();
Box::new(drained.into_iter())
}
}
#[derive(Debug, Clone)]
pub struct WriteBehindConfig {
pub batch_size: u32,
pub flush_interval: Duration,
pub wal_path: PathBuf,
pub encryption_key: Vec<u8>,
pub fallback_to_sync: bool,
}
impl Default for WriteBehindConfig {
fn default() -> Self {
Self {
batch_size: 100,
flush_interval: Duration::from_millis(100),
wal_path: PathBuf::from("wal/sz-orm-wal.log"),
encryption_key: Vec::new(),
fallback_to_sync: true,
}
}
}
impl WriteBehindConfig {
pub fn builder() -> WriteBehindConfigBuilder {
WriteBehindConfigBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct WriteBehindConfigBuilder {
batch_size: Option<u32>,
flush_interval: Option<Duration>,
wal_path: Option<PathBuf>,
encryption_key: Option<Vec<u8>>,
fallback_to_sync: Option<bool>,
}
impl WriteBehindConfigBuilder {
pub fn batch_size(mut self, size: u32) -> Self {
self.batch_size = Some(size);
self
}
pub fn flush_interval(mut self, interval: Duration) -> Self {
self.flush_interval = Some(interval);
self
}
pub fn wal_path(mut self, path: PathBuf) -> Self {
self.wal_path = Some(path);
self
}
pub fn encryption_key(mut self, key: Vec<u8>) -> Self {
self.encryption_key = Some(key);
self
}
pub fn fallback_to_sync(mut self, fallback: bool) -> Self {
self.fallback_to_sync = Some(fallback);
self
}
pub fn build(self) -> WriteBehindConfig {
WriteBehindConfig {
batch_size: self.batch_size.unwrap_or(100),
flush_interval: self
.flush_interval
.unwrap_or_else(|| Duration::from_millis(100)),
wal_path: self
.wal_path
.unwrap_or_else(|| PathBuf::from("wal/sz-orm-wal.log")),
encryption_key: self.encryption_key.unwrap_or_default(),
fallback_to_sync: self.fallback_to_sync.unwrap_or(true),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WriteOpType {
Insert,
Update,
Delete,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct WriteOp {
pub op_type: WriteOpType,
pub table: String,
pub pk: Value,
pub data: Vec<(String, Value)>,
pub timestamp: i64,
pub sequence: u64,
}
impl WriteOp {
pub fn new(op_type: WriteOpType, table: impl Into<String>, pk: Value) -> Self {
Self {
op_type,
table: table.into(),
pk,
data: Vec::new(),
timestamp: chrono::Utc::now().timestamp(),
sequence: 0,
}
}
pub fn with_data(mut self, data: Vec<(String, Value)>) -> Self {
self.data = data;
self
}
}
pub struct WriteBehindQueue {
wal: parking_lot::Mutex<WalFile>,
pending: crossbeam_queue::ArrayQueue<WriteOp>,
sequence: AtomicU64,
config: WriteBehindConfig,
}
impl WriteBehindQueue {
pub fn new(config: WriteBehindConfig) -> std::io::Result<Self> {
let wal = WalFile::open(&config.wal_path, &config.encryption_key)?;
let capacity = (config.batch_size * 10) as usize;
Ok(Self {
wal: parking_lot::Mutex::new(wal),
pending: crossbeam_queue::ArrayQueue::new(capacity.max(1024)),
sequence: AtomicU64::new(0),
config,
})
}
pub fn enqueue(&self, mut op: WriteOp) -> std::io::Result<()> {
op.sequence = self.sequence.fetch_add(1, Ordering::SeqCst);
self.wal.lock().append(&op)?;
let _ = self.pending.push(op);
Ok(())
}
pub fn drain_batch(&self) -> Vec<WriteOp> {
let batch_size = self.config.batch_size as usize;
let mut batch = Vec::with_capacity(batch_size);
for _ in 0..batch_size {
match self.pending.pop() {
Some(op) => batch.push(op),
None => break,
}
}
batch.sort_by_key(|op| op.sequence);
batch
}
pub fn truncate_wal(&self) -> std::io::Result<()> {
self.wal.lock().truncate()
}
pub fn replay(&self) -> std::io::Result<Vec<WriteOp>> {
self.wal.lock().read_all()
}
pub fn config(&self) -> &WriteBehindConfig {
&self.config
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
struct WalFile {
path: PathBuf,
encryption_key: Vec<u8>,
}
impl WalFile {
fn open(path: &std::path::Path, encryption_key: &[u8]) -> std::io::Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(Self {
path: path.to_path_buf(),
encryption_key: encryption_key.to_vec(),
})
}
fn append(&mut self, op: &WriteOp) -> std::io::Result<()> {
let json = serde_json::to_string(op)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let payload = json.as_bytes();
let encrypted = if self.encryption_key.is_empty() {
payload.to_vec()
} else {
self.encrypt(payload)
};
let crc = crc64(&encrypted);
let mut record = Vec::with_capacity(4 + encrypted.len() + 8);
record.extend_from_slice(&(encrypted.len() as u32).to_le_bytes());
record.extend_from_slice(&encrypted);
record.extend_from_slice(&crc.to_le_bytes());
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
use std::io::Write;
file.write_all(&record)?;
file.flush()?;
Ok(())
}
fn read_all(&self) -> std::io::Result<Vec<WriteOp>> {
let data = match std::fs::read(&self.path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
};
let mut ops = Vec::new();
let mut pos = 0;
while pos + 4 <= data.len() {
let len = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
pos += 4;
if pos + len + 8 > data.len() {
break;
}
let encrypted = &data[pos..pos + len];
pos += len;
let expected_crc = u64::from_le_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
pos += 8;
if crc64(encrypted) != expected_crc {
continue;
}
let decrypted = if self.encryption_key.is_empty() {
encrypted.to_vec()
} else {
self.decrypt(encrypted)
};
if let Ok(op) = serde_json::from_slice::<WriteOp>(&decrypted) {
ops.push(op);
}
}
ops.sort_by_key(|op| op.sequence);
Ok(ops)
}
fn truncate(&mut self) -> std::io::Result<()> {
std::fs::write(&self.path, b"")?;
Ok(())
}
fn encrypt(&self, data: &[u8]) -> Vec<u8> {
let crypter = sz_orm_crypto::AesGcmCrypter::from_key_str(
std::str::from_utf8(&self.encryption_key).unwrap_or("default-key"),
);
crypter
.encrypt_with_aad(data, &[])
.unwrap_or_else(|_| data.to_vec())
}
fn decrypt(&self, data: &[u8]) -> Vec<u8> {
let crypter = sz_orm_crypto::AesGcmCrypter::from_key_str(
std::str::from_utf8(&self.encryption_key).unwrap_or("default-key"),
);
crypter
.decrypt_with_aad(data, &[])
.unwrap_or_else(|_| data.to_vec())
}
}
fn crc64(data: &[u8]) -> u64 {
let mut crc: u64 = 0;
for &byte in data {
crc ^= byte as u64;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xC96E_8607_EAFC_E6CD;
} else {
crc >>= 1;
}
}
}
crc
}
pub struct BloomFilterGuard {
filter: parking_lot::RwLock<bloomfilter::Bloom<String>>,
capacity: usize,
false_positive_rate: f64,
count: AtomicU64,
}
impl BloomFilterGuard {
pub fn new(capacity: usize, false_positive_rate: f64) -> Self {
let filter = bloomfilter::Bloom::new_for_fp_rate(capacity, false_positive_rate);
Self {
filter: parking_lot::RwLock::new(filter),
capacity,
false_positive_rate,
count: AtomicU64::new(0),
}
}
pub fn default_config() -> Self {
Self::new(100_000, 0.01)
}
pub fn add(&self, key: &str) {
self.filter.write().set(&key.to_string());
self.count.fetch_add(1, Ordering::Relaxed);
}
pub fn might_contain(&self, key: &str) -> bool {
self.filter.read().check(&key.to_string())
}
pub fn rebuild(&self, keys: impl Iterator<Item = String>) {
let mut filter =
bloomfilter::Bloom::new_for_fp_rate(self.capacity, self.false_positive_rate);
for key in keys {
filter.set(&key);
self.count.fetch_add(1, Ordering::Relaxed);
}
*self.filter.write() = filter;
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
}
pub struct CacheMutexGuard {
mutexes: parking_lot::Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
}
impl CacheMutexGuard {
pub fn new() -> Self {
Self {
mutexes: parking_lot::Mutex::new(HashMap::new()),
}
}
pub fn get_mutex(&self, key: &str) -> Arc<tokio::sync::Mutex<()>> {
let mut map = self.mutexes.lock();
map.entry(key.to_string())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
pub async fn with_guard<F, R>(&self, key: &str, f: F) -> R
where
F: std::future::Future<Output = R>,
{
let mutex = self.get_mutex(key);
let _guard = mutex.lock().await;
f.await
}
}
impl Default for CacheMutexGuard {
fn default() -> Self {
Self::new()
}
}
pub struct RandomTtlJitter;
impl RandomTtlJitter {
pub fn jitter(base_ttl: Duration, jitter_range: f64) -> Duration {
use rand::Rng;
let mut rng = rand::thread_rng();
let random: f64 = rng.gen_range(-1.0..=1.0);
let factor = 1.0 + jitter_range * random;
let jittered_ms = (base_ttl.as_millis() as f64 * factor) as u64;
Duration::from_millis(jittered_ms.max(1))
}
pub fn default_jitter(base_ttl: Duration) -> Duration {
Self::jitter(base_ttl, 0.2)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redis_pubsub_serialize_message_key() {
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
let json = RedisPubSubInvalidationBus::serialize_message(&msg, "instance-1");
assert!(json.contains("\"type\":\"key\""));
assert!(json.contains("\"key\":\"user:42\""));
assert!(json.contains("\"src\":\"instance-1\""));
assert!(json.len() <= 1024, "消息应 ≤1KB: {} bytes", json.len());
}
#[test]
fn test_redis_pubsub_serialize_message_table() {
let msg = InvalidationMessage::InvalidateTable("users".to_string());
let json = RedisPubSubInvalidationBus::serialize_message(&msg, "instance-1");
assert!(json.contains("\"type\":\"table\""));
assert!(json.contains("\"table\":\"users\""));
assert!(json.len() <= 1024);
}
#[test]
fn test_redis_pubsub_serialize_message_all() {
let msg = InvalidationMessage::InvalidateAll;
let json = RedisPubSubInvalidationBus::serialize_message(&msg, "instance-1");
assert!(json.contains("\"type\":\"all\""));
assert!(json.len() <= 1024);
}
#[test]
fn test_redis_pubsub_deserialize_skips_self() {
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
let json = RedisPubSubInvalidationBus::serialize_message(&msg, "instance-1");
let result = RedisPubSubInvalidationBus::deserialize_message(&json, "instance-1");
assert!(result.is_none(), "应跳过自回环");
}
#[test]
fn test_redis_pubsub_deserialize_other_instance() {
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
let json = RedisPubSubInvalidationBus::serialize_message(&msg, "instance-1");
let result = RedisPubSubInvalidationBus::deserialize_message(&json, "instance-2");
assert!(result.is_some(), "应接收其他实例消息");
}
#[test]
fn test_redis_pubsub_disconnected_publish() {
let bus = RedisPubSubInvalidationBus::disconnected("instance-1");
bus.publish(InvalidationMessage::InvalidateAll);
}
#[test]
fn test_redis_pubsub_subscribe_drain() {
let bus = RedisPubSubInvalidationBus::disconnected("instance-1");
bus.push_received(InvalidationMessage::InvalidateTable("users".to_string()));
bus.push_received(InvalidationMessage::InvalidateAll);
let messages: Vec<_> = bus.subscribe().collect();
assert_eq!(messages.len(), 2);
let messages2: Vec<_> = bus.subscribe().collect();
assert_eq!(messages2.len(), 0);
}
#[test]
fn test_gossip_publish_and_subscribe() {
let bus = GossipInvalidationBus::new(
vec![NodeAddr::new("127.0.0.1", 8080)],
b"secret-key".to_vec(),
"instance-1",
);
bus.publish(InvalidationMessage::InvalidateTable("users".to_string()));
bus.publish(InvalidationMessage::InvalidateAll);
let messages: Vec<_> = bus.subscribe().collect();
assert_eq!(messages.len(), 2);
}
#[test]
fn test_gossip_hmac_authentication() {
let bus = GossipInvalidationBus::new(
vec![NodeAddr::new("127.0.0.1", 8080)],
b"secret-key".to_vec(),
"instance-1",
);
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
let tag = bus.compute_hmac(&msg);
assert!(bus.verify_hmac(&msg, &tag));
assert!(!bus.verify_hmac(&msg, &[0u8; 32]));
}
#[test]
fn test_gossip_receive_dedup() {
let bus = GossipInvalidationBus::new(
vec![NodeAddr::new("127.0.0.1", 8080)],
b"secret-key".to_vec(),
"instance-1",
);
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
let tag = bus.compute_hmac(&msg);
assert!(bus.receive(msg.clone(), 1, &tag));
assert!(!bus.receive(msg, 1, &tag));
}
#[test]
fn test_gossip_receive_unauthenticated() {
let bus = GossipInvalidationBus::new(
vec![NodeAddr::new("127.0.0.1", 8080)],
b"secret-key".to_vec(),
"instance-1",
);
let msg = InvalidationMessage::InvalidateKey("user:42".to_string());
assert!(!bus.receive(msg, 1, &[0u8; 32]));
}
#[test]
fn test_write_behind_config_default() {
let config = WriteBehindConfig::default();
assert_eq!(config.batch_size, 100);
assert_eq!(config.flush_interval, Duration::from_millis(100));
assert!(config.fallback_to_sync);
}
#[test]
fn test_write_behind_config_builder() {
let config = WriteBehindConfig::builder()
.batch_size(50)
.flush_interval(Duration::from_millis(200))
.fallback_to_sync(false)
.build();
assert_eq!(config.batch_size, 50);
assert_eq!(config.flush_interval, Duration::from_millis(200));
assert!(!config.fallback_to_sync);
}
#[test]
fn test_write_op_new() {
let op = WriteOp::new(WriteOpType::Insert, "users", Value::I64(42));
assert_eq!(op.op_type, WriteOpType::Insert);
assert_eq!(op.table, "users");
assert_eq!(op.pk, Value::I64(42));
assert_eq!(op.sequence, 0);
}
#[test]
fn test_write_behind_queue_enqueue_and_drain() {
let temp_dir = std::env::temp_dir().join("sz-orm-test-wal");
let _ = std::fs::remove_dir_all(&temp_dir);
let config = WriteBehindConfig::builder()
.batch_size(10)
.wal_path(temp_dir.join("test.log"))
.build();
let queue = WriteBehindQueue::new(config).unwrap();
for i in 0..3 {
let op = WriteOp::new(WriteOpType::Update, "users", Value::I64(i));
queue.enqueue(op).unwrap();
}
assert_eq!(queue.pending_count(), 3);
let batch = queue.drain_batch();
assert_eq!(batch.len(), 3);
assert!(batch.windows(2).all(|w| w[0].sequence <= w[1].sequence));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_write_behind_queue_replay() {
let temp_dir = std::env::temp_dir().join("sz-orm-test-wal-replay");
let _ = std::fs::remove_dir_all(&temp_dir);
let config = WriteBehindConfig::builder()
.batch_size(10)
.wal_path(temp_dir.join("test.log"))
.build();
let queue = WriteBehindQueue::new(config).unwrap();
for i in 0..5 {
let op = WriteOp::new(WriteOpType::Insert, "orders", Value::I64(i)).with_data(vec![(
"status".to_string(),
Value::String("pending".to_string()),
)]);
queue.enqueue(op).unwrap();
}
let replayed = queue.replay().unwrap();
assert_eq!(replayed.len(), 5);
assert!(replayed.windows(2).all(|w| w[0].sequence <= w[1].sequence));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_bloom_filter_basic() {
let guard = BloomFilterGuard::new(1000, 0.01);
guard.add("user:1");
guard.add("user:2");
assert!(guard.might_contain("user:1"));
assert!(guard.might_contain("user:2"));
assert_eq!(guard.count(), 2);
}
#[test]
fn test_bloom_filter_false_positive_rate() {
let guard = BloomFilterGuard::new(10_000, 0.01);
for i in 0..1000 {
guard.add(&format!("user:{}", i));
}
let mut false_positives = 0;
for i in 1000..2000 {
if guard.might_contain(&format!("user:{}", i)) {
false_positives += 1;
}
}
let fp_rate = false_positives as f64 / 1000.0;
assert!(fp_rate < 0.05, "假阳性率应 < 5%(实际 {})", fp_rate);
}
#[tokio::test]
async fn test_cache_mutex_guard() {
let guard = CacheMutexGuard::new();
guard
.with_guard("user:42", async {
})
.await;
guard
.with_guard("user:43", async {
})
.await;
}
#[test]
fn test_random_ttl_jitter_range() {
let base = Duration::from_millis(1000);
for _ in 0..100 {
let jittered = RandomTtlJitter::default_jitter(base);
let ms = jittered.as_millis();
assert!(
(800..=1200).contains(&ms),
"TTL 抖动应在 ±20% 范围内: {}ms",
ms
);
}
}
#[test]
fn test_consistency_level_default() {
assert_eq!(ConsistencyLevel::default(), ConsistencyLevel::Eventual);
}
#[test]
fn test_crc64() {
let crc1 = crc64(b"hello");
let crc2 = crc64(b"hello");
assert_eq!(crc1, crc2);
let crc3 = crc64(b"world");
assert_ne!(crc1, crc3);
}
}