metrale_storage/snapshot_swap/
wire.rs1use std::io::{Read, Write};
22
23use anyhow::{Context, Result, bail};
24
25use super::{Residency, SlotArena, SwapStore};
26
27const PAGING_MAGIC_V1_RETIRED: u64 = 0x5041_4745_0000_0001;
29
30pub const PAGING_MAGIC_V2: u64 = 0x5041_4745_0000_0002;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub struct PagingKind(pub u8);
39impl PagingKind {
40 pub const SSM: PagingKind = PagingKind(0);
41 pub const KV: PagingKind = PagingKind(1);
42 pub fn is_paging_rw(self) -> bool {
44 self.0 <= 1
45 }
46}
47
48pub fn parse_paging_header<R: Read>(first: u64, r: &mut R) -> Result<(PagingKind, u64, u64)> {
53 if first == PAGING_MAGIC_V1_RETIRED {
54 bail!(
55 "paging: v1 client no longer supported (magic {first:#x}); rebuild the client — \
56 every connect now sends [u64 PAGING_MAGIC_V2][u8 kind][u64 arena_bytes][u64 blob_bytes]"
57 );
58 }
59 if first != PAGING_MAGIC_V2 {
60 bail!(
61 "paging: first u64 {first:#x} is not PAGING_MAGIC_V2 (the bare legacy total_bytes \
62 handshake was retired; RAW one-sided clients send the v2 header with \
63 blob_bytes == 0)"
64 );
65 }
66 let mut kb = [0u8; 1];
67 r.read_exact(&mut kb).context("read paging kind")?;
68 let kind = PagingKind(kb[0]);
69 if !kind.is_paging_rw() {
70 bail!(
71 "paging: unsupported kind {} (only SSM/KV ride this handshake)",
72 kb[0]
73 );
74 }
75 let mut b8 = [0u8; 8];
76 r.read_exact(&mut b8).context("read paging arena_bytes")?;
77 let arena_bytes = u64::from_le_bytes(b8);
78 r.read_exact(&mut b8).context("read paging blob_bytes")?;
79 let blob_bytes = u64::from_le_bytes(b8);
80 Ok((kind, arena_bytes, blob_bytes))
81}
82
83pub fn encode_paging_v2_header(kind: PagingKind, arena_bytes: u64, blob_bytes: u64) -> [u8; 25] {
87 let mut w = [0u8; 25];
88 w[0..8].copy_from_slice(&PAGING_MAGIC_V2.to_le_bytes());
89 w[8] = kind.0;
90 w[9..17].copy_from_slice(&arena_bytes.to_le_bytes());
91 w[17..25].copy_from_slice(&blob_bytes.to_le_bytes());
92 w
93}
94
95pub fn stripe_plan(
101 blob_bytes: usize,
102 chunk_bytes: usize,
103 n_rails: usize,
104) -> Vec<Vec<(usize, usize)>> {
105 let n = n_rails.max(1);
106 let cb = chunk_bytes.max(1);
107 let mut rails: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
108 let mut off = 0usize;
109 let mut j = 0usize;
110 while off < blob_bytes {
111 let len = cb.min(blob_bytes - off);
112 rails[j % n].push((off, len));
113 off += len;
114 j += 1;
115 }
116 rails
117}
118
119pub fn staging_chunk_bytes() -> usize {
122 std::env::var("METRALE_SSM_CHUNK_BYTES")
123 .ok()
124 .and_then(|s| s.parse::<usize>().ok())
125 .filter(|&v| v >= 4096)
126 .unwrap_or(1024 * 1024)
127}
128pub fn staging_depth() -> usize {
131 std::env::var("METRALE_SSM_PIPELINE_DEPTH")
132 .ok()
133 .and_then(|s| s.parse::<usize>().ok())
134 .unwrap_or(16)
135 .clamp(1, 128)
136}
137
138pub const OP_BYE: u8 = 0;
139pub const OP_ALLOC: u8 = 1;
140pub const OP_COMMIT: u8 = 2;
141pub const OP_GET: u8 = 3;
142pub const OP_REMOVE: u8 = 4;
143
144pub const ST_OK: u8 = 0;
145pub const ST_MISS: u8 = 1;
146pub const ST_ERR: u8 = 2;
147
148#[derive(Debug, PartialEq, Eq)]
150pub enum PagingReply {
151 Located(u64),
153 Ok,
155 Miss,
157 Err,
159 Bye,
161}
162
163pub fn dispatch<A: SlotArena, S: SwapStore>(
166 res: &mut Residency<A, S>,
167 op: u8,
168 key: u64,
169) -> PagingReply {
170 match op {
171 OP_BYE => PagingReply::Bye,
172 OP_ALLOC => match res.alloc(key) {
173 Ok(slot) => PagingReply::Located(res.slot_offset(slot)),
174 Err(e) => {
175 tracing::warn!("paging ALLOC {key:#x} failed: {e:#}");
176 PagingReply::Err
177 }
178 },
179 OP_COMMIT => match res.commit(key) {
180 Ok(()) => PagingReply::Ok,
181 Err(e) => {
182 tracing::warn!("paging COMMIT {key:#x} failed: {e:#}");
183 PagingReply::Err
184 }
185 },
186 OP_GET => match res.locate(key) {
187 Ok(Some(slot)) => PagingReply::Located(res.slot_offset(slot)),
188 Ok(None) => PagingReply::Miss,
189 Err(e) => {
190 tracing::warn!("paging GET {key:#x} failed: {e:#}");
191 PagingReply::Err
192 }
193 },
194 OP_REMOVE => {
195 res.remove(key);
196 PagingReply::Ok
197 }
198 other => {
199 tracing::warn!("paging: unknown op {other}");
200 PagingReply::Err
201 }
202 }
203}
204
205fn write_reply<W: Write>(w: &mut W, reply: &PagingReply) -> Result<()> {
206 match reply {
207 PagingReply::Located(off) => {
208 w.write_all(&[ST_OK])?;
209 w.write_all(&off.to_le_bytes())?;
210 }
211 PagingReply::Ok => w.write_all(&[ST_OK])?,
212 PagingReply::Miss => w.write_all(&[ST_MISS])?,
213 PagingReply::Err => w.write_all(&[ST_ERR])?,
214 PagingReply::Bye => {}
215 }
216 w.flush()?;
217 Ok(())
218}
219
220fn handle_paging_op<A: SlotArena, S: SwapStore>(
226 res: &mut Residency<A, S>,
227 op: u8,
228 key: u64,
229 pinned: &mut Option<u64>,
230) -> PagingReply {
231 if let Some(prev) = pinned.take() {
232 res.unpin_read(prev);
233 }
234 let reply = dispatch(res, op, key);
235 if op == OP_GET && matches!(reply, PagingReply::Located(_)) {
236 res.pin_read(key);
237 *pinned = Some(key);
238 }
239 reply
240}
241
242pub fn run_paging_loop<T: Read + Write, A: SlotArena, S: SwapStore>(
245 stream: &mut T,
246 res: &mut Residency<A, S>,
247) -> Result<()> {
248 let mut pinned: Option<u64> = None;
249 loop {
250 let mut op = [0u8; 1];
251 if stream.read_exact(&mut op).is_err() {
252 break;
253 }
254 if op[0] == OP_BYE {
255 break;
256 }
257 let mut kb = [0u8; 8];
258 stream.read_exact(&mut kb).context("read paging key")?;
259 let key = u64::from_le_bytes(kb);
260 let reply = handle_paging_op(res, op[0], key, &mut pinned);
261 write_reply(stream, &reply)?;
262 }
263 if let Some(pk) = pinned {
264 res.unpin_read(pk);
265 }
266 Ok(())
267}
268
269fn send_req<T: Write>(s: &mut T, op: u8, key: u64) -> Result<()> {
274 let mut buf = [0u8; 9];
275 buf[0] = op;
276 buf[1..].copy_from_slice(&key.to_le_bytes());
277 s.write_all(&buf)?;
278 s.flush()?;
279 Ok(())
280}
281
282fn read_status<T: Read>(s: &mut T) -> Result<u8> {
283 let mut st = [0u8; 1];
284 s.read_exact(&mut st).context("read paging status")?;
285 Ok(st[0])
286}
287
288fn read_offset<T: Read>(s: &mut T) -> Result<u64> {
289 let mut b = [0u8; 8];
290 s.read_exact(&mut b).context("read paging offset")?;
291 Ok(u64::from_le_bytes(b))
292}
293
294pub fn client_alloc<T: Read + Write>(s: &mut T, key: u64) -> Result<u64> {
297 send_req(s, OP_ALLOC, key)?;
298 match read_status(s)? {
299 ST_OK => read_offset(s),
300 st => bail!("paging ALLOC {key:#x} refused (status {st})"),
301 }
302}
303
304pub fn client_commit<T: Read + Write>(s: &mut T, key: u64) -> Result<()> {
306 send_req(s, OP_COMMIT, key)?;
307 match read_status(s)? {
308 ST_OK => Ok(()),
309 st => bail!("paging COMMIT {key:#x} failed (status {st})"),
310 }
311}
312
313pub fn client_get<T: Read + Write>(s: &mut T, key: u64) -> Result<Option<u64>> {
315 send_req(s, OP_GET, key)?;
316 match read_status(s)? {
317 ST_OK => Ok(Some(read_offset(s)?)),
318 ST_MISS => Ok(None),
319 st => bail!("paging GET {key:#x} error (status {st})"),
320 }
321}
322
323pub fn client_remove<T: Read + Write>(s: &mut T, key: u64) -> Result<()> {
325 send_req(s, OP_REMOVE, key)?;
326 match read_status(s)? {
327 ST_OK => Ok(()),
328 st => bail!("paging REMOVE {key:#x} failed (status {st})"),
329 }
330}
331
332pub fn client_bye<T: Write>(s: &mut T) -> Result<()> {
334 send_req(s, OP_BYE, 0)
335}
336
337pub fn run_paging_loop_shared<T: Read + Write, A: SlotArena, S: SwapStore>(
342 stream: &mut T,
343 res: &std::sync::Mutex<Residency<A, S>>,
344) -> Result<()> {
345 let mut pinned: Option<u64> = None;
348 loop {
349 let mut op = [0u8; 1];
350 if stream.read_exact(&mut op).is_err() {
351 break;
352 }
353 if op[0] == OP_BYE {
354 break;
355 }
356 let mut kb = [0u8; 8];
357 stream.read_exact(&mut kb).context("read paging key")?;
358 let key = u64::from_le_bytes(kb);
359 let reply = {
360 let mut g = res.lock().expect("shared residency mutex poisoned");
361 handle_paging_op(&mut g, op[0], key, &mut pinned)
362 };
363 write_reply(stream, &reply)?;
364 }
365 if let Some(pk) = pinned {
366 res.lock()
367 .expect("shared residency mutex poisoned")
368 .unpin_read(pk);
369 }
370 Ok(())
371}
372
373#[cfg(test)]
374#[path = "wire_tests.rs"]
375mod tests;