use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::transport::InMemoryNetwork;
use crate::{FileSnapshot, ReplicatedMap};
use super::ephemeral_config;
async fn wait_until<F: FnMut() -> bool>(mut f: F) -> bool {
for _ in 0..300 {
tokio::time::sleep(Duration::from_millis(10)).await;
if f() {
return true;
}
}
false
}
#[tokio::test]
async fn local_addr_matches_the_configured_bind_address() {
let config = ephemeral_config();
let expected = SocketAddr::new(config.listen_addr, config.port);
let store = ReplicatedMap::<i32, i32>::new(config)
.await
.expect("bind failed");
assert_eq!(
store
.local_addr()
.expect("transport must report its address"),
expected
);
}
#[tokio::test(flavor = "multi_thread")]
async fn sync_state_advances_as_the_engine_runs() {
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed");
let initial = store.sync_state();
assert_eq!(initial.rounds, 0);
assert!(initial.last_round_at.is_none());
let task = tokio::spawn(store.clone().run(CancellationToken::new()));
assert!(
wait_until(|| store.sync_state().rounds > 0 && store.sync_state().last_round_at.is_some())
.await,
"sync_state never observed a completed reconciliation round"
);
task.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn peers_and_members_reflect_a_converged_pair() {
let net = InMemoryNetwork::new();
let port = crate::replica::tests::next_ephemeral_test_port();
let a_ip: IpAddr = "127.0.10.1".parse().unwrap();
let b_ip: IpAddr = "127.0.10.2".parse().unwrap();
let cfg = |ip: IpAddr| {
ephemeral_config()
.with_listen_addr(ip)
.with_port(port)
.with_reconcile_interval(Duration::from_millis(20))
};
let a = ReplicatedMap::<i32, i32>::new_with_transport(
cfg(a_ip),
Arc::new(net.bind(SocketAddr::new(a_ip, port))),
);
let b = ReplicatedMap::<i32, i32>::new_with_transport(
cfg(b_ip),
Arc::new(net.bind(SocketAddr::new(b_ip, port))),
);
let a = a.with_seed(b_ip);
let b = b.with_seed(a_ip);
assert!(a.members().is_empty());
let ta = tokio::spawn(a.clone().run(CancellationToken::new()));
let tb = tokio::spawn(b.clone().run(CancellationToken::new()));
assert!(
wait_until(|| a.peers().contains(&b_ip) && a.members().contains(&b_ip)).await,
"A never registered B as a peer/member"
);
assert!(
wait_until(|| b.peers().contains(&a_ip) && b.members().contains(&a_ip)).await,
"B never registered A as a peer/member"
);
ta.abort();
tb.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn run_observes_cancellation_and_flushes_a_durable_final_snapshot() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("shutdown.bin");
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed")
.with_persistence(Arc::new(FileSnapshot::new(&path)));
store.insert(7, 70);
let shutdown = CancellationToken::new();
let handle = tokio::spawn(store.clone().run(shutdown.clone()));
assert!(
wait_until(|| store.sync_state().rounds > 0).await,
"run() never completed a round before shutdown"
);
shutdown.cancel();
let outcome = tokio::time::timeout(Duration::from_secs(5), handle)
.await
.expect("run() did not return after the shutdown signal fired")
.expect("run() task panicked");
outcome
.final_snapshot
.expect("the final snapshot on shutdown should succeed");
let restarted = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed")
.with_persistence(Arc::new(FileSnapshot::new(&path)));
assert_eq!(
restarted.get(&7).as_deref(),
Some(&70),
"run()'s final snapshot on shutdown was not actually durable"
);
}
#[tokio::test]
async fn snapshot_now_flushes_without_the_periodic_task() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("forced.bin");
let store = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed")
.with_persistence(Arc::new(FileSnapshot::new(&path)));
store.just_insert(3, 30);
assert!(store.sync_state().last_snapshot_at.is_none());
store
.snapshot_now()
.expect("forced snapshot should succeed");
assert!(
store.sync_state().last_snapshot_at.is_some(),
"snapshot_now must record last_snapshot_at on success"
);
let restarted = ReplicatedMap::<i32, i32>::new(ephemeral_config())
.await
.expect("bind failed")
.with_persistence(Arc::new(FileSnapshot::new(&path)));
assert_eq!(
restarted.get(&3).as_deref(),
Some(&30),
"snapshot_now's write was not actually durable"
);
}