use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use gix_object::Kind as ObjKind;
use gunnar_core::{HashKind, NewRepo, Principal, PrincipalKind, RepoRef};
use gunnar_grpc::{EventBus, ServerConfig, ServiceContext};
use gunnar_proto::bootstrap_client::BootstrapClient;
use gunnar_proto::mirror_client::MirrorClient;
use gunnar_server::stores::ServerStores;
use gunnar_server::twin::{self, Follower, Progress};
use gunnar_server::CoreControlPlane;
use gunnar_store::conformance::pack::PackBuilder;
const BIG: usize = 24 << 20;
const BUDGET: u64 = 7 << 20;
const PARTIAL_DIR: &str = "txlog-restore-partial";
fn free_addr() -> SocketAddr {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
l.local_addr().expect("addr")
}
struct Served {
_rt: tokio::runtime::Runtime,
stop: Option<tokio::sync::oneshot::Sender<()>>,
addr: SocketAddr,
}
fn serve(stores: &Arc<ServerStores>) -> Served {
let addr = free_addr();
let plane: Arc<dyn gunnar_grpc::ControlPlane> =
Arc::new(CoreControlPlane::new(Arc::clone(stores)));
let ctx = ServiceContext::new(
plane,
Arc::new(EventBus::new(16)),
ServerConfig::default().insecure_allow_all_for_local_development(),
);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("rt");
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
rt.spawn(async move {
let _ = gunnar_grpc::router(ctx, || 0)
.serve_with_shutdown(addr, async {
let _ = rx.await;
})
.await;
});
wait("the box listens", Duration::from_secs(10), || {
std::net::TcpStream::connect(addr).ok()
});
Served {
_rt: rt,
stop: Some(tx),
addr,
}
}
impl Served {
fn kill(mut self) {
if let Some(tx) = self.stop.take() {
let _ = tx.send(());
}
let addr = self.addr;
wait("the box stops listening", Duration::from_secs(10), || {
std::net::TcpStream::connect(addr).is_err().then_some(())
});
}
}
fn wait<T>(what: &str, within: Duration, mut probe: impl FnMut() -> Option<T>) -> T {
let t0 = Instant::now();
loop {
if let Some(v) = probe() {
return v;
}
assert!(t0.elapsed() < within, "{what}: not within {within:?}");
std::thread::sleep(Duration::from_millis(25));
}
}
fn noise(seed: u64, n: usize) -> Vec<u8> {
let mut x = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(0..n)
.map(|_| {
x = x
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(x >> 33) as u8
})
.collect()
}
fn one_blob_pack(hash: gix_hash::Kind, content: &[u8]) -> (Vec<u8>, Vec<u8>) {
let id = gix_object::compute_hash(hash, ObjKind::Blob, content).expect("hash");
let mut pb = PackBuilder::new(hash);
pb.object(ObjKind::Blob, content.to_vec());
let fixture = pb.build().expect("pack");
(fixture.bytes, id.as_bytes().to_vec())
}
struct Primary {
stores: Arc<ServerStores>,
oids: Vec<Vec<u8>>,
}
fn primary(dir: &Path) -> Primary {
let stores = Arc::new(ServerStores::open(dir).expect("primary stores"));
stores.catalog().create_namespace("ns").expect("namespace");
let repo = stores
.create_repo(
NewRepo::new(RepoRef::new("ns", "app").unwrap()).hash_kind(HashKind::Sha256),
false,
)
.expect("repo");
let store = stores.repo_store(&repo).expect("store");
let git = store.serve();
let hash = store.hash_kind();
let (p1, o1) = one_blob_pack(hash, b"before the big one\n");
let (p2, o2) = one_blob_pack(hash, &noise(7, BIG));
let (p3, o3) = one_blob_pack(hash, b"after the big one\n");
git.put_pack(&p1).expect("pack 1");
git.update_ref("refs/heads/main", None, Some(&o1))
.expect("main");
git.put_pack(&p2).expect("the big pack");
git.update_ref("refs/heads/big", None, Some(&o2))
.expect("big");
git.put_pack(&p3).expect("pack 3");
git.update_ref("refs/heads/after", None, Some(&o3))
.expect("after");
stores
.auth()
.add_principal(Principal::new("alice", "Alice", PrincipalKind::User).unwrap())
.expect("principal");
assert!(
p2.len() >= BIG,
"the big pack is as big as its noise: {}",
p2.len()
);
Primary {
stores,
oids: vec![o1, o2, o3],
}
}
fn twin_of(primary: &Primary, dir: &Path) -> Arc<ServerStores> {
let twin = Arc::new(ServerStores::open(dir).expect("twin stores"));
twin::become_twin(&twin, "primary.test:50051").expect("a fresh box becomes a twin");
drop(twin);
let twin = Arc::new(ServerStores::open(dir).expect("the twin, reopened as one"));
let id = twin::log_id(primary.stores.root()).expect("a primary mints its log's identity");
twin::pin_log_id(twin.root(), &id).expect("the twin pins its primary's log identity");
let f = Follower::new(
Arc::clone(&twin),
Box::new(Arc::clone(primary.stores.txlog())),
);
let head = primary.stores.txlog().head_seq();
for _ in 0..64 {
match f.step().expect("the twin follows") {
Progress::CaughtUp { .. } => break,
Progress::Applied { .. } => continue,
other => panic!("the twin cannot follow: {other:?}"),
}
}
assert_eq!(
twin.txlog().head_seq(),
head,
"the twin is level with the primary"
);
twin
}
fn big_seq(log: &gunnar_core::txlog::Log) -> u64 {
(log.first_seq()..=log.head_seq())
.find(|s| log.read(*s).map(|e| e.blob.len() >= BIG).unwrap_or(false))
.expect("the big frame")
}
#[derive(Clone, Copy, Debug)]
enum Cut {
Close,
Abort,
OldTwinClose,
}
#[derive(Debug)]
enum Call {
Done(u64),
Stopped(String),
}
struct Lane {
rt: tokio::runtime::Runtime,
twin: SocketAddr,
budget: u64,
}
impl Lane {
fn new(twin: SocketAddr, budget: u64) -> Lane {
Lane {
rt: tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap(),
twin,
budget,
}
}
fn status(&self, main: SocketAddr) -> gunnar_proto::BootstrapStatusResponse {
self.rt.block_on(async {
let mut c = BootstrapClient::connect(format!("http://{main}"))
.await
.expect("connect");
c.status(gunnar_proto::BootstrapStatusRequest {})
.await
.expect("Bootstrap.Status")
.into_inner()
})
}
fn call(&self, main: SocketAddr, cut: Cut) -> Call {
let budget = self.budget;
let twin = self.twin;
let st = self.status(main);
self.rt.block_on(async move {
let mut mirror = MirrorClient::connect(format!("http://{twin}"))
.await
.expect("the twin");
let head = mirror
.log_head(gunnar_proto::LogHeadRequest::default())
.await
.expect("LogHead")
.into_inner();
let from = if st.restoring {
head.first_seq.max(st.log_head_seq + 1)
} else {
head.first_seq.max(1)
};
let old_twin = matches!(cut, Cut::OldTwinClose);
let offset = if st.restoring && st.restore_partial_seq == from && !old_twin {
st.restore_partial_bytes
} else {
0
};
let (tx, rx) = tokio::sync::mpsc::channel::<gunnar_proto::LogChunk>(4);
let restore = tokio::spawn(async move {
let mut c = BootstrapClient::connect(format!("http://{main}"))
.await
.expect("connect main");
c.restore(tokio_stream::wrappers::ReceiverStream::new(rx))
.await
.map(|r| r.into_inner())
});
let (mut cursor, mut sent, mut highest) = (from, 0u64, 0u64);
while cursor <= head.head_seq {
let req = gunnar_proto::LogFetchRequest {
from: cursor,
to: head.head_seq,
max_bytes: 0,
from_offset: if cursor == from { offset } else { 0 },
};
let mut s = mirror.log_fetch(req).await.expect("LogFetch").into_inner();
while let Some(mut chunk) = s.message().await.expect("the twin's stream") {
chunk.restore_head = head.head_seq;
chunk.log_id.clone_from(&head.log_id);
if old_twin {
chunk.offset = None;
chunk.entry_crc = None;
}
let (seq, last) = (chunk.seq, chunk.last);
sent += chunk.blob.len() as u64;
highest = highest.max(seq);
if tx.send(chunk).await.is_err() {
break;
}
if sent >= budget && !(last && seq >= head.head_seq) {
return match cut {
Cut::Close | Cut::OldTwinClose => {
drop(tx);
match restore.await.expect("the restore task") {
Ok(r) => {
Call::Stopped(format!("answered OK at head {}", r.head_seq))
}
Err(status) => Call::Stopped(status.message().to_owned()),
}
}
Cut::Abort => {
restore.abort();
drop(tx);
Call::Stopped("cancelled".into())
}
};
}
}
assert!(highest >= cursor, "the twin answered nothing at {cursor}");
cursor = highest + 1;
}
drop(tx);
match restore.await.expect("the restore task") {
Ok(r) => Call::Done(r.head_seq),
Err(status) => Call::Stopped(status.message().to_owned()),
}
})
}
}
fn assert_frames_equal(twin: &ServerStores, restored: &ServerStores) {
let (t, r) = (twin.txlog(), restored.txlog());
assert_eq!(
r.head_seq(),
t.head_seq(),
"the restored main is at the twin's head"
);
for seq in t.first_seq()..=t.head_seq() {
assert_eq!(
r.read(seq).unwrap(),
t.read(seq).unwrap(),
"seq {seq} differs between the twin's log and the restored main's"
);
}
}
fn assert_same_objects(primary: &Primary, restored: &ServerStores) {
let at = RepoRef::new("ns", "app").unwrap();
let p = primary
.stores
.repo_store(&primary.stores.catalog().get(&at).unwrap().unwrap())
.unwrap();
let r = restored
.repo_store(
&restored
.catalog()
.get(&at)
.unwrap()
.expect("the repository came back"),
)
.unwrap();
for oid in &primary.oids {
assert!(
r.serve().has(oid).unwrap(),
"object {} missing on the restored main",
hex::encode(oid)
);
assert_eq!(
p.serve().read(oid).unwrap(),
r.serve().read(oid).unwrap(),
"object {} differs",
hex::encode(oid)
);
}
let mut refs_p: Vec<_> = p
.serve()
.refs()
.unwrap()
.into_iter()
.map(|r| (r.name, r.oid))
.collect();
let mut refs_r: Vec<_> = r
.serve()
.refs()
.unwrap()
.into_iter()
.map(|r| (r.name, r.oid))
.collect();
refs_p.sort();
refs_r.sort();
assert_eq!(refs_p, refs_r, "refs");
}
#[test]
fn an_entry_larger_than_one_call_is_cut_three_times_and_resumed_inside_it() {
let (pd, td, md) = (
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
);
let primary = primary(pd.path());
let twin = twin_of(&primary, td.path());
let big = big_seq(twin.txlog());
let twin_served = serve(&twin);
let main = Arc::new(ServerStores::open(md.path()).expect("a fresh main"));
let main_served = serve(&main);
let lane = Lane::new(twin_served.addr, BUDGET);
let mut inside = Vec::new();
let mut done = None;
for call in 0..12 {
let said = lane.call(main_served.addr, Cut::Close);
let st = lane.status(main_served.addr);
println!(
"call {call}: {said:?} — head {} restoring {} partial {}:{}",
st.log_head_seq, st.restoring, st.restore_partial_seq, st.restore_partial_bytes
);
if st.log_head_seq >= big {
assert!(
!md.path().join(PARTIAL_DIR).exists(),
"call {call}: the big entry is applied and its staging is still on the volume"
);
} else if st.log_head_seq + 1 == big {
inside.push(st.restore_partial_bytes);
}
if let Call::Done(head) = said {
done = Some(head);
break;
}
}
assert_eq!(
done,
Some(twin.txlog().head_seq()),
"the restore never finished; the box held, call after call, {inside:?} bytes of seq {big}"
);
assert!(
inside.len() >= 3,
"the big entry was cut inside {} time(s), not three: {inside:?}",
inside.len()
);
assert!(
inside.windows(2).all(|w| w[1] > w[0]),
"every call went further INTO the entry: {inside:?}"
);
assert!(
!md.path().join(PARTIAL_DIR).exists(),
"nothing staged is left on the volume"
);
assert_frames_equal(&twin, &main);
main_served.kill();
drop(main);
let restored = ServerStores::open(md.path()).expect("the restored main, restarted");
assert_same_objects(&primary, &restored);
twin_served.kill();
}
#[test]
fn a_main_restarted_inside_the_entry_resumes_from_what_it_made_durable() {
let (pd, td, md) = (
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
);
let primary = primary(pd.path());
let twin = twin_of(&primary, td.path());
let big = big_seq(twin.txlog());
let twin_served = serve(&twin);
let lane = Lane::new(twin_served.addr, BUDGET);
let mut main = Arc::new(ServerStores::open(md.path()).expect("a fresh main"));
let mut main_served = serve(&main);
let mut held = Vec::new();
for _ in 0..2 {
lane.call(main_served.addr, Cut::Abort);
let st = wait(
"the cancelled stream's handler let go",
Duration::from_secs(20),
|| {
let st = lane.status(main_served.addr);
(st.restore_partial_seq == 0
|| st.restore_partial_bytes > held.last().copied().unwrap_or(0))
.then_some(st)
},
);
held.push(st.restore_partial_bytes);
}
let before = lane.status(main_served.addr);
main_served.kill();
drop(main);
let data = md.path().join(PARTIAL_DIR).join("entry.blob");
if data.exists() {
use std::io::Write as _;
std::fs::OpenOptions::new()
.append(true)
.open(&data)
.unwrap()
.write_all(&noise(99, 12_345))
.unwrap();
}
main = Arc::new(ServerStores::open(md.path()).expect("the main, back mid-restore"));
main_served = serve(&main);
let after = lane.status(main_served.addr);
assert!(after.restoring, "the main comes back still restoring");
assert_eq!(
(after.restore_partial_seq, after.restore_partial_bytes),
(before.restore_partial_seq, before.restore_partial_bytes),
"it holds what it made durable — no more, no less"
);
let mut done = None;
for _ in 0..12 {
if let Call::Done(head) = lane.call(main_served.addr, Cut::Close) {
done = Some(head);
break;
}
}
assert_eq!(
done,
Some(twin.txlog().head_seq()),
"the restore never finished after the restart (held {held:?} of seq {big} before it)"
);
assert!(
!md.path().join(PARTIAL_DIR).exists(),
"nothing staged is left on the volume"
);
assert_frames_equal(&twin, &main);
main_served.kill();
drop(main);
let restored = ServerStores::open(md.path()).expect("the restored main, restarted");
assert_same_objects(&primary, &restored);
twin_served.kill();
}
#[test]
fn an_old_twin_that_cannot_resume_inside_an_entry_is_named_not_looped_on() {
let (pd, td, md) = (
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
);
let primary = primary(pd.path());
let twin = twin_of(&primary, td.path());
let twin_served = serve(&twin);
let main = Arc::new(ServerStores::open(md.path()).expect("a fresh main"));
let main_served = serve(&main);
let lane = Lane::new(twin_served.addr, BUDGET);
let first = lane.call(main_served.addr, Cut::OldTwinClose);
let mut held = lane.status(main_served.addr).restore_partial_bytes;
assert!(
held > 0,
"the first call staged part of the big entry: {first:?}"
);
let mut named = None;
for _ in 0..3 {
let Call::Stopped(said) = lane.call(main_served.addr, Cut::OldTwinClose) else {
panic!("an old twin cannot finish the big entry")
};
let now = lane.status(main_served.addr).restore_partial_bytes;
assert!(now >= held, "nothing held is lost: {now} < {held}");
held = now;
if said.contains("restore-twin-too-old") {
named = Some(said);
break;
}
}
let said = named.expect("the box never named the old twin");
assert!(
said.contains("upgrade the twin"),
"the box says what to do: {said}"
);
let mut done = None;
for _ in 0..12 {
if let Call::Done(head) = lane.call(main_served.addr, Cut::Close) {
done = Some(head);
break;
}
}
assert_eq!(done, Some(twin.txlog().head_seq()));
assert_frames_equal(&twin, &main);
main_served.kill();
twin_served.kill();
}
#[test]
fn a_staged_entry_whose_bytes_rotted_between_calls_is_refused_by_its_crc_and_taken_again() {
let (pd, td, md) = (
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
tempfile::tempdir().unwrap(),
);
let primary = primary(pd.path());
let twin = twin_of(&primary, td.path());
let big = big_seq(twin.txlog());
let twin_served = serve(&twin);
let main = Arc::new(ServerStores::open(md.path()).expect("a fresh main"));
let main_served = serve(&main);
let lane = Lane::new(twin_served.addr, BUDGET);
lane.call(main_served.addr, Cut::Close);
let st = lane.status(main_served.addr);
assert_eq!(
st.restore_partial_seq, big,
"the first call staged part of the big entry"
);
{
use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
let mut f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(md.path().join(PARTIAL_DIR).join("entry.blob"))
.unwrap();
f.seek(SeekFrom::Start(1000)).unwrap();
let mut b = [0u8; 1];
f.read_exact(&mut b).unwrap();
f.seek(SeekFrom::Start(1000)).unwrap();
f.write_all(&[b[0] ^ 0x5A]).unwrap();
}
let mut refused = None;
let mut done = None;
for _ in 0..16 {
match lane.call(main_served.addr, Cut::Close) {
Call::Done(head) => {
done = Some(head);
break;
}
Call::Stopped(said) if said.contains("crc") => refused = Some(said),
Call::Stopped(_) => {}
}
}
let refused = refused.expect("the rotted entry was refused by its crc");
assert!(refused.contains("starts again from byte 0"), "{refused}");
assert_eq!(
done,
Some(twin.txlog().head_seq()),
"taken again from byte 0, the entry finished"
);
assert_frames_equal(&twin, &main);
main_served.kill();
twin_served.kill();
}