//! A stand-in Multithreaded, so the forum-membership screens can be put under //! load without a real instance. //! //! The two described forum screens (`src/quasi/forum_memberships.rs`) are the //! only ones whose work is an outbound HTTP call rather than a database round //! trip, and quasi's router is sync: the described version holds a //! BLOCKING-POOL thread for that whole call, where the Askama version pays the //! same latency on a runtime worker. Every runtime number about the description //! layer so far came from sub-millisecond sqlx queries, so nothing has measured //! that regime. //! //! What makes it measurable is that the upstream latency becomes the variable. //! This stub sleeps `LOAD_MT_LATENCY_MS` before answering, so a run can be //! repeated at 0ms, 50ms, 500ms and 5s and the question becomes the threshold at //! which occupancy starts to cost something, rather than a pass/fail against //! whatever a real instance happened to be doing that afternoon. //! //! It sleeps on the runtime rather than blocking, which is the point: the stub //! must not itself consume the pool being measured. use axum::Router; use axum::extract::Path; use axum::routing::get; use std::time::Duration; /// A running stub, alive until it is dropped. pub(super) struct MtStub { /// What to hand `IntegrationsConfig::mt_base_url`. base_url: String, server: tokio::task::JoinHandle<()>, } impl MtStub { /// Bind on an ephemeral port and start answering. /// /// `latency` is slept before every answer. `memberships` is how many rows /// come back, which decides how much describing the screen has to do once /// the call lands. pub(super) async fn start(latency: Duration, memberships: usize) -> Self { let app = Router::new().route( "/api/user/{user_id}/summary", get(move |Path(_user_id): Path| async move { if !latency.is_zero() { tokio::time::sleep(latency).await; } axum::Json(summary(memberships)) }), ); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("MT stub could not bind"); let addr = listener.local_addr().expect("MT stub has no address"); let server = tokio::spawn(async move { // Ignore the result: the runner drops the stub at the end of the // run and a shutdown error there is not a finding. let _ = axum::serve(listener, app).await; }); MtStub { base_url: format!("http://{addr}"), server, } } pub(super) fn base_url(&self) -> String { self.base_url.clone() } } impl Drop for MtStub { fn drop(&mut self) { self.server.abort(); } } /// The shape `fetch` in `src/quasi/forum_memberships.rs` reads, and the same /// shape the Askama handler in `routes::pages::public::landing` reads. Both /// sides of the comparison parse this, so a change to it moves both together. fn summary(memberships: usize) -> serde_json::Value { let rows: Vec = (0..memberships) .map(|i| { serde_json::json!({ "community_slug": format!("community-{i}"), "community_name": format!("Community {i}"), "role": if i == 0 { "Moderator" } else { "Member" }, "post_count": 40 + i as i64, "joined_at": "2026-03-14T09:00:00Z", }) }) .collect(); serde_json::json!({ "memberships": rows }) }