use std::time::Duration;
use crate::clock::NodeId;
use crate::{replicated_map::Config, ReplicatedMap};
use super::ephemeral_config;
#[tokio::test]
async fn tombstones_expiration() {
let config = Config::default()
.with_port(8090)
.with_listen_addr("127.0.0.45".parse().unwrap())
.with_net("127.0.0.45/32".parse().unwrap())
.with_insecure_no_key();
let store = ReplicatedMap::<i32, i32>::new(config)
.await
.expect("bind failed")
.with_tombstone_timeout(Duration::from_millis(1));
store.remove(&0);
tokio::time::sleep(Duration::from_millis(10)).await;
assert_eq!(store.tombstones.expired(), vec![0]);
assert_eq!(store.tombstones.remove(&0), Some(0));
assert_eq!(store.tombstones.remove(&0), None);
}
mod tombstone_expiry_bound {
use super::*;
use crate::clock::{Hlc, LogicalCounter, PhysicalTime};
use crate::entry::Entry;
use crate::replicated_map::write::TOMBSTONE_STAMP_DRIFT_BUDGET;
use chrono::Utc;
async fn plant(physical_ms: u64) -> (ReplicatedMap<i32, i32>, chrono::DateTime<Utc>) {
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed");
let stamp = crate::clock::Timestamp::new(
Hlc::new(
PhysicalTime::from_millis(physical_ms),
LogicalCounter::new(7),
),
NodeId::new(0xBEEF),
);
store
.engine
.just_insert_bulk(&[(1, Entry::tombstone(stamp))]);
assert_eq!(
store.engine.map.read().get(&1).unwrap().stamp,
stamp,
"the stored stamp must be exactly as received — only the expiry instant is bounded"
);
let when = store
.tombstones
.instant_of(&1)
.expect("tombstone was not tracked");
(store, when)
}
fn now_ms() -> i64 {
Utc::now().timestamp_millis()
}
fn cap_ms() -> i64 {
now_ms() + TOMBSTONE_STAMP_DRIFT_BUDGET.millis() as i64
}
#[tokio::test]
async fn a_normal_stamp_is_used_verbatim() {
let physical = now_ms() as u64 - 1_000;
let (_store, when) = plant(physical).await;
assert_eq!(when.timestamp_millis(), physical as i64);
}
#[tokio::test]
async fn a_far_future_representable_stamp_is_capped() {
let physical = now_ms() as u64 + 10_000 * 365 * 24 * 3_600_000;
let (_store, when) = plant(physical).await;
assert!(
when.timestamp_millis() <= cap_ms(),
"instant {when} escaped the cap"
);
assert!(
when.timestamp_millis() >= now_ms() - 1_000,
"a capped instant must stay in the near future, not fall into the past"
);
}
#[tokio::test]
async fn a_stamp_above_i64_max_is_bounded_not_wrapped() {
let (_store, when) = plant(u64::MAX).await;
assert!(
when.timestamp_millis() > 0,
"stamp wrapped to a pre-epoch instant: {when}"
);
assert!(
when.timestamp_millis() <= cap_ms(),
"instant {when} escaped the cap"
);
}
#[tokio::test]
async fn a_capped_tombstone_has_a_finite_expiry_horizon() {
let timeout = Duration::from_secs(60);
let physical = now_ms() as u64 + 10_000 * 365 * 24 * 3_600_000;
let (_store, when) = plant(physical).await;
let deadline = when.timestamp_millis() + timeout.as_millis() as i64;
assert!(
deadline <= cap_ms() + timeout.as_millis() as i64,
"expiry deadline {deadline} is beyond now + budget + timeout"
);
let unbounded = physical as i64 + timeout.as_millis() as i64;
assert!(
unbounded > deadline + 100 * 365 * 24 * 3_600_000,
"the unbounded deadline should be astronomically later than the bounded one"
);
}
}
#[tokio::test]
async fn just_insert_bulk_actually_inserts_every_pair() {
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.unwrap();
store.just_insert_bulk(&[(1, 10), (2, 20), (3, 30)]);
assert_eq!(store.get(&1).as_deref(), Some(&10));
assert_eq!(store.get(&2).as_deref(), Some(&20));
assert_eq!(store.get(&3).as_deref(), Some(&30));
}
#[tokio::test]
async fn just_remove_bulk_actually_removes_every_key() {
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.unwrap();
store.just_insert_bulk(&[(1, 10), (2, 20)]);
store.just_remove_bulk(&[1, 2]);
assert_eq!(store.get(&1).as_deref(), None);
assert_eq!(store.get(&2).as_deref(), None);
}
#[tokio::test]
async fn set_tombstone_timeout_actually_retunes_the_wheel() {
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.unwrap()
.with_tombstone_timeout(Duration::from_secs(3600)); store.remove(&0);
assert!(
store.tombstones.expired().is_empty(),
"must not be expired yet under the long initial timeout"
);
store.set_tombstone_timeout(Duration::from_millis(1));
tokio::time::sleep(Duration::from_millis(10)).await;
assert_eq!(
store.tombstones.expired(),
vec![0],
"retuning the timeout down must make the tombstone expire promptly"
);
}