armdb 0.7.0

sharded bitcask key-value storage optimized for NVMe
Documentation
//! Resumable wire-frame reader shared by both replication stacks.
//!
//! The free `read_frame` helpers in `replication::protocol` and
//! `fixed_replication::protocol` issue three sequential `read_exact` calls;
//! on a socket with `SO_RCVTIMEO` a timeout mid-frame silently discards the
//! bytes already consumed and desynchronizes the stream. Any code path that
//! keeps reading from the same connection after a `WouldBlock`/`TimedOut`
//! must therefore use [`FrameReader`], which buffers partial frames across
//! timeouts. Blocking handshake paths keep using the free `read_frame`.

use core::marker::PhantomData;
use std::io::{self, Read};

/// Protocol parameter: type-byte validation plus the payload size cap.
pub trait FrameKind: Sized + Copy {
    const MAX_PAYLOAD: usize;
    fn from_u8(b: u8) -> Option<Self>;
}

/// Wire frame: `[type:u8][len:u32 LE][payload:len bytes]`.
#[derive(Debug)]
pub struct GenericFrame<M> {
    pub msg_type: M,
    pub payload: Vec<u8>,
}

/// Stateful, resumable frame reader for non-blocking / read-timeout sockets.
///
/// Accumulates bytes across `WouldBlock`/`TimedOut` and only yields a frame
/// once a full `[type][len][payload]` is present, so a frame split by a
/// timeout is reassembled exactly on the next call.
pub struct FrameReader<M: FrameKind> {
    /// Bytes read from the socket but not yet consumed into a completed frame.
    pending: Vec<u8>,
    _marker: PhantomData<M>,
}

// Manual impl: derive(Default) would add a spurious `M: Default` bound via
// PhantomData.
impl<M: FrameKind> Default for FrameReader<M> {
    fn default() -> Self {
        Self::new()
    }
}

impl<M: FrameKind> FrameReader<M> {
    /// How many bytes to request per underlying `read`. Sized so a large
    /// frame needs few syscalls without an oversized per-call allocation.
    const READ_CHUNK: usize = 64 * 1024;

    pub fn new() -> Self {
        Self {
            pending: Vec::new(),
            _marker: PhantomData,
        }
    }

    /// Bytes buffered toward the in-progress frame.
    ///
    /// A growing value across two timeout returns proves the peer made
    /// progress (liveness signal). Only meaningful between resumable
    /// (`WouldBlock`/`TimedOut`) errors — after a terminal error the reader
    /// must be discarded together with the connection.
    pub fn buffered(&self) -> usize {
        self.pending.len()
    }

    /// Read one frame, resuming from any partially-buffered bytes.
    ///
    /// On a `WouldBlock`/`TimedOut` mid-frame the error is returned *without*
    /// discarding buffered bytes — call again later to continue. After any
    /// other error (EOF, `InvalidData`, transport errors) the reader state is
    /// unspecified: discard it together with the connection.
    pub fn read_frame(&mut self, r: &mut impl Read) -> io::Result<GenericFrame<M>> {
        loop {
            if let Some(frame) = self.try_parse()? {
                return Ok(frame);
            }
            let old_len = self.pending.len();
            self.pending.resize(old_len + Self::READ_CHUNK, 0);
            match r.read(&mut self.pending[old_len..]) {
                Ok(0) => {
                    self.pending.truncate(old_len);
                    return Err(io::Error::new(
                        io::ErrorKind::UnexpectedEof,
                        "peer closed connection mid-frame",
                    ));
                }
                Ok(n) => {
                    self.pending.truncate(old_len + n);
                    // Loop back to try_parse with the new bytes.
                }
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {
                    self.pending.truncate(old_len);
                    // Retry.
                }
                Err(e) => {
                    self.pending.truncate(old_len);
                    return Err(e);
                }
            }
        }
    }

