metrale_storage/cache_peer/
server_impl.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The cache peer's accept loop and connection handler: the v2
4//! handshake, the server side of the rail handshake, and the two data planes
5//! (the shared paging control loop, or RAW mode idling until hangup).
6//!
7//! Owner: metrale-storage peers.
8//! Invariants:
9//! - `handle_conn` holds the crate's only `reg_mr_rw` call (remote read and
10//!   write access).
11//! - A connection's memory registrations are dropped before the arena they
12//!   cover, on every path.
13
14use std::net::{TcpListener, TcpStream, ToSocketAddrs};
15
16use anyhow::{Context, Result, bail};
17
18/// 2026-09-25: The cache peer's settings. A client asks for up to
19/// `rails.len()` rails, and the peer registers the arena on each.
20#[derive(Clone, Debug)]
21pub struct RdmaConfig {
22    /// 2026-09-25: `(device, gid_idx)` per rail; a client asking for n rails
23    /// gets the first n.
24    pub rails: Vec<(String, u32)>,
25    /// 2026-09-25: Ceiling on the arena bytes reserved across this `serve`'s
26    /// connections; 0 means none.
27    pub max_blade_bytes: u64,
28    /// 2026-09-25: Directory of the paging arenas' O_DIRECT swap files. With
29    /// `None`, paging clients are refused.
30    pub swap_dir: Option<std::path::PathBuf>,
31    /// 2026-09-25: Swap-disk budget shared by the paging arenas of kinds
32    /// without a per-kind cap (`registry::carve_disk_slots`); 0 = unbounded.
33    pub swap_cap_bytes: u64,
34    /// 2026-09-25: Per-kind disk caps (`kind.0` → bytes). A listed kind gets
35    /// this budget instead of a share of `swap_cap_bytes`; 0 = unbounded.
36    pub per_kind_swap_cap_bytes: std::collections::HashMap<u8, u64>,
37}
38
39impl Default for RdmaConfig {
40    fn default() -> Self {
41        Self {
42            rails: vec![("roceP2p1s0f1".into(), 3), ("rocep1s0f1".into(), 3)],
43            max_blade_bytes: 0,
44            swap_dir: None,
45            swap_cap_bytes: 50 * 1024 * 1024 * 1024,
46            per_kind_swap_cap_bytes: std::collections::HashMap::new(),
47        }
48    }
49}
50
51/// 2026-09-25: Serve on `addr`, one thread per connection. Accept errors are
52/// logged and skipped; only a bind error returns.
53pub fn serve<A: ToSocketAddrs>(addr: A, rdma: RdmaConfig) -> Result<()> {
54    let listener = TcpListener::bind(addr).context("bind cache-peer listener")?;
55    let local = listener.local_addr().ok();
56    // 2026-09-25: One ledger per `serve`, shared by its connection threads.
57    let ledger = std::sync::Arc::new(crate::blade_cap::CommitLedger::new(rdma.max_blade_bytes));
58    tracing::info!(
59        "cache-peer (RW RDMA overflow blade) listening on {:?} (rails {:?}, cap {})",
60        local,
61        rdma.rails,
62        if rdma.max_blade_bytes == 0 {
63            "unlimited".to_string()
64        } else {
65            format!(
66                "{:.1} GiB",
67                rdma.max_blade_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
68            )
69        },
70    );
71    // 2026-09-25: Paging arenas are never freed, so without a ceiling every
72    // new (kind, blob_bytes) adds RAM that stays mapped.
73    if rdma.max_blade_bytes == 0 && rdma.swap_dir.is_some() {
74        tracing::warn!(
75            "cache-peer paging registry active with NO blade ceiling (--max-blade-gb 0 = \
76             unlimited): each new (kind, shape) arena pins RDMA-registered RAM without bound. \
77             Set --max-blade-gb <G> to cap total memlocked blade RAM."
78        );
79    }
80    for conn in listener.incoming() {
81        let stream = match conn {
82            Ok(s) => s,
83            Err(e) => {
84                tracing::warn!("cache-peer accept error: {e}");
85                continue;
86            }
87        };
88        let rdma = rdma.clone();
89        let ledger = ledger.clone();
90        std::thread::spawn(move || {
91            if let Err(e) = handle_conn(stream, &rdma, &ledger) {
92                tracing::warn!("cache-peer connection ended: {e}");
93            }
94        });
95    }
96    Ok(())
97}
98
99#[cfg(not(metrale_rdma_verbs))]
100fn handle_conn(
101    _stream: TcpStream,
102    _rdma: &RdmaConfig,
103    _ledger: &std::sync::Arc<crate::blade_cap::CommitLedger>,
104) -> Result<()> {
105    bail!("cache-peer needs a build with rdma-core (metrale_rdma_verbs)");
106}
107
108#[cfg(metrale_rdma_verbs)]
109fn handle_conn(
110    mut stream: TcpStream,
111    rdma: &RdmaConfig,
112    ledger: &std::sync::Arc<crate::blade_cap::CommitLedger>,
113) -> Result<()> {
114    use super::registry::{self, Mmap, SharedPaging};
115    use metrale_gpu_sys::verbs::Verbs;
116    use metrale_gpu_sys::wire::{CacheServerParams, STATUS_OK, VerbsClientParams};
117    use std::io::{Read, Write};
118    stream.set_nodelay(true).ok();
119
120    // 2026-09-25: Every client sends
121    // `[u64 PAGING_MAGIC_V2][u8 kind][u64 arena_bytes][u64 blob_bytes]`, and
122    // `parse_paging_header` rejects any other first word. blob_bytes > 0
123    // selects paging over the shared (kind, blob_bytes) arena; 0 selects RAW
124    // mode, a per-connection arena that the client allocates in.
125    let mut b8 = [0u8; 8];
126    stream.read_exact(&mut b8).context("read paging magic")?;
127    let first = u64::from_le_bytes(b8);
128    let (kind, arena_bytes, blob) = crate::snapshot_swap::parse_paging_header(first, &mut stream)?;
129    let total = arena_bytes as usize;
130    let blob = blob as usize;
131    // 2026-09-25: The ledger has no ceiling unless one is configured, so the
132    // arena size is bounded here too.
133    if total == 0 || total > (1usize << 42) {
134        bail!("implausible blade arena size: {total}");
135    }
136    let paging: Option<(u8, usize)> = if blob == 0 {
137        None
138    } else {
139        if !total.is_multiple_of(blob) {
140            bail!("paging: arena_bytes {total} not a multiple of blob_bytes {blob}");
141        }
142        // 2026-09-25: Refused before the rail handshake.
143        if rdma.swap_dir.is_none() {
144            bail!("paging client but peer started without --swap-dir; refusing");
145        }
146        Some((kind.0, blob))
147    };
148    let mut b1 = [0u8; 1];
149    stream.read_exact(&mut b1).context("read n_rails")?;
150    let n_rails = b1[0] as usize;
151    if n_rails == 0 || n_rails > rdma.rails.len() {
152        bail!(
153            "client asked for {n_rails} rails; peer has {}",
154            rdma.rails.len()
155        );
156    }
157
158    // 2026-09-25: RAW mode maps and charges an arena per connection. Paging
159    // uses the shared arena of (kind, blob_bytes), charged once when it was
160    // created, so every client's rails point at the same slots.
161    let pid = std::process::id();
162    let shared: Option<std::sync::Arc<SharedPaging>> = match paging {
163        Some((kind, blob)) => Some(registry::get_or_init_shared_paging(
164            rdma, kind, total, blob, ledger,
165        )?),
166        None => None,
167    };
168    // 2026-09-25: RAW mode only; the shared arena's reservation lives in the
169    // registry.
170    let local: Option<(crate::blade_cap::Reservation, Mmap)> = if shared.is_none() {
171        let reservation = ledger.try_reserve(total as u64).context("kv blade cap")?;
172        let arena = Mmap::anon(total).context("mmap kv blade arena")?;
173        Some((reservation, arena))
174    } else {
175        None
176    };
177    let (arena_base, arena_len): (*mut libc::c_void, usize) = match (&shared, &local) {
178        (Some(sh), _) => (sh.arena.addr, sh.arena.len),
179        (None, Some((_, arena))) => (arena.addr, arena.len),
180        _ => unreachable!("exactly one of shared/local is set"),
181    };
182    // 2026-09-25: Each rail registers the same arena: n rails make n
183    // registrations of one memory range.
184    let mut rails: Vec<Verbs> = Vec::with_capacity(n_rails);
185    let mut rkeys: Vec<u32> = Vec::with_capacity(n_rails);
186    for (i, (dev, gid)) in rdma.rails.iter().take(n_rails).enumerate() {
187        let psn = (0x5a5a5a ^ pid ^ ((i as u32) << 20)) & 0xff_ffff;
188        let mut v = Verbs::create(dev, *gid, psn)?;
189        // 2026-09-25: SAFETY: the arena (shared or local) outlives every rail;
190        // see the drops at the end.
191        let keys = unsafe { v.reg_mr_rw(arena_base as *mut _, arena_len)? };
192        rkeys.push(keys.rkey);
193        rails.push(v);
194    }
195
196    stream.write_all(&[n_rails as u8]).context("send n_rails")?;
197    for (v, rkey) in rails.iter().zip(&rkeys) {
198        CacheServerParams {
199            qpn: v.qpn(),
200            psn: v.psn(),
201            gid: v.gid(),
202            base_addr: arena_base as u64,
203            rkey: *rkey,
204        }
205        .write_to(&mut stream)
206        .context("send kv server params")?;
207    }
208
209    stream.read_exact(&mut b1).context("read client n_rails")?;
210    if b1[0] as usize != n_rails {
211        bail!("client rail count mismatch");
212    }
213    for v in rails.iter_mut() {
214        let cp = VerbsClientParams::read_from(&mut stream).context("read kv client params")?;
215        v.connect(cp.qpn, cp.psn, &cp.gid)?;
216    }
217    stream
218        .write_all(&[STATUS_OK])
219        .context("send kv ready ack")?;
220    let mode = if paging.is_some() {
221        "paging"
222    } else {
223        "raw one-sided"
224    };
225    tracing::info!(
226        "cache-peer client connected: kind {}, {n_rails} rail(s), {:.1} GiB RW blade ({mode})",
227        kind.0,
228        total as f64 / (1024.0 * 1024.0 * 1024.0),
229    );
230
231    if let Some(sh) = shared {
232        // 2026-09-25: Paging: control messages on this stream drive the
233        // shared residency; record bytes move over RDMA through the
234        // registrations made above.
235        tracing::info!("cache-peer PAGING client joined shared arena ({n_rails} rail(s))");
236        let r = crate::snapshot_swap::run_paging_loop_shared(&mut stream, &sh.residency);
237        drop(rails);
238        return r;
239    }
240
241    // 2026-09-25: RAW mode: the client allocates in the arena; the peer
242    // waits for hangup.
243    let mut sink = [0u8; 8];
244    loop {
245        match stream.read(&mut sink) {
246            Ok(0) => break,
247            Ok(_) => {}
248            Err(_) => break,
249        }
250    }
251    // 2026-09-25: Registrations go before the mapping they cover.
252    drop(rails);
253    drop(local);
254    Ok(())
255}