gunnar-core 3.0.0

gunnar's repository model, identity table, ACL and path sandbox. gunnar-ssh, gunnar-grpc and gunnar-cli all sit on it.
Documentation
//! Eight parallel writers creating repositories in one catalog: none may be
//! refused. Measured on the rig 2026-08-29: 11 of 2400 push-creates
//! were refused with "the control plane is unavailable" after the
//! compare-and-swap retry gave up.

use std::sync::Arc;

use gunnar_core::{NewRepo, RepoCatalog, RepoRef};

#[test]
fn eight_parallel_writers_three_hundred_creates_each_none_refused() {
    let dir = tempfile::tempdir().expect("tempdir");
    let store = gunnar_core::open_control_store(dir.path().join("control.redb")).expect("open");
    let catalog = RepoCatalog::new(Arc::clone(&store)).expect("catalog");
    catalog.create_namespace("rig").expect("namespace");
    let started = std::time::Instant::now();
    let handles: Vec<_> = (0..8)
        .map(|w| {
            let catalog = catalog.clone();
            std::thread::spawn(move || {
                let mut errors = Vec::new();
                for i in 0..300 {
                    let path = RepoRef::new("rig", &format!("w{w}-{i}")).expect("legal");
                    if let Err(e) = catalog.create(NewRepo::new(path)) {
                        errors.push(format!("w{w}-{i}: {e}"));
                    }
                }
                errors
            })
        })
        .collect();
    let errors: Vec<String> = handles
        .into_iter()
        .flat_map(|h| h.join().expect("thread"))
        .collect();
    eprintln!(
        "2400 creates by 8 writers in {} ms; {} refused",
        started.elapsed().as_millis(),
        errors.len()
    );
    for e in errors.iter().take(20) {
        eprintln!("  {e}");
    }
    assert!(
        errors.is_empty(),
        "{} of 2400 creates refused",
        errors.len()
    );
    assert_eq!(catalog.list(Some("rig")).expect("list").len(), 2400);
}