ursula-stream 0.5.0-patch1

Durable Streams state machine for Ursula: bucket and stream commands, events, and offset bookkeeping.
Documentation
use serde::Deserialize;
use serde::Serialize;
use setsum::Setsum;
use ursula_shard::BucketStreamId;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StreamIntegritySnapshot {
    pub live_setsum: String,
    pub evicted_setsum: String,
    pub total_setsum: String,
    pub live_start_offset: u64,
    pub tail_offset: u64,
    pub live_records: u64,
    pub evicted_records: u64,
    pub total_records: u64,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct StreamIntegrity {
    live: Setsum,
    total: Setsum,
    total_records: u64,
}

impl StreamIntegrity {
    pub(crate) fn append_payload(
        &mut self,
        stream_id: &BucketStreamId,
        start_offset: u64,
        end_offset: u64,
        payload: &[u8],
    ) {
        let record = record_setsum(stream_id, start_offset, end_offset, b"inline", &[payload]);
        self.append_record(start_offset, end_offset, record);
    }

    pub(crate) fn append_external(
        &mut self,
        stream_id: &BucketStreamId,
        start_offset: u64,
        end_offset: u64,
        s3_path: &str,
        object_size: u64,
    ) {
        let object_size = object_size.to_le_bytes();
        let record = record_setsum(stream_id, start_offset, end_offset, b"external", &[
            s3_path.as_bytes(),
            &object_size,
        ]);
        self.append_record(start_offset, end_offset, record);
    }

    pub(crate) fn evict_before(&mut self, retained_offset: u64) {
        let _ = retained_offset;
    }

    pub(crate) fn snapshot(
        &self,
        live_start_offset: u64,
        tail_offset: u64,
    ) -> StreamIntegritySnapshot {
        StreamIntegritySnapshot {
            live_setsum: self.live.hexdigest(),
            evicted_setsum: Setsum::default().hexdigest(),
            total_setsum: self.total.hexdigest(),
            live_start_offset,
            tail_offset,
            live_records: self.total_records,
            evicted_records: 0,
            total_records: self.total_records,
        }
    }

    pub(crate) fn restore(snapshot: StreamIntegritySnapshot) -> Option<Self> {
        let live = Setsum::from_hexdigest(&snapshot.live_setsum)?;
        let evicted = Setsum::from_hexdigest(&snapshot.evicted_setsum)?;
        let total = Setsum::from_hexdigest(&snapshot.total_setsum)?;
        if live + evicted != total {
            return None;
        }
        Some(Self {
            live: total,
            total,
            total_records: snapshot.total_records,
        })
    }

    fn append_record(&mut self, start_offset: u64, end_offset: u64, record: Setsum) {
        if start_offset == end_offset {
            return;
        }
        self.live += record;
        self.total += record;
        self.total_records = self.total_records.saturating_add(1);
    }
}

fn record_setsum(
    stream_id: &BucketStreamId,
    start_offset: u64,
    end_offset: u64,
    kind: &[u8],
    pieces: &[&[u8]],
) -> Setsum {
    let mut setsum = Setsum::default();
    let start = start_offset.to_le_bytes();
    let end = end_offset.to_le_bytes();
    let mut item = if let Some(affinity_key) = &stream_id.affinity_key {
        vec![
            b"ursula-stream-record-v2".as_slice(),
            stream_id.bucket_id.as_bytes(),
            b"\0",
            affinity_key.as_bytes(),
            b"\0",
            stream_id.stream_id.as_bytes(),
            b"\0",
            &start,
            &end,
            kind,
        ]
    } else {
        vec![
            b"ursula-stream-record-v1".as_slice(),
            stream_id.bucket_id.as_bytes(),
            b"\0",
            stream_id.stream_id.as_bytes(),
            b"\0",
            &start,
            &end,
            kind,
        ]
    };
    item.extend_from_slice(pieces);
    setsum.insert_vectored(&item);
    setsum
}

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

    fn stream_id() -> BucketStreamId {
        BucketStreamId::new("benchcmp", "integrity")
    }

    #[test]
    fn snapshot_omits_per_append_records() {
        let stream_id = stream_id();
        let mut integrity = StreamIntegrity::default();
        integrity.append_payload(&stream_id, 0, 3, b"abc");
        integrity.append_payload(&stream_id, 3, 5, b"de");

        let snapshot = integrity.snapshot(0, 5);

        assert_eq!(snapshot.live_records, 2);
        assert_eq!(snapshot.evicted_records, 0);
        assert_eq!(snapshot.total_records, 2);
        assert_eq!(snapshot.live_setsum, snapshot.total_setsum);
    }

    #[test]
    fn snapshot_wire_format_has_no_records_field() {
        let stream_id = stream_id();
        let mut integrity = StreamIntegrity::default();
        integrity.append_payload(&stream_id, 0, 3, b"abc");

        let encoded =
            serde_json::to_value(integrity.snapshot(0, 3)).expect("encode compacted integrity");

        assert!(encoded.get("records").is_none());
    }
}