Skip to main content

ozzy_proto/
nack.rs

1//! Bounded negative responses. A timeout is not a negative response.
2
3use super::append::Authority;
4use super::{ENVELOPE_BYTES, Envelope, EnvelopeError, EnvelopeLimits, Opcode, Packet};
5use crate::{
6    GroupId, NodeId,
7    layout::{Fixed, LayoutError, Reader},
8};
9
10/// Routing information, never evidence of primary activation or quorum commit.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct AuthorityHint {
13    /// Group/configuration and observed proposed view.
14    pub authority: Authority,
15    /// Configured primary for that view; clients verify against fixed membership.
16    pub primary: NodeId,
17}
18
19impl AuthorityHint {
20    /// Encode the 48-byte detail carried by authority-related native NACKs.
21    pub fn encode(self) -> Result<[u8; 48], NackError> {
22        self.validate()?;
23        let mut bytes = [0; 48];
24        bytes[..16].copy_from_slice(self.authority.group_id.as_bytes());
25        bytes[16..24].copy_from_slice(&self.authority.config_epoch.to_be_bytes());
26        bytes[24..32].copy_from_slice(&self.authority.view.to_be_bytes());
27        bytes[32..].copy_from_slice(self.primary.as_bytes());
28        Ok(bytes)
29    }
30
31    /// Decode structural fields only. Membership and monotonic-view checks remain
32    /// client responsibilities, after verifying the response's session/correlation.
33    pub fn decode(bytes: &[u8]) -> Result<Self, NackError> {
34        let bytes: &[u8; 48] = bytes.try_into().map_err(|_| NackError::Length)?;
35        let hint = Self {
36            authority: Authority {
37                group_id: GroupId::from_bytes(bytes.array::<0, 16>()),
38                config_epoch: bytes.u64_at::<16>(),
39                view: bytes.u64_at::<24>(),
40            },
41            primary: NodeId::from_bytes(bytes.array::<32, 16>()),
42        };
43        hint.validate()?;
44        Ok(hint)
45    }
46
47    fn validate(self) -> Result<(), NackError> {
48        if self.authority.group_id.as_bytes() == &[0; 16]
49            || self.authority.config_epoch == 0
50            || self.primary.as_bytes() == &[0; 16]
51        {
52            return Err(NackError::Authority);
53        }
54        Ok(())
55    }
56}
57
58/// Machine-readable next action. Does not describe the outcome of prior attempts.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[repr(u8)]
61pub enum RetryClass {
62    /// Correct the request; repeating unchanged bytes cannot resolve this rejection.
63    Permanent = 1,
64    /// Retry after local/remote capacity becomes available.
65    AfterBackoff = 2,
66    /// Negotiate a fresh link session before retrying unchanged append identity.
67    AfterReconnect = 3,
68    /// Refresh group/owner authority without changing append identity.
69    AfterAuthorityRefresh = 4,
70    /// An admitted attempt may have committed. Reconcile the same identity.
71    UnknownOutcome = 5,
72}
73
74/// Typed rejection with opaque code-specific detail; unknown codes remain readable.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Nack<'a> {
77    /// Stable error code, independent of diagnostic text.
78    pub code: u16,
79    /// Protocol retry classification, never parsed from text.
80    pub retry: RetryClass,
81    /// Bounded code-specific detail bytes.
82    pub detail: &'a [u8],
83    /// Optional human-readable UTF-8; never used for decisions.
84    pub diagnostic: &'a str,
85}
86
87/// Encode a correlated NACK into reserved metadata; payload must be empty.
88pub fn encode(
89    envelope: Envelope,
90    nack: Nack<'_>,
91    metadata: &mut Vec<u8>,
92    limits: EnvelopeLimits,
93) -> Result<[u8; ENVELOPE_BYTES], NackError> {
94    command(envelope)?;
95    let size = 11_usize
96        .checked_add(nack.detail.len())
97        .and_then(|n| n.checked_add(nack.diagnostic.len()))
98        .ok_or(NackError::Length)?;
99    let header = envelope.encode_header(size, 0, limits)?;
100    let detail = u32::try_from(nack.detail.len()).map_err(|_| NackError::Length)?;
101    let diagnostic = u32::try_from(nack.diagnostic.len()).map_err(|_| NackError::Length)?;
102    if metadata.capacity() < size {
103        return Err(NackError::Capacity);
104    }
105    metadata.clear();
106    metadata.extend_from_slice(&nack.code.to_be_bytes());
107    metadata.push(nack.retry as u8);
108    metadata.extend_from_slice(&detail.to_be_bytes());
109    metadata.extend_from_slice(nack.detail);
110    metadata.extend_from_slice(&diagnostic.to_be_bytes());
111    metadata.extend_from_slice(nack.diagnostic.as_bytes());
112    Ok(header)
113}
114
115/// Decode exact frame lengths without allocating, retaining unknown codes/detail.
116pub fn decode(packet: Packet<'_>, limits: EnvelopeLimits) -> Result<Nack<'_>, NackError> {
117    command(packet.envelope)?;
118    packet
119        .envelope
120        .validate_frames(packet.metadata.len(), packet.payload.len(), limits)?;
121    if !packet.payload.is_empty() {
122        return Err(NackError::Command);
123    }
124    let mut reader = Reader::new(packet.metadata);
125    let code = reader.u16()?;
126    let retry = match reader.u8()? {
127        1 => RetryClass::Permanent,
128        2 => RetryClass::AfterBackoff,
129        3 => RetryClass::AfterReconnect,
130        4 => RetryClass::AfterAuthorityRefresh,
131        5 => RetryClass::UnknownOutcome,
132        _ => return Err(NackError::Command),
133    };
134    let size = reader.u32()? as usize;
135    let detail = reader.take(size)?;
136    let size = reader.u32()? as usize;
137    let diagnostic = std::str::from_utf8(reader.take(size)?).map_err(|_| NackError::Length)?;
138    reader.finish()?;
139    Ok(Nack {
140        code,
141        retry,
142        detail,
143        diagnostic,
144    })
145}
146
147fn command(envelope: Envelope) -> Result<(), NackError> {
148    if envelope.opcode != Opcode::Nack || !envelope.response || envelope.request_id.is_none() {
149        return Err(NackError::Command);
150    }
151    Ok(())
152}
153
154/// Malformed or unencodable negative response.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
156pub enum NackError {
157    /// Zero group, configuration epoch, or primary identity in a routing hint.
158    #[error("invalid NACK authority hint")]
159    Authority,
160    /// Invalid outer command/session/limits.
161    #[error(transparent)]
162    Envelope(#[from] EnvelopeError),
163    /// Wrong direction, opcode, payload, or retry class.
164    #[error("invalid NACK command")]
165    Command,
166    /// Truncated, trailing, oversized, or invalid text bytes.
167    #[error("invalid NACK field length")]
168    Length,
169    /// Caller must reserve sufficient metadata capacity.
170    #[error("NACK encode buffer capacity exhausted")]
171    Capacity,
172}
173
174impl From<LayoutError> for NackError {
175    fn from(_: LayoutError) -> Self {
176        Self::Length
177    }
178}