    /// Parse a full frame out of `pending` if one is buffered. Returns
    /// `Ok(None)` when more bytes are needed. The type byte is validated as
    /// soon as it is buffered (parity with the free `read_frame`, which
    /// errors after one byte), and an oversized length errors before any
    /// payload wait.
    fn try_parse(&mut self) -> io::Result<Option<GenericFrame<M>>> {
        let Some(&type_byte) = self.pending.first() else {
            return Ok(None);
        };
        let msg_type = M::from_u8(type_byte).ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("unknown message type: {type_byte}"),
            )
        })?;
        if self.pending.len() < 5 {
            return Ok(None);
        }
        let len =
            u32::from_le_bytes(self.pending[1..5].try_into().expect("5 bytes buffered")) as usize;
        if len > M::MAX_PAYLOAD {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("frame payload too large: {len} > {}", M::MAX_PAYLOAD),
            ));
        }
        let total = 5 + len;
        if self.pending.len() < total {
            return Ok(None);
        }
        let payload = self.pending[5..total].to_vec();
        self.pending.drain(..total);
        Ok(Some(GenericFrame { msg_type, payload }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::VecDeque;

    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum TestMsg {
        Ping,
        Data,
    }

    impl FrameKind for TestMsg {
        const MAX_PAYLOAD: usize = 64;
        fn from_u8(b: u8) -> Option<Self> {
            match b {
                1 => Some(Self::Ping),
                2 => Some(Self::Data),
                _ => None,
            }
        }
    }

    /// Scripted reader: each `read` call pops the next item — either bytes
    /// (copied into the buffer whole) or an error. An exhausted script reads
    /// as EOF (`Ok(0)`).
    struct ScriptReader {
        script: VecDeque<io::Result<Vec<u8>>>,
    }

    impl ScriptReader {
        fn new(script: Vec<io::Result<Vec<u8>>>) -> Self {
            Self {
                script: script.into(),
            }
        }
    }

    impl Read for ScriptReader {
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
            match self.script.pop_front() {
                Some(Ok(bytes)) => {
                    assert!(bytes.len() <= buf.len(), "script chunk exceeds read buffer");
                    buf[..bytes.len()].copy_from_slice(&bytes);
                    Ok(bytes.len())
                }
                Some(Err(e)) => Err(e),
                None => Ok(0),
            }
        }
    }

    fn would_block() -> io::Error {
        io::Error::new(io::ErrorKind::WouldBlock, "wb")
    }

    fn frame_bytes(t: u8, payload: &[u8]) -> Vec<u8> {
        let mut v = vec![t];
        v.extend_from_slice(&(payload.len() as u32).to_le_bytes());
        v.extend_from_slice(payload);
        v
    }

    #[test]
    fn resumes_across_timeouts_at_every_boundary() {
        let wire = frame_bytes(2, b"hello"); // 10 bytes: 1 + 4 + 5
        // Split points: after the type byte, inside the length, inside payload.
        for split in [1usize, 3, 7] {
            let (a, b) = wire.split_at(split);
            let mut r = ScriptReader::new(vec![Ok(a.to_vec()), Err(would_block()), Ok(b.to_vec())]);
            let mut fr = FrameReader::<TestMsg>::new();
            let err = fr.read_frame(&mut r).unwrap_err();
            assert_eq!(err.kind(), io::ErrorKind::WouldBlock, "split={split}");
            assert_eq!(fr.buffered(), split, "split={split}");
            let frame = fr.read_frame(&mut r).unwrap();
            assert_eq!(frame.msg_type, TestMsg::Data);
            assert_eq!(frame.payload, b"hello");
            assert_eq!(fr.buffered(), 0);
        }
    }

    #[test]
    fn empty_payload_frame() {
        let mut r = ScriptReader::new(vec![Ok(frame_bytes(1, b""))]);
        let mut fr = FrameReader::<TestMsg>::new();
        let frame = fr.read_frame(&mut r).unwrap();
        assert_eq!(frame.msg_type, TestMsg::Ping);
        assert!(frame.payload.is_empty());
        assert_eq!(fr.buffered(), 0);
    }

    #[test]
    fn two_frames_in_one_chunk() {
        let mut wire = frame_bytes(1, b"");
        wire.extend_from_slice(&frame_bytes(2, b"xy"));
        let mut r = ScriptReader::new(vec![Ok(wire)]);
        let mut fr = FrameReader::<TestMsg>::new();
        let f1 = fr.read_frame(&mut r).unwrap();
        assert_eq!(f1.msg_type, TestMsg::Ping);
        // The second frame is already fully buffered in pending — it is
        // read without any new syscalls.
        let f2 = fr.read_frame(&mut r).unwrap();
        assert_eq!(f2.msg_type, TestMsg::Data);
        assert_eq!(f2.payload, b"xy");
        assert_eq!(fr.buffered(), 0);
    }

    #[test]
    fn garbage_type_byte_fails_immediately() {
        // Only ONE byte delivered, then silence: early validation (R3) must
        // reject it without waiting for a full 5-byte header.
        let mut r = ScriptReader::new(vec![Ok(vec![0xEE])]);
        let mut fr = FrameReader::<TestMsg>::new();
        let err = fr.read_frame(&mut r).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("unknown message type"));
    }

    #[test]
    fn oversized_len_fails_before_payload() {
        let mut header = vec![1u8];
        header.extend_from_slice(&65u32.to_le_bytes()); // MAX_PAYLOAD = 64
        let mut r = ScriptReader::new(vec![Ok(header)]);
        let mut fr = FrameReader::<TestMsg>::new();
        let err = fr.read_frame(&mut r).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("frame payload too large"));
    }

    #[test]
    fn eof_mid_frame() {
        // Valid type byte, then EOF (exhausted script → Ok(0)).
        let mut r = ScriptReader::new(vec![Ok(vec![1u8])]);
        let mut fr = FrameReader::<TestMsg>::new();
        let err = fr.read_frame(&mut r).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
    }

    #[test]
    fn interrupted_is_retried_internally() {
        let mut r = ScriptReader::new(vec![
            Err(io::Error::new(io::ErrorKind::Interrupted, "eintr")),
            Ok(frame_bytes(1, b"")),
        ]);
        let mut fr = FrameReader::<TestMsg>::new();
        let frame = fr.read_frame(&mut r).unwrap();
        assert_eq!(frame.msg_type, TestMsg::Ping);
    }

    #[test]
    fn default_and_debug_contracts() {
        // R5: Default (parity with the old fixed FrameReader's
        // derive(Default)) and Debug on the frame (parity with the old
        // Frame structs' derive(Debug)).
        let fr = FrameReader::<TestMsg>::default();
        assert_eq!(fr.buffered(), 0);
        let frame = GenericFrame {
            msg_type: TestMsg::Ping,
            payload: vec![1],
        };
        let dbg = format!("{frame:?}");
        assert!(dbg.contains("Ping"));
    }
}