Skip to main content

ozzy_replication/wire/
checkpoint.rs

1//! Nonvoting checkpoint bytes over bounded, source/nonce-bound PEER exchanges.
2
3use super::{
4    COMMON_BYTES, ControlEncoding, PeerBinding, Reader, WireError, WireLimits, Writer,
5    transfer::{read_source, validate_source, write_source},
6    validate_scope,
7};
8use crate::{LogSource, Scope};
9use ozzy_proto::{Envelope, LinkSessionId, NodeId, Opcode, RequestId};
10
11const METADATA_BYTES: usize = COMMON_BYTES + 16 + 72 + 8 + 4;
12
13/// One bounded byte range of a previously pinned recovery checkpoint.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct CheckpointRequest {
16    /// Exact configuration/view of the recovery attempt.
17    pub scope: Scope,
18    /// Outstanding exchange, checked again against the current link session.
19    pub request_id: RequestId,
20    /// Fresh recovery nonce, independent of transient link identities.
21    pub nonce: RequestId,
22    /// Exact donor incarnation and accepted tail retained by its pin.
23    pub source: LogSource,
24    /// Next logical checkpoint-state byte, beginning at zero.
25    pub offset: u64,
26    /// Positive maximum returned bytes; a source chunk boundary may return less.
27    pub max_bytes: u32,
28}
29
30impl CheckpointRequest {
31    fn validate(self, limits: WireLimits) -> Result<(), WireError> {
32        validate_scope(self.scope)?;
33        validate_source(self.source)?;
34        if self.request_id.as_bytes() == &[0; 16] || self.nonce.as_bytes() == &[0; 16] {
35            return Err(WireError::Correlation);
36        }
37        if self.max_bytes == 0 || self.max_bytes as usize > limits.envelope.max_payload_bytes {
38            return Err(WireError::Limit);
39        }
40        Ok(())
41    }
42}
43
44/// Source-bound checkpoint exchange, supplying no normal quorum evidence.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum CheckpointMessage<'a> {
47    /// Request bytes only after the donor has offered its immutable descriptor.
48    Request(CheckpointRequest),
49    /// Echo the exact request alongside one positive bounded state slice.
50    Chunk {
51        /// Exact nonce/source-bound request being answered.
52        request: CheckpointRequest,
53        /// Positive state slice within the requested limit.
54        bytes: &'a [u8],
55    },
56}
57
58/// Encode checkpoint request/chunk metadata without copying state bytes.
59pub fn encode_checkpoint(
60    sender: NodeId,
61    session: LinkSessionId,
62    message: CheckpointMessage<'_>,
63    output: &mut [u8],
64    limits: WireLimits,
65) -> Result<ControlEncoding, WireError> {
66    let (request, payload_bytes, response) = match message {
67        CheckpointMessage::Request(request) => (request, 0, false),
68        CheckpointMessage::Chunk { request, bytes } => {
69            if bytes.is_empty()
70                || bytes.len() > request.max_bytes as usize
71                || request.source.voter != sender
72            {
73                return Err(WireError::History);
74            }
75            (request, bytes.len(), true)
76        }
77    };
78    request.validate(limits)?;
79    let header = Envelope {
80        opcode: if response {
81            Opcode::SnapshotChunk
82        } else {
83            Opcode::SnapshotBegin
84        },
85        response,
86        request_id: Some(request.request_id),
87        sender,
88        session: Some(session),
89    }
90    .encode_header(METADATA_BYTES, payload_bytes, limits.envelope)?;
91    if output.len() < METADATA_BYTES {
92        return Err(WireError::Capacity);
93    }
94    let mut writer = Writer::new(&mut output[..METADATA_BYTES]);
95    writer.scope(request.scope, sender);
96    writer.bytes(request.nonce.as_bytes());
97    write_source(&mut writer, request.source);
98    writer.bytes(&request.offset.to_be_bytes());
99    writer.bytes(&request.max_bytes.to_be_bytes());
100    Ok(ControlEncoding {
101        header,
102        metadata_bytes: METADATA_BYTES,
103    })
104}
105
106pub(super) fn decode<'a>(
107    scope: Scope,
108    envelope: Envelope,
109    mut reader: Reader<'_>,
110    payload: &'a [u8],
111    binding: PeerBinding,
112    limits: WireLimits,
113) -> Result<CheckpointMessage<'a>, WireError> {
114    let request = CheckpointRequest {
115        scope,
116        request_id: envelope.request_id.ok_or(WireError::Correlation)?,
117        nonce: RequestId::from_bytes(reader.bytes()?),
118        source: read_source(&mut reader)?,
119        offset: u64::from_be_bytes(reader.bytes()?),
120        max_bytes: reader.u32()?,
121    };
122    reader.finish()?;
123    request.validate(limits)?;
124    binding.configuration.voter_index(request.source.voter)?;
125    match envelope.opcode {
126        Opcode::SnapshotBegin if !envelope.response && payload.is_empty() => {
127            Ok(CheckpointMessage::Request(request))
128        }
129        Opcode::SnapshotChunk
130            if envelope.response
131                && request.source.voter == binding.peer
132                && !payload.is_empty()
133                && payload.len() <= request.max_bytes as usize =>
134        {
135            Ok(CheckpointMessage::Chunk {
136                request,
137                bytes: payload,
138            })
139        }
140        _ => Err(WireError::Correlation),
141    }
142}
143
144/// Exact old receiver or history request being withdrawn. Neither is authority.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum HistoryFence {
147    /// Volatile normal receive incarnation.
148    Receive(crate::flow::ReceiveEpoch),
149    /// Outstanding election history request.
150    Fetch(RequestId),
151}
152
153/// Primary hint to withdraw a lagging receiver before fresh quorum recovery.
154/// This carries no vote, commit proof, or checkpoint installation authority.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct HistoryRetired {
157    /// Exact normal authority view.
158    pub scope: Scope,
159    /// Receiver incarnation whose independently checked history is unavailable.
160    pub fence: HistoryFence,
161    /// Last original operation preceding all required retained operations.
162    pub before: crate::Prefix,
163}
164
165/// Encode a receiver-fenced withdrawal hint on the control plane.
166pub fn encode_history_retired(
167    sender: NodeId,
168    session: LinkSessionId,
169    notice: HistoryRetired,
170    output: &mut [u8],
171    limits: WireLimits,
172) -> Result<ControlEncoding, WireError> {
173    validate_scope(notice.scope)?;
174    super::validate_prefix(notice.before)?;
175    if notice.before.op.0 == 0
176        || matches!(notice.fence, HistoryFence::Fetch(id) if id.as_bytes() == &[0; 16])
177    {
178        return Err(WireError::History);
179    }
180    let length = COMMON_BYTES + 1 + 16 + 40;
181    let header = Envelope {
182        opcode: Opcode::HistoryRetired,
183        response: false,
184        request_id: None,
185        sender,
186        session: Some(session),
187    }
188    .encode_header(length, 0, limits.envelope)?;
189    if output.len() < length {
190        return Err(WireError::Capacity);
191    }
192    let mut writer = Writer::new(&mut output[..length]);
193    writer.scope(notice.scope, sender);
194    match notice.fence {
195        HistoryFence::Receive(epoch) => {
196            writer.bytes(&[0]);
197            writer.bytes(&epoch.get().to_be_bytes());
198        }
199        HistoryFence::Fetch(id) => {
200            writer.bytes(&[1]);
201            writer.bytes(id.as_bytes());
202        }
203    }
204    writer.prefix(notice.before);
205    Ok(ControlEncoding {
206        header,
207        metadata_bytes: length,
208    })
209}
210
211pub(super) fn decode_retired(
212    scope: Scope,
213    envelope: Envelope,
214    mut reader: Reader<'_>,
215    payload: &[u8],
216) -> Result<HistoryRetired, WireError> {
217    if envelope.response || envelope.request_id.is_some() || !payload.is_empty() {
218        return Err(WireError::Correlation);
219    }
220    let tag: [u8; 1] = reader.bytes()?;
221    let id: [u8; 16] = reader.bytes()?;
222    let fence = match tag[0] {
223        0 => HistoryFence::Receive(crate::flow::ReceiveEpoch::new(u128::from_be_bytes(id))?),
224        1 if id != [0; 16] => HistoryFence::Fetch(RequestId::from_bytes(id)),
225        _ => return Err(WireError::Correlation),
226    };
227    let before = reader.prefix()?;
228    reader.finish()?;
229    super::validate_prefix(before)?;
230    if before.op.0 == 0 {
231        return Err(WireError::History);
232    }
233    Ok(HistoryRetired {
234        scope,
235        fence,
236        before,
237    })
238}