crates/gunnar-loadbench/src/estate.rs
//! **The estate, as measured rather than as remembered.**
//!
//! The three VMs, and what of each one is actually reachable from outside. This
//! was surveyed before the bench was written, and the survey is recorded here
//! because two thirds of it is the interesting part:
//!
//! | machine | address | reachable from the internet |
//! |-----------|------------------|------------------------------------------------|
//! | front | 95.111.204.164 | 443 (Caddy → the console BFF), 2222 (gunnar SSH)|
//! | appliance | 81.27.106.241 | 2222 only — `git.gunnar.rs` DNATs here |
//! | twin | 212.147.247.82 | **nothing**: 22/80/443/2222/50051 all filtered |
//!
//! **gunnar's gRPC control plane cannot be benched directly.** It listens on
//! `10.13.8.101:50051` — an UpCloud SDN address, private to the estate. From
//! outside there is exactly one route that reaches it: `/login/repos`, the
//! login page's public repository list, which the BFF answers by calling
//! gunnar over that gRPC socket. So `bff-public-repos` is the gRPC read verb's
//! measurement, with the front's proxy hop included in every number, and that
//! is stated rather than hidden.
//!
//! **Everything else on the console answers 401.** `require_auth` wraps every
//! `/api/` route, and the console's only login is a WebAuthn passkey — there is
//! no token this bench could hold even if it should. So the authenticated read
//! endpoints are measured as what they are from here: the cost of the auth
//! layer refusing, which is a real and useful capacity number (it is what a
//! crawler or a scanner costs the front) and is honestly labelled.
//!
//! ## MEASURED, 2026-09-16, from a laptop in Sweden
//!
//! The VMs, as the UpCloud API reports them: front `STARTER-4xCPU-8GB`,
//! appliance `STARTER-4xCPU-16GB`, twin `STARTER-1xCPU-1GB`, all `se-sto1`.
//!
//! | offered | probe | p50 | p90 | p99 | achieved | refusals |
//! |-------------------|--------------------|-----|------|------|----------|----------|
//! | 2 rps / 4 | all seven | ≤287 | ≤327 | ≤517 | 2.03 rps | none |
//! | 15 rps / 8, n=900 | `bff-public-repos` | 40 | 104 | 396 | 15.0 rps | none |
//! | 15 rps / 8, n=900 | `bff-auth-status` | 37 | 65 | 630 | 15.0 rps | none |
//! | 15 rps / 8, n=900 | `front-login-page` | 367 | 1355 | 5276 | **11.3** | none |
//! | 40 rps / 32, n=400| `front-login-page` | 717 | 2701 | 6097 | **24.0** | none |
//! | 80 rps / 48, n=600| `bff-public-repos` | 58 | 270 | 514 | 77.6 rps | none |
//! | 15 rps / 8, n=600 | both SSH doors | 38 | ≤54 | ≤123 | 15.0 rps | none |
//! | 30 rps / 24, n=300| `git-ls-refs` | 540 | 1490 | 2013 | **25.8** | none |
//!
//! `git-ls-refs` is the real thing: a full SSH key exchange, publickey auth and
//! a protocol-v2 `ls-refs` against `git.gunnar.rs:2222`, through
//! `gunnar_client::ls_remote` over russh — no `git` subprocess. Alone at 1 rps
//! it costs 270 ms. The appliance absorbs **26 of them per second** on 4 cores,
//! p90 1.5 s, p99 2.0 s, zero refusals, and the generator keeps its schedule.
//! That is the busiest real workload measured anywhere on this estate.
//!
//! **Where it saturates: the 36 KB `/login` page, on BYTES, not on requests.**
//! The small JSON routes and the SSH door hold every rate that was offered, up
//! to 78 rps, with a flat tail and not one refusal of any kind. `/login` stops
//! scaling between 12 and 24 rps and plateaus at about **0.87 MB/s ≈ 7 Mbit/s**
//! of HTTPS payload, at which point p90 is 2.7 s and the generator's own
//! schedule slips by seconds. `/login` is served uncompressed: the BFF's
//! `CompressionLayer` is on the readme and render routes, not on the SPA shell.
//!
//! **Whether that 7 Mbit/s is the front, its egress, or the measuring laptop's
//! own uplink cannot be told from here**, and that is precisely the question
//! `--from-upcloud` exists to answer. It did not run (see `src/upcloud.rs`), so
//! the ceiling is recorded as measured-from-here and not attributed.
//!
//! **A rare 5-7 s stall** appears at roughly one request in several hundred,
//! and it is NOT confined to one service: the same 5.8-5.9 s shows up on
//! `bff-public-repos` and on `bff-auth-status` (so it is not the gRPC hop), and
//! 5.3-7.0 s shows up on `git-ls-refs`, which is SSH on a different port to a
//! different machine. The one probe it never produced in 1200 attempts is
//! `ssh-kexinit`, which is also the only probe that completes in a single
//! round trip and does no crypto — i.e. the one with the fewest packets to
//! lose. A 5-7 s stall with a clean p99 underneath it, on every service at
//! once, in proportion to the number of round trips, is the shape of packet
//! loss on the path rather than of anything the estate is doing. From this
//! vantage that cannot be proven, and `--from-upcloud` is how it would be.
//!
//! **Zero refusals were ever produced.** In every run above, across every rate,
//! the by-reason counters for `maintenance`, `rate-limited`, `server-error`,
//! `timeout`, `connect-refused`, `connection-reset` and `tls` stayed at zero.
//! The estate degrades by getting slower, not by saying no.
//!
//! **Read the tail honestly.** Percentiles are nearest-rank, so at the
//! conservative profile's `--requests 60` the reported p99 IS the maximum. Use
//! `--requests 300` or more before believing a p99 number.
use std::time::Duration;
use crate::http::Probe;
use crate::outcome::Outcome;
use crate::verdict::Threshold;
/// The three machines, for the report header.
pub const MACHINES: &[(&str, &str)] = &[
("front", "95.111.204.164"),
("appliance", "81.27.106.241"),
("twin", "212.147.247.82"),
];
/// The git SSH doors, as `(name, host, port)`.
pub const SSH_DOORS: &[(&str, &str, u16)] = &[
("ssh-kexinit-appliance", "81.27.106.241", 2222),
("ssh-kexinit-front", "95.111.204.164", 2222),
];
/// A benched HTTP endpoint, before its agent is built.
pub struct Spec {
pub name: &'static str,
pub url: &'static str,
/// What a healthy estate answers with.
pub expect: Outcome,
pub drain_all: bool,
pub bar: Threshold,
/// One line for the report saying what this number is about.
pub about: &'static str,
}
/// The catalogue. Nothing is added here without saying what it measures, and
/// nothing that is not a read gets in at all — [`crate::safety`] enforces the
/// second half at construction.
pub const HTTP_PROBES: &[Spec] = &[
Spec {
name: "front-health",
url: "https://gunnar.rs/health",
expect: Outcome::Ok,
drain_all: true,
bar: Threshold::default_bar(),
about: "Caddy → the console BFF, two bytes back. The floor: TLS, proxy hop, nothing else.",
},
Spec {
name: "front-login-page",
url: "https://gunnar.rs/login",
expect: Outcome::Ok,
drain_all: true,
bar: Threshold::default_bar(),
about: "the console's 36 KB SPA shell, rendered by the BFF. The heaviest public page.",
},
Spec {
name: "bff-auth-status",
url: "https://gunnar.rs/auth/status",
expect: Outcome::Ok,
drain_all: true,
bar: Threshold::default_bar(),
about: "the BFF's own session + bootstrap state. BFF work, no gunnar call.",
},
Spec {
name: "bff-public-repos",
url: "https://gunnar.rs/login/repos",
expect: Outcome::Ok,
drain_all: true,
bar: Threshold::default_bar(),
about: "THE gRPC READ PATH: BFF → gunnar's control plane on the private SDN → back.",
},
Spec {
name: "console-auth-wall",
url: "https://gunnar.rs/api/version",
expect: Outcome::Unauthorised,
drain_all: true,
bar: Threshold::default_bar(),
about: "require_auth refusing. 401 is the HEALTHY answer here; it is what a scanner costs.",
},
];
/// Build the probes, applying `--only`. A spec whose URL the safety gate
/// refuses stops the whole run rather than being skipped: a target list that
/// quietly lost an entry is worse than no run.
pub fn build(
only: Option<&str>,
timeout: Duration,
max_in_flight: usize,
) -> Result<Vec<(&'static Spec, Probe)>, String> {
let mut out = Vec::with_capacity(HTTP_PROBES.len());
for s in HTTP_PROBES {
if let Some(o) = only {
if o != s.name {
continue;
}
}
let p = Probe::new(s.name, s.url, s.expect, s.drain_all, timeout, max_in_flight)?;
out.push((s, p));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_catalogued_url_passes_the_safety_gate() {
for s in HTTP_PROBES {
crate::safety::SafeGet::new(s.url)
.unwrap_or_else(|e| panic!("catalogue entry {} is not safe: {e}", s.name));
}
}
#[test]
fn probe_names_are_unique() {
let mut n: Vec<&str> = HTTP_PROBES.iter().map(|s| s.name).collect();
let before = n.len();
n.sort_unstable();
n.dedup();
assert_eq!(n.len(), before);
}
#[test]
fn only_selects_exactly_one() {
let v = build(Some("front-health"), Duration::from_secs(1), 2).unwrap();
assert_eq!(v.len(), 1);
assert_eq!(v[0].0.name, "front-health");
}
#[test]
fn an_unknown_only_selects_nothing_rather_than_everything() {
let v = build(Some("nope"), Duration::from_secs(1), 2).unwrap();
assert!(v.is_empty());
}
/// The catalogue must contain the one route that reaches gunnar's gRPC.
#[test]
fn the_grpc_read_path_is_in_the_catalogue() {
assert!(HTTP_PROBES.iter().any(|s| s.name == "bff-public-repos"));
}
/// And at least one probe whose healthy answer is a refusal, so the
/// expected-bucket machinery is exercised against the real estate.
#[test]
fn at_least_one_probe_expects_a_refusal() {
assert!(HTTP_PROBES
.iter()
.any(|s| s.expect == Outcome::Unauthorised));
}
}