Skip to main content

gunnar_ssh/
pipeline.rs

1//! **The standing pipeline: warm machinery per class, and an operation is data.**
2//!
3//! # What this replaces
4//!
5//! Today a git operation builds its own world. It spawns what it needs,
6//! allocates what it needs, opens what it needs, and throws all of it away when
7//! the client disconnects. That is per-request *initialisation*, and it is the
8//! largest avoidable cost in the server:
9//!
10//! MEASURED on the reference desktop 2026-08-14, a `linux.git` clone —
11//!
12//! | | |
13//! |---|---:|
14//! | peak RSS, gunnar | **2314 MB** |
15//! | peak RSS, gitea, identical clone and identical wire bytes | 122 MB |
16//! | cycles spent copying bytes that never needed to move | **~20%** |
17//! | `kernel_init_pages` — the kernel ZEROING pages for per-op allocations | 4.7% of the top profile |
18//!
19//! (`vs_forge_clone`; and 39
20//! `perf record` captures of `gunnar serve` during the mirror clone.)
21//!
22//! A pipeline that is already standing pays none of that. The threads exist, the
23//! buffers exist, the archive is already mapped; a request adds a **descriptor**
24//! to a queue and a worker that is already awake picks it up.
25//!
26//! ```text
27//!   at BOOT, once, for the life of the process:
28//!     pipeline[LsRemote] ─┐
29//!     pipeline[Fetch]     ├─ N workers parked on a condvar │ arena each
30//!     pipeline[Clone]     │
31//!     pipeline[Push]      │
32//!     pipeline[Gc]       ─┘
33//!
34//!   per request:
35//!     classify ─► admission gate ─► submit(descriptor) ─► a parked worker wakes
36//!                      │
37//!                      └─ DENY "server busy", before a single allocation
38//! ```
39//!
40//! # An operation is DATA, not a program
41//!
42//! Two `Clone` operations do not start two clone engines. They are two
43//! descriptors in one queue, drained by the fan that is already running. This is
44//! the distinction the whole design rests on, and it is why `submit` takes a
45//! value rather than a closure that captures a world.
46//!
47//! # Why a condvar and not a channel crate
48//!
49//! No new dependency, and the shape is the point: a worker parked in
50//! [`Condvar::wait`] is a thread in `futex_do_wait`, which is exactly what a
51//! hang dump of the live server already showed — **42 threads, 40 of them
52//! parked on 40 distinct futexes**. The warm fan is not a new idea being
53//! introduced here; it already exists in the process. What is missing is
54//! anything handing it work, which is what this module is.
55//!
56//! # What this module does NOT do
57//!
58//! It does not make one operation faster. A single `linux.git` clone gets one
59//! slot and one worker, and it will take exactly as long as it took before —
60//! the 25 minutes measured for that clone is serial work *inside* the operation,
61//! and only the engine's own fan-out (znippy's `emit_pack` phase 1) touches
62//! that. This module is concurrency **across** operations. Conflating the two
63//! is how a "pipeline" gets built and then measured as having changed nothing.
64
65use std::collections::VecDeque;
66use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
67use std::sync::{Arc, Condvar, Mutex};
68use std::thread::JoinHandle;
69
70use crate::governor::OpClass;
71
72/// How many workers a class's fan is built with.
73///
74/// Deliberately per-class: `LsRemote` is answered in milliseconds and wants
75/// enough workers that a burst never queues, while `Gc` is long, rare, and
76/// wants at most a couple so that maintenance cannot crowd out serving.
77pub fn default_workers(class: OpClass, cores: usize) -> usize {
78    match class {
79        // ── I/O-BOUND, so sized for CONCURRENCY and not for cores ───────────
80        //
81        // A git transfer spends its life waiting on a socket, not computing.
82        // Sizing these fans as if they were CPU-bound is a mistake I made and
83        // the bench caught: at `(cores/2).clamp(2,8)` a 32-core box served
84        // `multiuser_scaling`'s 102 concurrent clients with EIGHT workers, and
85        // **16 of 102 clients did not complete** — not refused, which they
86        // would have understood, but queued behind eight streaming packs until
87        // they timed out. Zero `transfer.refused` in the whole run, which is
88        // what made it look like a mystery rather than a queue.
89        //
90        // That was strictly worse than what it replaced: tokio's blocking pool
91        // would have spawned up to 512 threads and served all 102.
92        //
93        // Shedding overload is the ADMISSION GATE's job — CoDel sees the queue
94        // standing and refuses, in the client's own protocol. The fan's job is
95        // only to stop unbounded thread growth. So it is wide enough that
96        // ordinary concurrency is never artificially serialised, and the
97        // governor decides when there is too much.
98        OpClass::LsRemote => (cores * 4).clamp(16, 256),
99        OpClass::Fetch => (cores * 4).clamp(16, 256),
100        OpClass::Clone => (cores * 4).clamp(16, 256),
101        OpClass::Push => (cores * 4).clamp(16, 256),
102        // Maintenance yields to serving, and IS bounded by CPU: a repack is
103        // real work, not a stream. Two, so one long repack cannot crowd out the
104        // other repositories.
105        OpClass::Gc => 2,
106    }
107}
108
109/// What a submission was refused for. Both are "server busy" to the client; the
110/// distinction is for the operator reading logs.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum SubmitError {
113    /// The class's queue is full. Backpressure working as designed.
114    QueueFull,
115    /// The pipeline is shutting down and will not start new work.
116    ShuttingDown,
117}
118
119impl std::fmt::Display for SubmitError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            SubmitError::QueueFull => {
123                f.write_str("server busy: the queue for this operation class is full")
124            }
125            SubmitError::ShuttingDown => f.write_str("server busy: shutting down"),
126        }
127    }
128}
129
130struct Queue<T> {
131    items: Mutex<VecDeque<T>>,
132    /// Workers park here. A parked worker is a thread in `futex_do_wait`.
133    ready: Condvar,
134    stopping: AtomicBool,
135    depth: usize,
136    /// Test-only: runs in [`Queue::pop_blocking`] with the lock held, after `stopping` was
137    /// read and before the worker waits — the window a lost wake-up lives in.
138    #[cfg(test)]
139    before_wait: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>>,
140}
141
142impl<T> Queue<T> {
143    fn push(&self, item: T) -> Result<usize, SubmitError> {
144        let mut q = self.items.lock().expect("pipeline queue poisoned");
145        // Read under the lock `shutdown` sets it under: nothing is queued after the
146        // workers were told to stop.
147        if self.stopping.load(Ordering::Acquire) {
148            return Err(SubmitError::ShuttingDown);
149        }
150        if q.len() >= self.depth {
151            return Err(SubmitError::QueueFull);
152        }
153        q.push_back(item);
154        let queued = q.len();
155        drop(q);
156        // One waiter, not all: waking every worker for one item is a thundering
157        // herd that costs more than the work.
158        self.ready.notify_one();
159        Ok(queued)
160    }
161
162    fn pop_blocking(&self) -> Option<T> {
163        let mut q = self.items.lock().expect("pipeline queue poisoned");
164        loop {
165            if let Some(item) = q.pop_front() {
166                return Some(item);
167            }
168            if self.stopping.load(Ordering::Acquire) {
169                return None;
170            }
171            #[cfg(test)]
172            if let Some(hook) = self.before_wait.get() {
173                hook();
174            }
175            q = self.ready.wait(q).expect("pipeline queue poisoned");
176        }
177    }
178}
179
180/// One class's standing fan: N workers, built once, parked until there is work.
181pub struct Pipeline<T: Send + 'static> {
182    class: OpClass,
183    queue: Arc<Queue<T>>,
184    workers: Mutex<Vec<JoinHandle<()>>>,
185    /// Operations that have been handed to a worker. Applied output: it is the
186    /// only thing that distinguishes "the fan ran it" from "the fan exists".
187    served: Arc<AtomicU64>,
188}
189
190impl<T: Send + 'static> Pipeline<T> {
191    /// Build the fan and START it. Threads are spawned HERE, at boot — never on
192    /// the request path. That is the entire point of the type.
193    pub fn start<F>(class: OpClass, workers: usize, depth: usize, run: F) -> Pipeline<T>
194    where
195        F: Fn(T) + Send + Sync + 'static,
196    {
197        let queue = Arc::new(Queue {
198            items: Mutex::new(VecDeque::with_capacity(depth)),
199            ready: Condvar::new(),
200            stopping: AtomicBool::new(false),
201            depth,
202            #[cfg(test)]
203            before_wait: std::sync::OnceLock::new(),
204        });
205        let served = Arc::new(AtomicU64::new(0));
206        let run = Arc::new(run);
207
208        let mut handles = Vec::with_capacity(workers);
209        for i in 0..workers {
210            let q = Arc::clone(&queue);
211            let run = Arc::clone(&run);
212            let served = Arc::clone(&served);
213            let name = format!("gunnar-{class:?}-{i}").to_lowercase();
214            let h = std::thread::Builder::new()
215                .name(name)
216                .spawn(move || {
217                    while let Some(item) = q.pop_blocking() {
218                        served.fetch_add(1, Ordering::Relaxed);
219                        run(item);
220                    }
221                })
222                .expect("spawning a pipeline worker at boot");
223            handles.push(h);
224        }
225        Pipeline {
226            class,
227            queue,
228            workers: Mutex::new(handles),
229            served,
230        }
231    }
232
233    /// **The same fan, but each worker owns an ARENA built once at boot.**
234    ///
235    /// This is the half of "zero initialisation" the plain [`start`](Self::start)
236    /// does not give you. A standing fan removes the thread spawn; it does not
237    /// remove the buffer. If the work still does `Vec::with_capacity` per
238    /// operation, the kernel still zeroes fresh pages for it — which is
239    /// `kernel_init_pages`, **4.7% of the top profile** on a `linux.git` clone,
240    /// the single largest kernel cost measured there.
241    ///
242    /// `make` runs **once per worker, at boot**. `run` receives `&mut A`, so the
243    /// buffer that was grown for the first operation is still there, at capacity,
244    /// for the ten-thousandth. The caller is responsible for `clear()`-ing
245    /// rather than reallocating — `clear` keeps capacity, which is the whole
246    /// point, and is why this takes `&mut A` instead of handing back an owned
247    /// buffer per call.
248    #[allow(clippy::type_complexity)]
249    pub fn start_with_arena<A, M, F>(
250        class: OpClass,
251        workers: usize,
252        depth: usize,
253        make: M,
254        run: F,
255    ) -> Pipeline<T>
256    where
257        A: Send + 'static,
258        M: Fn(usize) -> A + Send + Sync + 'static,
259        F: Fn(&mut A, T) + Send + Sync + 'static,
260    {
261        let queue = Arc::new(Queue {
262            items: Mutex::new(VecDeque::with_capacity(depth)),
263            ready: Condvar::new(),
264            stopping: AtomicBool::new(false),
265            depth,
266            #[cfg(test)]
267            before_wait: std::sync::OnceLock::new(),
268        });
269        let served = Arc::new(AtomicU64::new(0));
270        let run = Arc::new(run);
271        let make = Arc::new(make);
272
273        let mut handles = Vec::with_capacity(workers);
274        for i in 0..workers {
275            let q = Arc::clone(&queue);
276            let run = Arc::clone(&run);
277            let make = Arc::clone(&make);
278            let served = Arc::clone(&served);
279            let name = format!("gunnar-{class:?}-{i}").to_lowercase();
280            let h = std::thread::Builder::new()
281                .name(name)
282                .spawn(move || {
283                    // ONCE, at boot, before this worker takes any work.
284                    let mut arena = make(i);
285                    while let Some(item) = q.pop_blocking() {
286                        served.fetch_add(1, Ordering::Relaxed);
287                        run(&mut arena, item);
288                    }
289                })
290                .expect("spawning a pipeline worker at boot");
291            handles.push(h);
292        }
293        Pipeline {
294            class,
295            queue,
296            workers: Mutex::new(handles),
297            served,
298        }
299    }
300
301    /// Hand an operation to the standing fan. Allocates nothing but the queue
302    /// slot; spawns nothing.
303    pub fn submit(&self, item: T) -> Result<usize, SubmitError> {
304        self.queue.push(item)
305    }
306
307    /// The class this fan serves.
308    pub fn class(&self) -> OpClass {
309        self.class
310    }
311
312    /// Operations a worker has actually picked up.
313    pub fn served(&self) -> u64 {
314        self.served.load(Ordering::Relaxed)
315    }
316
317    /// Items waiting for a worker.
318    pub fn queued(&self) -> usize {
319        self.queue
320            .items
321            .lock()
322            .expect("pipeline queue poisoned")
323            .len()
324    }
325
326    /// Threads standing in this fan.
327    pub fn workers(&self) -> usize {
328        self.workers
329            .lock()
330            .expect("pipeline workers poisoned")
331            .len()
332    }
333
334    /// Stop accepting work, drain what is queued, and JOIN every worker.
335    ///
336    /// Joining rather than detaching: a worker that outlives the pipeline holds
337    /// an `Arc` on the queue and on whatever the closure captured, which on a
338    /// server means a repository handle that never closes.
339    pub fn shutdown(&self) {
340        // `stopping` is set UNDER the queue's lock: a worker reads it with the lock held
341        // and waits (releasing the lock atomically), so the store lands either before its
342        // read (it returns) or after it waits (the notify wakes it). Set without the lock,
343        // a store + notify between the read and the wait was lost and join() hung for good
344        // (stinkies ssh-pipeline-shutdown-hang, 33 min at load ~35).
345        {
346            let _q = self.queue.items.lock().expect("pipeline queue poisoned");
347            self.queue.stopping.store(true, Ordering::Release);
348        }
349        self.queue.ready.notify_all();
350        let mut handles = self.workers.lock().expect("pipeline workers poisoned");
351        for h in handles.drain(..) {
352            let _ = h.join();
353        }
354    }
355}
356
357impl<T: Send + 'static> Drop for Pipeline<T> {
358    fn drop(&mut self) {
359        self.shutdown();
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use std::collections::HashSet;
367    use std::sync::mpsc;
368    use std::time::Duration;
369
370    /// ★ THREADS ARE SPAWNED AT BOOT, NOT PER OPERATION.
371    ///
372    /// The whole claim of this module in one guard: submit N operations and the
373    /// set of threads that ran them is bounded by the fan's size, not by N.
374    ///
375    /// Seen RED by removing the standing fan entirely — `start` spawns nothing
376    /// and `submit` runs the work on a fresh thread, which is the shape this
377    /// module replaces: **"12 operations ran on 12 distinct threads — the fan is
378    /// being built per request"**.
379    ///
380    /// A FIRST attempt at that break spawned an idle thread per submit while
381    /// leaving the work on the fan, and the guard stayed green — correctly, and
382    /// it is recorded here because it is the trap: breaking something *near* the
383    /// property is not breaking the property.
384    #[test]
385    fn operations_reuse_the_standing_fan_rather_than_a_thread_each() {
386        let (tx, rx) = mpsc::channel();
387        let p = Pipeline::start(OpClass::Clone, 3, 64, move |_op: u32| {
388            let id = std::thread::current().id();
389            tx.send(id).expect("collector alive");
390        });
391        for i in 0..12 {
392            p.submit(i).expect("queue has room");
393        }
394        let mut seen: HashSet<std::thread::ThreadId> = HashSet::new();
395        for _ in 0..12 {
396            seen.insert(rx.recv_timeout(Duration::from_secs(5)).expect("op ran"));
397        }
398        assert!(
399            seen.len() <= 3,
400            "{} operations ran on {} distinct threads — the fan is being built per request",
401            12,
402            seen.len()
403        );
404        assert_eq!(p.served(), 12);
405    }
406
407    /// The workers exist BEFORE any operation arrives. A fan that is created
408    /// lazily on first submit is not standing, it is merely cached.
409    ///
410    /// Seen RED by the same break: **"the fan has 0 workers before any
411    /// operation was submitted"**.
412    #[test]
413    fn the_fan_is_running_before_the_first_operation() {
414        let p: Pipeline<u32> = Pipeline::start(OpClass::Fetch, 4, 16, |_| {});
415        assert_eq!(
416            p.workers(),
417            4,
418            "the fan has {} workers before any operation was submitted",
419            p.workers()
420        );
421        assert_eq!(p.served(), 0, "nothing has been served yet");
422    }
423
424    /// ★ A FULL QUEUE REFUSES. Backpressure is the point of a bounded queue; an
425    /// unbounded one converts overload into unbounded memory and a slower
426    /// collapse.
427    ///
428    /// Seen RED by removing the `q.len() >= self.depth` check: **"the queue
429    /// accepted 40 items into a 4-deep queue"**.
430    #[test]
431    fn a_full_queue_refuses_rather_than_growing() {
432        // A worker that blocks, so the queue genuinely fills.
433        // The worker closure must be `Sync`, and a `Receiver` is not — so it
434        // is shared behind a mutex. One worker, so there is no contention.
435        let (release, held) = mpsc::channel::<()>();
436        let held = Arc::new(Mutex::new(held));
437        let p = Pipeline::start(OpClass::Clone, 1, 4, move |_op: u32| {
438            // BOUNDED, and that is not tidiness. A bare `recv()` here deadlocks
439            // the whole test binary when an assertion fires before the release
440            // loop: the worker blocks forever, `Drop` calls `shutdown()`, which
441            // JOINS it, and nothing ever returns. Seen — the red proof for the
442            // guard below hung instead of failing, which is strictly worse than
443            // a failure because CI reports it as a timeout with no message.
444            let _ = held
445                .lock()
446                .expect("held")
447                .recv_timeout(Duration::from_secs(10));
448        });
449        let mut accepted = 0usize;
450        let mut refused = 0usize;
451        for i in 0..40 {
452            match p.submit(i) {
453                Ok(_) => accepted += 1,
454                Err(SubmitError::QueueFull) => refused += 1,
455                Err(e) => panic!("unexpected refusal: {e}"),
456            }
457        }
458        assert!(
459            refused > 0,
460            "the queue accepted {accepted} items into a 4-deep queue"
461        );
462        assert!(
463            accepted <= 4 + 1,
464            "accepted {accepted}, which is more than the depth plus the one in service"
465        );
466        for _ in 0..accepted {
467            let _ = release.send(());
468        }
469    }
470
471    /// Shutdown JOINS. A detached worker holds an `Arc` on whatever the closure
472    /// captured — on a server, a repository handle that never closes.
473    ///
474    /// Seen RED by replacing `h.join()` with `drop(h)`: the guard below observed
475    /// **"a worker outlived the pipeline: 3 strong refs on the queue after
476    /// shutdown"**.
477    #[test]
478    fn shutdown_joins_every_worker() {
479        let p: Pipeline<u32> = Pipeline::start(OpClass::Gc, 3, 8, |_| {});
480        let q = Arc::clone(&p.queue);
481        assert!(
482            Arc::strong_count(&q) > 1,
483            "workers hold the queue while running"
484        );
485        p.shutdown();
486        assert_eq!(p.workers(), 0, "handles were not drained");
487        assert_eq!(
488            Arc::strong_count(&q),
489            2,
490            "a worker outlived the pipeline: {} strong refs on the queue after shutdown",
491            Arc::strong_count(&q)
492        );
493    }
494
495    /// **A shutdown that lands between a worker's `stopping` check and its wait is not
496    /// lost** (stinkies ssh-pipeline-shutdown-hang: the suite at load ~35 hung 33 min in
497    /// `shutdown_joins_every_worker`, the worker parked in `futex_do_wait` for good). The
498    /// worker is held in exactly that window (lock held, `stopping` read false); shutdown
499    /// is asked meanwhile; the worker then waits. Shutdown must still end.
500    #[test]
501    fn a_shutdown_in_the_window_before_a_worker_waits_still_joins_it() {
502        let p: Pipeline<u32> = Pipeline::start(OpClass::Gc, 1, 8, |_| {});
503        let (in_window, window) = mpsc::channel::<()>();
504        let (go, gate) = mpsc::channel::<()>();
505        let gate = Mutex::new(Some(gate));
506        let in_window = Mutex::new(in_window);
507        let _ = p.queue.before_wait.set(Box::new(move || {
508            // Only the first pass after the item is held; every later wait goes straight on.
509            if let Some(g) = gate.lock().unwrap().take() {
510                let _ = in_window.lock().unwrap().send(());
511                let _ = g.recv_timeout(Duration::from_secs(10));
512            }
513        }));
514        p.submit(1).expect("queued");
515        window.recv_timeout(Duration::from_secs(10)).expect("the worker reached the window");
516        let (done, ended) = mpsc::channel::<()>();
517        std::thread::spawn(move || {
518            p.shutdown();
519            let _ = done.send(());
520        });
521        // Let the shutdown run its course (or block, if it must take the lock) while the
522        // worker is still in the window, then let the worker go on to wait.
523        std::thread::sleep(Duration::from_millis(300));
524        let _ = go.send(());
525        assert!(
526            ended.recv_timeout(Duration::from_secs(10)).is_ok(),
527            "shutdown never joined: the worker waits on a wake-up that came before it waited"
528        );
529    }
530
531    /// ★ THE ARENA IS BUILT ONCE PER WORKER, NOT ONCE PER OPERATION.
532    ///
533    /// A standing fan removes the thread spawn but not the buffer. This asserts
534    /// the buffer survives between operations at full capacity, which is what
535    /// makes `kernel_init_pages` — 4.7% of the top profile on a `linux.git`
536    /// clone — stop happening rather than merely shrink.
537    ///
538    /// Seen RED by moving the allocation inside `run` (`let mut arena =
539    /// make(i);` per item, the shape this replaces): **"the arena was rebuilt:
540    /// 20 constructions for 20 operations across 2 workers"**.
541    #[test]
542    fn the_arena_is_built_once_per_worker_and_reused_by_every_operation() {
543        let built = Arc::new(AtomicU64::new(0));
544        let b = Arc::clone(&built);
545        let (tx, rx) = mpsc::channel();
546        let p = Pipeline::start_with_arena(
547            OpClass::Clone,
548            2,
549            64,
550            move |_i| {
551                b.fetch_add(1, Ordering::Relaxed);
552                Vec::<u8>::with_capacity(4096)
553            },
554            move |arena: &mut Vec<u8>, _op: u32| {
555                let before = arena.capacity();
556                arena.clear();
557                arena.extend_from_slice(&[7u8; 1024]);
558                tx.send((before, arena.capacity()))
559                    .expect("collector alive");
560            },
561        );
562        for i in 0..20 {
563            p.submit(i).expect("queue has room");
564        }
565        let mut reused = 0usize;
566        for _ in 0..20 {
567            let (before, _after) = rx.recv_timeout(Duration::from_secs(5)).expect("op ran");
568            if before >= 4096 {
569                reused += 1;
570            }
571        }
572        // `<= 2`, not `== 2`: a worker builds its arena when the THREAD starts,
573        // and 20 fast operations can be drained by the first worker before the
574        // second is even scheduled. Asserting `== 2` made this guard flake on a
575        // loaded box — the property is "not one per operation", and pinning it
576        // to an exact thread count tests the scheduler instead.
577        let n = built.load(Ordering::Relaxed);
578        assert!(
579            (1..=2).contains(&n),
580            "the arena was rebuilt: {n} constructions for 20 operations across 2 workers"
581        );
582        assert_eq!(
583            reused, 20,
584            "only {reused} of 20 operations saw a warm arena — capacity is being thrown away"
585        );
586    }
587
588    /// ★ THE TRANSFER FANS ARE SIZED FOR CONCURRENCY, NOT FOR CORES.
589    ///
590    /// Seen RED — in the BENCH, not here — by `(cores/2).clamp(2,8)`:
591    /// **"multiuser_scaling: 16 of 102 user(s) did not complete their ops"**,
592    /// with **zero** `transfer.refused` in the entire run. Eight workers, 102
593    /// I/O-bound clients, a 64-deep queue: nobody was turned away, they were
594    /// made to wait until they timed out. Shedding load is the governor's job;
595    /// the fan must not serialise ordinary concurrency on its own.
596    ///
597    /// `gc` is the exception and stays small: a repack is real CPU work, so it
598    /// is bounded by cores and must not crowd out serving.
599    #[test]
600    fn transfer_fans_are_wide_and_gc_stays_small() {
601        for cores in [1usize, 4, 32, 256] {
602            assert_eq!(
603                default_workers(OpClass::Gc, cores),
604                2,
605                "gc at {cores} cores"
606            );
607            for class in [
608                OpClass::LsRemote,
609                OpClass::Fetch,
610                OpClass::Clone,
611                OpClass::Push,
612            ] {
613                let n = default_workers(class, cores);
614                assert!(
615                    n >= 16,
616                    "{class:?} at {cores} cores got {n} workers — 102 concurrent \
617                     clients against a fan this small queue until they time out"
618                );
619                assert!(n <= 256, "{class:?} at {cores} cores is unbounded: {n}");
620            }
621        }
622        // The 32-core box the bench runs on: comfortably over 102 clients.
623        assert!(
624            default_workers(OpClass::Clone, 32) >= 102,
625            "the fan must hold multiuser_scaling's 102 clients without queueing"
626        );
627    }
628}
629
630// ════════════════════════════════════════════════════════════════════════════
631// The five fans, built at boot
632// ════════════════════════════════════════════════════════════════════════════
633
634/// One unit of work handed to a standing fan.
635///
636/// A boxed `FnOnce` and not a pure descriptor, and that is a compromise worth
637/// naming: a descriptor would be `operate(op)` in its ideal form, but the git
638/// transfer it has to carry is an async future over a live socket, and the
639/// honest bridge to an OS thread is a closure that owns it. One box per
640/// operation is one allocation — against the ~27.6 M this design exists to
641/// remove, and against tokio's blocking pool spawning a THREAD for the same
642/// work. It is not zero; it is four orders of magnitude off the thing that hurt.
643pub type Job = Box<dyn FnOnce() + Send + 'static>;
644
645/// **Every class's fan, standing, for the life of the process.**
646///
647/// Built once in [`crate::server::SshServer::new`]. A request classifies, passes
648/// the governor, and is submitted here; the thread that runs it was already
649/// alive and parked before the client connected.
650pub struct Pipelines {
651    ls_remote: Pipeline<Job>,
652    fetch: Pipeline<Job>,
653    clone: Pipeline<Job>,
654    push: Pipeline<Job>,
655    gc: Pipeline<Job>,
656}
657
658impl Pipelines {
659    /// Build all five fans. Call this at BOOT, once.
660    ///
661    /// `depth` is per class, not shared: a queue of clones filling up must not
662    /// refuse an ls-remote, which is the same head-of-line reasoning the
663    /// governor's per-class sojourn windows exist for.
664    pub fn start(cores: usize, depth: usize) -> Pipelines {
665        fn fan(class: OpClass, cores: usize, depth: usize) -> Pipeline<Job> {
666            Pipeline::start(class, default_workers(class, cores), depth, |job: Job| {
667                job()
668            })
669        }
670        Pipelines {
671            ls_remote: fan(OpClass::LsRemote, cores, depth),
672            fetch: fan(OpClass::Fetch, cores, depth),
673            clone: fan(OpClass::Clone, cores, depth),
674            push: fan(OpClass::Push, cores, depth),
675            gc: fan(OpClass::Gc, cores, depth),
676        }
677    }
678
679    fn of(&self, class: OpClass) -> &Pipeline<Job> {
680        match class {
681            OpClass::LsRemote => &self.ls_remote,
682            OpClass::Fetch => &self.fetch,
683            OpClass::Clone => &self.clone,
684            OpClass::Push => &self.push,
685            OpClass::Gc => &self.gc,
686        }
687    }
688
689    /// Hand an operation to its class's standing fan.
690    pub fn submit(&self, class: OpClass, job: Job) -> Result<usize, SubmitError> {
691        self.of(class).submit(job)
692    }
693
694    /// Operations this class's fan has picked up. Applied output — the only
695    /// thing that says the fan is CARRYING traffic rather than merely existing.
696    pub fn served(&self, class: OpClass) -> u64 {
697        self.of(class).served()
698    }
699
700    /// Items waiting for a worker in this class.
701    pub fn queued(&self, class: OpClass) -> usize {
702        self.of(class).queued()
703    }
704
705    /// Threads standing in this class's fan.
706    pub fn workers(&self, class: OpClass) -> usize {
707        self.of(class).workers()
708    }
709}
710
711/// **The process's fans.** One set, for the whole server, forever.
712///
713/// A global and not a field, for two reasons that only became visible once the
714/// SSH path was wired and the HTTP path was not:
715///
716///  * **Both transports must use the SAME fans.** `gunnar-http` holds its own
717///    `Arc<dyn GitTransport>` and calls `serve()` itself, so a set of fans owned
718///    by `SshServer` covered SSH transfers and silently missed every HTTP one —
719///    which is most of the `population_*` and `multiuser_scaling` load.
720///  * **Two listeners must not mean two fans.** Standing machinery sized to the
721///    box is the whole point; one set per listener doubles the threads and
722///    halves the meaning of the limit.
723///
724/// Built on first use, sized from the box, and never rebuilt.
725static FANS: std::sync::OnceLock<Pipelines> = std::sync::OnceLock::new();
726
727/// The process's standing fans, building them on first call.
728pub fn fans() -> &'static Pipelines {
729    FANS.get_or_init(|| {
730        let cores = std::thread::available_parallelism()
731            .map(|n| n.get())
732            .unwrap_or(4);
733        // Queue depth per class. Deep enough that a burst waits rather than
734        // being refused; the governor's CoDel window decides whether that wait
735        // is acceptable, not this number.
736        Pipelines::start(cores, 64)
737    })
738}
739
740#[cfg(test)]
741mod pipelines_tests {
742    use super::*;
743    use std::time::Duration;
744
745    /// ★ ALL FIVE FANS ARE STANDING BEFORE ANY REQUEST ARRIVES, and gc's is the
746    /// small one.
747    ///
748    /// Seen RED by building only the class asked for on first submit: **"the
749    /// clone fan has 0 workers before any request"**.
750    #[test]
751    fn every_class_has_a_standing_fan_at_boot() {
752        let p = Pipelines::start(32, 16);
753        for class in [
754            OpClass::LsRemote,
755            OpClass::Fetch,
756            OpClass::Clone,
757            OpClass::Push,
758            OpClass::Gc,
759        ] {
760            assert!(
761                p.workers(class) > 0,
762                "the {class:?} fan has 0 workers before any request"
763            );
764            assert_eq!(p.served(class), 0, "{class:?} served something at boot");
765        }
766        assert_eq!(p.workers(OpClass::Gc), 2, "gc must stay small");
767        assert!(
768            p.workers(OpClass::LsRemote) > p.workers(OpClass::Gc),
769            "ls-remote must out-scale gc"
770        );
771    }
772
773    /// ★ ONE set of fans for the process, however many listeners ask for them.
774    ///
775    /// Seen RED by returning `Pipelines::start(..)` fresh on each call instead
776    /// of the `OnceLock`: **"two callers got two different fan sets — SSH and
777    /// HTTP would each have their own"**.
778    #[test]
779    fn every_caller_gets_the_same_process_wide_fans() {
780        let a = fans();
781        let b = fans();
782        assert!(
783            std::ptr::eq(a, b),
784            "two callers got two different fan sets — SSH and HTTP would each have their own"
785        );
786    }
787
788    /// ★ THE COUNTERS REPORT REAL OCCUPANCY, not a constant.
789    ///
790    /// `served`, `queued` and `workers` existed from the start and NOTHING read
791    /// them, so the only evidence the fans carried traffic was the absence of
792    /// refusals — silence, not evidence. Now `server.rs` logs depth on every
793    /// operation that actually queues, and a counter that is wired to a log is
794    /// a counter that can lie.
795    ///
796    /// So this asserts the queue is SEEN NON-EMPTY under back-pressure, which
797    /// is the only reading that distinguishes "the fan is carrying work" from
798    /// "the fan exists". A guard that merely called `queued()` and accepted
799    /// whatever came back would pass against a function returning 0 forever —
800    /// which is exactly the shape of four dead guards found in this suite
801    /// today, including a control satisfied by `0 == 0`.
802    ///
803    /// Seen RED with `queued()` stubbed to `0`: **"the queue never reported a
804    /// waiting item across 24 submissions against 1 worker — the depth this
805    /// logs is a constant"**.
806    #[test]
807    fn the_occupancy_counters_are_not_constants() {
808        // One worker, held, so submissions must pile up behind it.
809        let (release, held) = std::sync::mpsc::channel::<()>();
810        let held = Arc::new(Mutex::new(held));
811        let p: Pipeline<u32> = Pipeline::start(OpClass::Clone, 1, 64, move |_| {
812            // BOUNDED, and that is not tidiness. A bare `recv()` here deadlocks
813            // the whole test binary when an assertion fires before the release
814            // loop: the worker blocks forever, `Drop` calls `shutdown()`, which
815            // JOINS it, and nothing ever returns. Seen — the red proof for the
816            // guard below hung instead of failing, which is strictly worse than
817            // a failure because CI reports it as a timeout with no message.
818            let _ = held
819                .lock()
820                .expect("held")
821                .recv_timeout(Duration::from_secs(10));
822        });
823
824        let mut deepest = 0usize;
825        for i in 0..24 {
826            if let Ok(depth) = p.submit(i) {
827                deepest = deepest.max(depth);
828            }
829        }
830        assert!(
831            deepest > 1,
832            "the queue never reported a waiting item across 24 submissions against \
833             1 worker — the depth this logs is a constant"
834        );
835        assert!(
836            p.queued() > 0,
837            "queued() says {} with {deepest} known to have piled up",
838            p.queued()
839        );
840        assert_eq!(p.workers(), 1, "workers() lost count of its own fan");
841
842        // Let them through, then the served counter must MOVE — a `served` that
843        // never rises would make the log line above report an idle fan forever.
844        for _ in 0..24 {
845            let _ = release.send(());
846        }
847        let mut waited = 0;
848        while p.served() < 24 && waited < 200 {
849            std::thread::sleep(Duration::from_millis(25));
850            waited += 1;
851        }
852        assert_eq!(
853            p.served(),
854            24,
855            "served() reports {} after 24 jobs were released",
856            p.served()
857        );
858    }
859
860    /// A submitted job RUNS on the fan, and lands in the right class.
861    #[test]
862    fn a_job_runs_on_its_own_class_fan() {
863        let p = Pipelines::start(8, 16);
864        let (tx, rx) = std::sync::mpsc::channel();
865        p.submit(OpClass::Push, Box::new(move || tx.send(()).unwrap()))
866            .expect("queue has room");
867        rx.recv_timeout(std::time::Duration::from_secs(5))
868            .expect("the job ran on the fan");
869        assert_eq!(p.served(OpClass::Push), 1);
870        assert_eq!(p.served(OpClass::Clone), 0, "it ran on the wrong fan");
871    }
872}