dynomite/msg/mod.rs
1//! Message model: the in-memory shape every request and response
2//! takes inside the engine, plus the queues, indices, and per-DC
3//! response managers that thread them through the dispatcher.
4//!
5//! This module exposes the data layer: the message shape, queues,
6//! indices, and per-DC response managers, plus the data-only helpers
7//! (error response construction, fragment bookkeeping, quorum
8//! decisions). The connection-coupled lifecycle (recv/send, timeout
9//! queues, peer forwarding) lives in [`crate::net`].
10//!
11//! # Examples
12//!
13//! ```
14//! use dynomite::msg::{Msg, MsgQueue, MsgType, ResponseMgr};
15//!
16//! let mut q = MsgQueue::new();
17//! q.push_back(Msg::new(1, MsgType::ReqRedisGet, true));
18//!
19//! let req = q.front().unwrap();
20//! let mgr = ResponseMgr::new(req, 1, None);
21//! assert_eq!(mgr.quorum_responses(), 1);
22//! ```
23
24use std::sync::OnceLock;
25
26pub mod index;
27pub mod keypos;
28pub mod message;
29pub mod msg_type;
30pub mod queue;
31pub mod request;
32pub mod response;
33pub mod response_mgr;
34
35pub use self::index::MsgIndex;
36pub use self::keypos::{ArgPos, KeyPos};
37pub use self::message::{ConnId, Msg, MsgFlags, MsgParseResult, MsgRouting};
38pub use self::msg_type::MsgType;
39pub use self::queue::MsgQueue;
40pub use self::response_mgr::{QuorumOutcome, ResponseMgr, MAX_REPLICAS_PER_DC};
41
42/// Cluster consistency level applied to a single message.
43///
44/// The numeric values are stable wire codes for the consistency
45/// level.
46#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
47#[repr(u8)]
48pub enum ConsistencyLevel {
49 /// Wait for one replica to ack.
50 #[default]
51 DcOne = 0,
52 /// Wait for a per-DC majority.
53 DcQuorum = 1,
54 /// `DcQuorum` with body checksum agreement; mismatches trigger
55 /// read repair.
56 DcSafeQuorum = 2,
57 /// `DcSafeQuorum` evaluated independently per datacenter.
58 DcEachSafeQuorum = 3,
59}
60
61impl ConsistencyLevel {
62 /// Stable string name.
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use dynomite::msg::ConsistencyLevel;
68 /// assert_eq!(ConsistencyLevel::DcQuorum.name(), "DC_QUORUM");
69 /// ```
70 #[must_use]
71 pub fn name(self) -> &'static str {
72 match self {
73 ConsistencyLevel::DcOne => "DC_ONE",
74 ConsistencyLevel::DcQuorum => "DC_QUORUM",
75 ConsistencyLevel::DcSafeQuorum => "DC_SAFE_QUORUM",
76 ConsistencyLevel::DcEachSafeQuorum => "DC_EACH_SAFE_QUORUM",
77 }
78 }
79
80 /// Recover the level from its uppercase name.
81 ///
82 /// # Examples
83 ///
84 /// ```
85 /// use dynomite::msg::ConsistencyLevel;
86 /// assert_eq!(
87 /// ConsistencyLevel::from_name("DC_SAFE_QUORUM"),
88 /// Some(ConsistencyLevel::DcSafeQuorum),
89 /// );
90 /// ```
91 #[must_use]
92 pub fn from_name(name: &str) -> Option<Self> {
93 match name {
94 "DC_ONE" => Some(Self::DcOne),
95 "DC_QUORUM" => Some(Self::DcQuorum),
96 "DC_SAFE_QUORUM" => Some(Self::DcSafeQuorum),
97 "DC_EACH_SAFE_QUORUM" => Some(Self::DcEachSafeQuorum),
98 _ => None,
99 }
100 }
101}
102
103/// Dynomite-side error code carried in a message envelope.
104///
105/// The numeric values are stable wire codes for the error.
106#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
107#[repr(u8)]
108pub enum DynErrorCode {
109 /// No error.
110 #[default]
111 Ok = 0,
112 /// Unspecified error.
113 DynomiteUnknownError = 1,
114 /// Engine state forbids the request.
115 DynomiteInvalidState = 2,
116 /// Admin-only command issued in non-admin mode.
117 DynomiteInvalidAdminReq = 3,
118 /// Peer refused the connection.
119 PeerConnectionRefuse = 4,
120 /// Peer reachable but reported down.
121 PeerHostDown = 5,
122 /// Peer not yet connected.
123 PeerHostNotConnected = 6,
124 /// Datastore refused the connection.
125 StorageConnectionRefuse = 7,
126 /// Bad message framing.
127 BadFormat = 8,
128 /// Quorum not achieved.
129 DynomiteNoQuorumAchieved = 9,
130 /// Lua script keys span multiple nodes.
131 DynomiteScriptSpansNodes = 10,
132 /// Payload exceeds the configured limit.
133 DynomitePayloadTooLarge = 11,
134}
135
136impl DynErrorCode {
137 /// Human-readable label used in error responses.
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use dynomite::msg::DynErrorCode;
143 /// assert_eq!(DynErrorCode::PeerHostDown.message(), "Peer Node is down");
144 /// ```
145 #[must_use]
146 pub fn message(self) -> &'static str {
147 match self {
148 DynErrorCode::Ok => "Success",
149 DynErrorCode::DynomiteUnknownError => "Unknown Error",
150 DynErrorCode::DynomiteInvalidState => {
151 "Dynomite's current state does not allow this request"
152 }
153 DynErrorCode::DynomiteInvalidAdminReq => "Invalid request in Dynomite's admin mode",
154 DynErrorCode::PeerConnectionRefuse => "Peer Node refused connection",
155 DynErrorCode::PeerHostDown => "Peer Node is down",
156 DynErrorCode::PeerHostNotConnected => "Peer Node is not connected",
157 DynErrorCode::StorageConnectionRefuse => "Datastore refused connection",
158 DynErrorCode::BadFormat => "Bad message format",
159 DynErrorCode::DynomiteNoQuorumAchieved => "Failed to achieve Quorum",
160 DynErrorCode::DynomiteScriptSpansNodes => {
161 "Keys in the script cannot span multiple nodes"
162 }
163 DynErrorCode::DynomitePayloadTooLarge => "MSET/MGET/SCAN payload too large",
164 }
165 }
166
167 /// Origin label (`Dynomite:`, `Peer:`, `Storage:`, `unknown:`)
168 /// used in error response prefixes, mirroring
169 /// `dyn_error_source`.
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use dynomite::msg::DynErrorCode;
175 /// assert_eq!(DynErrorCode::PeerHostDown.source(), "Peer:");
176 /// ```
177 #[must_use]
178 pub fn source(self) -> &'static str {
179 match self {
180 DynErrorCode::DynomiteInvalidAdminReq
181 | DynErrorCode::DynomiteInvalidState
182 | DynErrorCode::DynomiteNoQuorumAchieved
183 | DynErrorCode::DynomiteScriptSpansNodes
184 | DynErrorCode::DynomitePayloadTooLarge => "Dynomite:",
185 DynErrorCode::PeerConnectionRefuse
186 | DynErrorCode::PeerHostDown
187 | DynErrorCode::PeerHostNotConnected => "Peer:",
188 DynErrorCode::StorageConnectionRefuse => "Storage:",
189 _ => "unknown:",
190 }
191 }
192}
193
194static READ_REPAIRS_ENABLED: OnceLock<bool> = OnceLock::new();
195
196/// Configure whether read repairs are globally enabled.
197///
198/// Called once during configuration validation; subsequent calls are
199/// silently ignored, so the cluster-wide read-repair flag is
200/// write-once. Returns `true` when the value was
201/// installed and `false` when an earlier call already pinned it.
202///
203/// # Examples
204///
205/// ```
206/// use dynomite::msg::set_read_repairs_enabled;
207/// // Calling twice from a single test wins or loses depending on
208/// // whether anyone else got there first; the API is idempotent.
209/// let _ = set_read_repairs_enabled(true);
210/// ```
211pub fn set_read_repairs_enabled(enabled: bool) -> bool {
212 READ_REPAIRS_ENABLED.set(enabled).is_ok()
213}
214
215/// True when read repairs are enabled cluster-wide.
216///
217/// Defaults to `false` until configuration validation enables it.
218///
219/// # Examples
220///
221/// ```
222/// use dynomite::msg::is_read_repairs_enabled;
223/// // Default state is "disabled".
224/// let _ = is_read_repairs_enabled();
225/// ```
226#[must_use]
227pub fn is_read_repairs_enabled() -> bool {
228 *READ_REPAIRS_ENABLED.get().unwrap_or(&false)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn consistency_round_trip() {
237 for level in [
238 ConsistencyLevel::DcOne,
239 ConsistencyLevel::DcQuorum,
240 ConsistencyLevel::DcSafeQuorum,
241 ConsistencyLevel::DcEachSafeQuorum,
242 ] {
243 assert_eq!(ConsistencyLevel::from_name(level.name()), Some(level));
244 }
245 assert!(ConsistencyLevel::from_name("DC_BOGUS").is_none());
246 }
247
248 #[test]
249 fn dyn_error_code_strings_match_c() {
250 assert_eq!(DynErrorCode::Ok.message(), "Success");
251 assert_eq!(DynErrorCode::PeerHostDown.source(), "Peer:");
252 assert_eq!(DynErrorCode::DynomiteUnknownError.source(), "unknown:");
253 assert_eq!(DynErrorCode::StorageConnectionRefuse.source(), "Storage:");
254 }
255}