spark_storage/cache_peer/
server_impl.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//
3// Accept loop + per-connection lifecycle for the RW blade: the v2-only
4// handshake parse (paging vs RAW one-sided mode, selected by `blob_bytes`),
5// the server-side rail handshake holding the crate's SINGLE `reg_mr_rw` call
6// site (the access flag is a security invariant and stays AT the call site —
7// census-pinned by `tests/reg_mr_flag_audit.rs`), and the two data planes:
8// the shared paging control loop vs the RAW idle-until-hangup blade.
9
10use std::net::{TcpListener, TcpStream, ToSocketAddrs};
11
12use anyhow::{Context, Result, bail};
13
14/// RDMA rail selection for the blade. One `(dev, gid_idx)` per CX7 adapter;
15/// a client requests N rails and the peer registers its arena on each so the
16/// client can stripe traffic across both adapters (~1.75x aggregate on GB10).
17#[derive(Clone, Debug)]
18pub struct RdmaConfig {
19    /// `(device, gid_idx)` per rail, in link order (rail 0 = .178, 1 = .177).
20    pub rails: Vec<(String, u32)>,
21    /// Ceiling on total committed (registered) blade RAM across all
22    /// concurrent connections, in bytes. `0` = unlimited (the default).
23    pub max_blade_bytes: u64,
24    /// Directory for NVMe swap files backing paging-mode connections
25    ///. `None` = paging clients are refused (RAM-only). When set, a
26    /// paging connection's RDMA arena becomes a page-cache over an O_DIRECT
27    /// swap file here, bounded by `swap_cap_bytes`.
28    pub swap_dir: Option<std::path::PathBuf>,
29    /// Disk cap for the paging swap file, in bytes: bounds the on-disk
30    /// snapshot count (coldest dropped when full → later GET misses →
31    /// recompute). 0 = unbounded. Default 50 GiB (operator sanity limit).
32    /// In the multi-arena registry this is the SHARED ceiling carved across
33    /// kinds unless a kind has a `per_kind_swap_cap_bytes` override.
34    pub swap_cap_bytes: u64,
35    /// Per-`PagingKind` disk-cap overrides (`kind.0 → bytes`). When a kind
36    /// is present here, its arena gets this FIXED disk budget instead of
37    /// carving from the shared `swap_cap_bytes` remainder — so one kind
38    /// (e.g. KV) can't starve another (e.g. SSM snapshots). 0 = unbounded
39    /// for that kind. Set via `--swap-cap-gb-<kind>`.
40    pub per_kind_swap_cap_bytes: std::collections::HashMap<u8, u64>,
41}
42
43impl Default for RdmaConfig {
44    fn default() -> Self {
45        Self {
46            rails: vec![("roceP2p1s0f1".into(), 3), ("rocep1s0f1".into(), 3)],
47            max_blade_bytes: 0,
48            swap_dir: None,
49            swap_cap_bytes: 50 * 1024 * 1024 * 1024,
50            per_kind_swap_cap_bytes: std::collections::HashMap::new(),
51        }
52    }
53}
54
55/// Serve a KV overflow blade on `addr` until interrupted. One thread per
56/// connection; each connection gets its own RW arena sized by the client.
57pub fn serve<A: ToSocketAddrs>(addr: A, rdma: RdmaConfig) -> Result<()> {
58    let listener = TcpListener::bind(addr).context("bind cache-peer listener")?;
59    let local = listener.local_addr().ok();
60    // One process-global ledger, shared by every connection thread; a
61    // connection reserves its arena size before it maps/registers any RAM.
62    let ledger = std::sync::Arc::new(crate::blade_cap::CommitLedger::new(rdma.max_blade_bytes));
63    tracing::info!(
64        "cache-peer (RW RDMA overflow blade) listening on {:?} (rails {:?}, cap {})",
65        local,
66        rdma.rails,
67        if rdma.max_blade_bytes == 0 {
68            "unlimited".to_string()
69        } else {
70            format!(
71                "{:.1} GiB",
72                rdma.max_blade_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
73            )
74        },
75    );
76    // Explicit memlock ceiling: paging arenas are anon-mmap'd AND
77    // RDMA-registered (pinned = memlocked). With no `--max-blade-gb` the
78    // registry can pin unbounded RAM across (kind, shape) arenas as clients
79    // of new shapes connect — on a shared box that can exhaust host RAM /
80    // hit the memlock rlimit. Warn so the operator sets an explicit cap.
81    if rdma.max_blade_bytes == 0 && rdma.swap_dir.is_some() {
82        tracing::warn!(
83            "cache-peer paging registry active with NO blade ceiling (--max-blade-gb 0 = \
84             unlimited): each new (kind, shape) arena pins RDMA-registered RAM without bound. \
85             Set --max-blade-gb <G> to cap total memlocked blade RAM."
86        );
87    }
88    for conn in listener.incoming() {
89        let stream = match conn {
90            Ok(s) => s,
91            Err(e) => {
92                tracing::warn!("cache-peer accept error: {e}");
93                continue;
94            }
95        };
96        let rdma = rdma.clone();
97        let ledger = ledger.clone();
98        std::thread::spawn(move || {
99            if let Err(e) = handle_conn(stream, &rdma, &ledger) {
100                tracing::warn!("cache-peer connection ended: {e}");
101            }
102        });
103    }
104    Ok(())
105}
106
107#[cfg(not(atlas_rdma_verbs))]
108fn handle_conn(
109    _stream: TcpStream,
110    _rdma: &RdmaConfig,
111    _ledger: &std::sync::Arc<crate::blade_cap::CommitLedger>,
112) -> Result<()> {
113    bail!("cache-peer needs a build with rdma-core (atlas_rdma_verbs)");
114}
115
116#[cfg(atlas_rdma_verbs)]
117fn handle_conn(
118    mut stream: TcpStream,
119    rdma: &RdmaConfig,
120    ledger: &std::sync::Arc<crate::blade_cap::CommitLedger>,
121) -> Result<()> {
122    use super::registry::{self, Mmap, SharedPaging};
123    use atlas_rdma::verbs::Verbs;
124    use atlas_rdma::wire::{CacheServerParams, STATUS_OK, VerbsClientParams};
125    use std::io::{Read, Write};
126    stream.set_nodelay(true).ok();
127
128    // 1. Client handshake (v2-only): EVERY client sends
129    //    `[u64 PAGING_MAGIC_V2][u8 kind][u64 arena_bytes][u64 blob_bytes]`.
130    //    blob_bytes > 0 → paging mode (peer-owned residency over the shared
131    //    per-(kind, blob) registry arena). blob_bytes == 0 → RAW one-sided
132    //    mode: a per-connection anonymous arena with a client-owned allocator
133    //    (the legacy data plane, selected explicitly).
134    //    `parse_paging_header` rejects the retired v1 magic and any bare
135    //    legacy total_bytes with a legible diagnostic.
136    let mut b8 = [0u8; 8];
137    stream.read_exact(&mut b8).context("read paging magic")?;
138    let first = u64::from_le_bytes(b8);
139    let (kind, arena_bytes, blob) = crate::snapshot_swap::parse_paging_header(first, &mut stream)?;
140    let total = arena_bytes as usize;
141    let blob = blob as usize;
142    // Explicit arena sanity bound. Pre-Step-C the `1<<42` check did double
143    // duty (legacy-vs-magic dispatch AND size sanity); the dispatch role is
144    // gone but the bound stays — arena size must never be limited only by
145    // the (default-unlimited, warn-only) blade ledger.
146    if total == 0 || total > (1usize << 42) {
147        bail!("implausible blade arena size: {total}");
148    }
149    let paging: Option<(u8, usize)> = if blob == 0 {
150        None // RAW one-sided mode
151    } else {
152        if !total.is_multiple_of(blob) {
153            bail!("paging: arena_bytes {total} not a multiple of blob_bytes {blob}");
154        }
155        // Reject BEFORE the rail handshake when this peer has no swap
156        // dir — the client's connect_paging then errors cleanly and
157        // falls back to the bounded/host-RAM tier.
158        if rdma.swap_dir.is_none() {
159            bail!("paging client but peer started without --swap-dir; refusing");
160        }
161        Some((kind.0, blob))
162    };
163    let mut b1 = [0u8; 1];
164    stream.read_exact(&mut b1).context("read n_rails")?;
165    let n_rails = b1[0] as usize;
166    if n_rails == 0 || n_rails > rdma.rails.len() {
167        bail!(
168            "client asked for {n_rails} rails; peer has {}",
169            rdma.rails.len()
170        );
171    }
172
173    // Acquire the arena to register. RAW mode: a per-connection anonymous
174    // mapping, charged per-conn. Paging: the process-global SHARED
175    // arena (charged ONCE at init) so every client's QPs point at the SAME
176    // physical slots → a snapshot PUT by one client is GET-able by another.
177    let pid = std::process::id();
178    let shared: Option<std::sync::Arc<SharedPaging>> = match paging {
179        Some((kind, blob)) => Some(registry::get_or_init_shared_paging(
180            rdma, kind, total, blob, ledger,
181        )?),
182        None => None,
183    };
184    // Per-connection arena + blade reservation (RAW mode only), kept alive
185    // until teardown; the shared arena's reservation lives in the static.
186    let local: Option<(crate::blade_cap::Reservation, Mmap)> = if shared.is_none() {
187        let reservation = ledger.try_reserve(total as u64).context("kv blade cap")?;
188        let arena = Mmap::anon(total).context("mmap kv blade arena")?;
189        Some((reservation, arena))
190    } else {
191        None
192    };
193    let (arena_base, arena_len): (*mut libc::c_void, usize) = match (&shared, &local) {
194        (Some(sh), _) => (sh.arena.addr, sh.arena.len),
195        (None, Some((_, arena))) => (arena.addr, arena.len),
196        _ => unreachable!("exactly one of shared/local is set"),
197    };
198    // Register the arena ONCE per rail (each device its own PD/rkey; shared
199    // refcounted pages, so N rails cost N MR handles + rkeys, not N× RAM).
200    let mut rails: Vec<Verbs> = Vec::with_capacity(n_rails);
201    let mut rkeys: Vec<u32> = Vec::with_capacity(n_rails);
202    for (i, (dev, gid)) in rdma.rails.iter().take(n_rails).enumerate() {
203        let psn = (0x5a5a5a ^ pid ^ ((i as u32) << 20)) & 0xff_ffff;
204        let mut v = Verbs::create(dev, *gid, psn)?;
205        // SAFETY: the arena (shared or local) outlives every rail below.
206        let keys = unsafe { v.reg_mr_rw(arena_base as *mut _, arena_len)? };
207        rkeys.push(keys.rkey);
208        rails.push(v);
209    }
210
211    // 2. Publish rail count + each rail's QP + rkey (shared base).
212    stream.write_all(&[n_rails as u8]).context("send n_rails")?;
213    for (v, rkey) in rails.iter().zip(&rkeys) {
214        CacheServerParams {
215            qpn: v.qpn(),
216            psn: v.psn(),
217            gid: v.gid(),
218            base_addr: arena_base as u64,
219            rkey: *rkey,
220        }
221        .write_to(&mut stream)
222        .context("send kv server params")?;
223    }
224
225    // 3-4. Learn each client rail's QP, connect, ack.
226    stream.read_exact(&mut b1).context("read client n_rails")?;
227    if b1[0] as usize != n_rails {
228        bail!("client rail count mismatch");
229    }
230    for v in rails.iter_mut() {
231        let cp = VerbsClientParams::read_from(&mut stream).context("read kv client params")?;
232        v.connect(cp.qpn, cp.psn, &cp.gid)?;
233    }
234    stream
235        .write_all(&[STATUS_OK])
236        .context("send kv ready ack")?;
237    let mode = if paging.is_some() {
238        "paging"
239    } else {
240        "raw one-sided"
241    };
242    tracing::info!(
243        "cache-peer client connected: kind {}, {n_rails} rail(s), {:.1} GiB RW blade ({mode})",
244        kind.0,
245        total as f64 / (1024.0 * 1024.0 * 1024.0),
246    );
247
248    // 5. Data plane.
249    if let Some(sh) = shared {
250        // Paging mode: drive the SHARED residency — a snapshot PUT by
251        // one client is GET-able by another (cross-connection warm cache).
252        // Bytes move one-sided over RDMA into/out of the shared arena slots;
253        // only tiny [op][key] control messages cross this TCP stream. The MR
254        // is never re-registered — swap happens under the stable rkey.
255        tracing::info!("cache-peer PAGING client joined shared arena ({n_rails} rail(s))");
256        let r = crate::snapshot_swap::run_paging_loop_shared(&mut stream, &sh.residency);
257        drop(rails); // dereg this conn's MRs; the shared arena stays mapped
258        return r;
259    }
260
261    // RAW one-sided blade (v2, blob_bytes == 0): the client owns allocation
262    // against the fixed arena; the peer just idles until hangup.
263    let mut sink = [0u8; 8];
264    loop {
265        match stream.read(&mut sink) {
266            Ok(0) => break,
267            Ok(_) => {}
268            Err(_) => break,
269        }
270    }
271    // Dereg (rails) before unmap (arena): drop rails first.
272    drop(rails);
273    drop(local);
274    Ok(())
275}