metrale_gpu_sys/
handshake.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The client side of the rail handshake as byte functions over
4//! `Read`/`Write` that take QP identities as `(qpn, psn, gid)` tuples, so they
5//! need no `Verbs`. `railset::RailSet` calls them; tests/transcript_golden.rs
6//! drives them against a scripted peer.
7//!
8//! Wire order (the params' integers are little-endian, see `wire`):
9//! - client: `[u8 n_rails]` (`write_n_rails`);
10//! - server, RO dialect: `[u8 n]`, then n `VerbsServerParams`
11//!   (`wire::read_server_rails`); RW dialect: `[u8 n]`, then n
12//!   `CacheServerParams` (`read_rw_server_params`);
13//! - client: `[u8 n_rails]` a second time, then n `VerbsClientParams`
14//!   (`write_client_params`);
15//! - server: `[u8 ack]`, which must be `STATUS_OK` (`read_ack`).
16//!
17//! Owner: metrale-gpu-sys.
18//! Invariants: none beyond the types.
19
20use anyhow::{Context, Result, bail};
21use std::io::{Read, Write};
22
23use crate::wire::{CacheServerParams, STATUS_OK, VerbsClientParams};
24
25/// 2026-09-26: Write the rail count as one byte.
26pub fn write_n_rails<W: Write>(w: &mut W, n: usize) -> Result<()> {
27    w.write_all(&[n as u8]).context("send n_rails")?;
28    Ok(())
29}
30
31/// 2026-09-26: RW dialect (the KV and snapshot tiers): read the peer's
32/// `[u8 n]`, which must equal `want`, then `want` `CacheServerParams`. Unlike
33/// `wire::read_server_rails`, the count is not limited to 1..=8.
34pub fn read_rw_server_params<R: Read>(
35    r: &mut R,
36    want: usize,
37    peer: &str,
38) -> Result<Vec<CacheServerParams>> {
39    let mut b1 = [0u8; 1];
40    if let Err(e) = r.read_exact(&mut b1) {
41        // 2026-09-26: A clean EOF here means the peer refused the client: the
42        // cache peer logs the reason (e.g. "paging blade cap") and closes, and
43        // the wire has no error frame to carry it.
44        if e.kind() == std::io::ErrorKind::UnexpectedEof {
45            bail!(
46                "{peer} closed the connection during the rail handshake without sending rail \
47                 params. The peer REJECTED this client — read ITS log for the reason. Most \
48                 common: the arena this client requested exceeds the peer's --max-blade-gb \
49                 cap (peer logs \"paging blade cap\"); also possible: a blob_bytes/kind \
50                 mismatch against an arena a prior client already fixed."
51            );
52        }
53        return Err(e).context("read peer n_rails");
54    }
55    if b1[0] as usize != want {
56        bail!("{peer} granted {} rails, wanted {want}", b1[0]);
57    }
58    let mut server = Vec::with_capacity(want);
59    for _ in 0..want {
60        server
61            .push(CacheServerParams::read_from(r).with_context(|| format!("read {peer} params"))?);
62    }
63    Ok(server)
64}
65
66/// 2026-09-26: Write the rail count again, then each rail's
67/// `VerbsClientParams`.
68pub fn write_client_params<W: Write>(w: &mut W, ids: &[(u32, u32, [u8; 16])]) -> Result<()> {
69    w.write_all(&[ids.len() as u8])
70        .context("send client n_rails")?;
71    for &(qpn, psn, gid) in ids {
72        VerbsClientParams { qpn, psn, gid }
73            .write_to(w)
74            .context("send verbs client params")?;
75    }
76    Ok(())
77}
78
79/// 2026-09-26: Read the peer's one-byte ack, which must be `STATUS_OK`.
80pub fn read_ack<R: Read>(r: &mut R, peer: &str) -> Result<()> {
81    let mut ack = [0u8; 1];
82    r.read_exact(&mut ack).context("read verbs ready ack")?;
83    if ack[0] != STATUS_OK {
84        bail!("{peer} refused connection (ack {})", ack[0]);
85    }
86    Ok(())
87}