gunnar-server 2.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **The twin's own growth holds its follower** (T21, owner ruling 2026-09-22: the twin
//! grows exactly like main and is never restarted). A held twin fetches, appends and
//! applies NOTHING, says `maintenance` on its status and on the report its primary
//! reads, still asks the primary for its head, and follows on from its own head the
//! moment it is released.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use super::super::*;
use super::{hold, is_held, release};
use crate::stores::ServerStores;

fn wait_for(what: &str, mut cond: impl FnMut() -> bool) {
    let deadline = Instant::now() + Duration::from_secs(20);
    while !cond() {
        assert!(Instant::now() < deadline, "timed out waiting for: {what}");
        std::thread::sleep(Duration::from_millis(5));
    }
}

#[test]
fn a_held_twin_applies_nothing_says_maintenance_and_follows_again_when_released() {
    let pd = tempfile::tempdir().unwrap();
    let td = tempfile::tempdir().unwrap();
    let primary = Arc::new(ServerStores::open(pd.path()).unwrap());
    primary.catalog().create_namespace("before").unwrap();
    TwinConfig { primary: "primary.test:50051".into(), joined_at_unix_ms: 0 }.save(td.path()).unwrap();
    let twin = Arc::new(ServerStores::open(td.path()).unwrap());

    let stop = Arc::new(AtomicBool::new(false));
    let runner = {
        let (twin, log, stop) = (Arc::clone(&twin), Arc::clone(primary.txlog()), Arc::clone(&stop));
        std::thread::spawn(move || Follower::new(twin, Box::new(log)).run(Duration::from_millis(5), &stop))
    };
    wait_for("the twin level with its primary", || twin.txlog().head_seq() == primary.txlog().head_seq());

    // ── the grow machine holds it (BoxWorld::pause_admission on a twin) ──
    hold(twin.root());
    assert!(is_held(twin.root()));
    let held_at = twin.txlog().head_seq();
    let s = status(twin.root()).expect("a status");
    assert_eq!(s.state, State::Maintenance);
    assert_eq!(s.state.to_string(), "maintenance");
    let report = measure_report(&twin);
    assert_eq!(report.state, "maintenance", "the word the primary's heartbeat carries");
    assert_eq!((report.disk_total_bytes, report.disk_used_bytes), (0, 0), "an unmounted set has no size to report");

    // The primary goes on writing while the twin's set is unmounted.
    primary.catalog().create_namespace("during").unwrap();
    let primary_head = primary.txlog().head_seq();
    assert!(primary_head > held_at);
    wait_for("the held twin to hear the primary's new head", || status(twin.root()).is_some_and(|s| s.primary_head_seq == primary_head));
    std::thread::sleep(Duration::from_millis(60));
    assert_eq!(twin.txlog().head_seq(), held_at, "a held twin appended nothing");
    assert!(twin.catalog().namespace("during").unwrap().is_none(), "a held twin applied nothing");
    let s = status(twin.root()).unwrap();
    assert_eq!(s.state, State::Maintenance, "still maintenance, with the lag visible: {s:?}");
    assert!(!s.caught_up());

    // ── the set is back (BoxWorld::unpause_admission) ──
    release(twin.root());
    wait_for("the released twin level again", || twin.txlog().head_seq() == primary_head);
    wait_for("the status back to following", || status(twin.root()).is_some_and(|s| s.state == State::Following));
    assert!(twin.catalog().namespace("during").unwrap().is_some(), "what was written during the growth is on the twin now");
    assert!(status(twin.root()).unwrap().caught_up());

    stop.store(true, Ordering::SeqCst);
    runner.join().unwrap();
}

/// A twin whose follower is NOT running (never started, or between two supervisor
/// attempts) still says `maintenance` the moment it is held — the status is the
/// hold's, not the loop's — and the supervisor's `unreachable` does not overwrite it.
#[test]
fn a_held_twin_says_maintenance_even_without_a_running_follower() {
    let td = tempfile::tempdir().unwrap();
    TwinConfig { primary: "primary.test:50051".into(), joined_at_unix_ms: 0 }.save(td.path()).unwrap();
    let twin = Arc::new(ServerStores::open(td.path()).unwrap());
    hold(twin.root());
    stalled(&twin, 1, "the primary did not answer", Duration::from_secs(5));
    assert_eq!(status(twin.root()).unwrap().state, State::Maintenance);
    release(twin.root());
    assert!(!is_held(twin.root()));
}