reddb_server/replication/
fence.rs1pub use reddb_file::FileTermStore;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum FenceBoundary {
43 Apply,
45 Handshake,
47 Lease,
49}
50
51impl FenceBoundary {
52 pub fn as_str(self) -> &'static str {
53 match self {
54 Self::Apply => "apply",
55 Self::Handshake => "handshake",
56 Self::Lease => "lease",
57 }
58 }
59}
60
61#[inline]
64pub fn term_is_stale(incoming_term: u64, current_term: u64) -> bool {
65 incoming_term < current_term
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct StreamHandshake {
71 pub peer_id: String,
72 pub term: u64,
73}
74
75impl StreamHandshake {
76 pub fn new(peer_id: impl Into<String>, term: u64) -> Self {
77 Self {
78 peer_id: peer_id.into(),
79 term,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct StaleTermFenced {
88 pub boundary: FenceBoundary,
89 pub incoming_term: u64,
90 pub current_term: u64,
91}
92
93impl std::fmt::Display for StaleTermFenced {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(
96 f,
97 "fenced stale-term {} message: incoming term {} is behind current term {}",
98 self.boundary.as_str(),
99 self.incoming_term,
100 self.current_term
101 )
102 }
103}
104
105impl std::error::Error for StaleTermFenced {}
106
107pub type StaleTermRejection = StaleTermFenced;
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum FenceVerdict {
112 Admit { term: u64 },
114 Adopt { new_term: u64 },
117 Fenced(StaleTermFenced),
119}
120
121impl FenceVerdict {
122 pub fn is_admitted(&self) -> bool {
125 matches!(self, Self::Admit { .. } | Self::Adopt { .. })
126 }
127
128 pub fn is_fenced(&self) -> bool {
130 matches!(self, Self::Fenced(_))
131 }
132}
133
134#[derive(Debug)]
136pub enum TermStoreError {
137 Io(std::io::Error),
138 InvalidFormat(String),
139}
140
141impl std::fmt::Display for TermStoreError {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Io(err) => write!(f, "term store io error: {err}"),
145 Self::InvalidFormat(msg) => write!(f, "invalid term store format: {msg}"),
146 }
147 }
148}
149
150impl std::error::Error for TermStoreError {}
151
152impl From<reddb_file::RdbFileError> for TermStoreError {
153 fn from(value: reddb_file::RdbFileError) -> Self {
154 match value {
155 reddb_file::RdbFileError::Io(err) => Self::Io(err),
156 reddb_file::RdbFileError::InvalidOperation(msg) => Self::InvalidFormat(msg),
157 err @ reddb_file::RdbFileError::ZoneUnrecoverable { .. } => {
160 Self::InvalidFormat(err.to_string())
161 }
162 }
163 }
164}
165
166pub trait TermStore {
171 fn load(&self) -> Result<u64, TermStoreError>;
172 fn persist(&self, term: u64) -> Result<(), TermStoreError>;
173}
174
175#[derive(Debug)]
177pub struct MemoryTermStore {
178 inner: std::sync::Mutex<u64>,
179}
180
181impl Default for MemoryTermStore {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl MemoryTermStore {
188 pub fn new() -> Self {
189 Self {
190 inner: std::sync::Mutex::new(crate::replication::DEFAULT_REPLICATION_TERM),
191 }
192 }
193
194 pub fn seeded(term: u64) -> Self {
197 Self {
198 inner: std::sync::Mutex::new(term),
199 }
200 }
201}
202
203impl TermStore for MemoryTermStore {
204 fn load(&self) -> Result<u64, TermStoreError> {
205 Ok(*self.inner.lock().expect("term store mutex"))
206 }
207
208 fn persist(&self, term: u64) -> Result<(), TermStoreError> {
209 *self.inner.lock().expect("term store mutex") = term;
210 Ok(())
211 }
212}
213
214impl TermStore for FileTermStore {
215 fn load(&self) -> Result<u64, TermStoreError> {
216 self.load_file().map_err(TermStoreError::from)
217 }
218
219 fn persist(&self, term: u64) -> Result<(), TermStoreError> {
220 self.persist_file(term).map_err(TermStoreError::from)
221 }
222}
223
224pub struct TermFence<S: TermStore> {
227 store: S,
228}
229
230impl<S: TermStore> TermFence<S> {
231 pub fn new(store: S) -> Self {
232 Self { store }
233 }
234
235 pub fn current_term(&self) -> Result<u64, TermStoreError> {
237 self.store.load()
238 }
239
240 pub fn classify(
244 &self,
245 boundary: FenceBoundary,
246 incoming_term: u64,
247 ) -> Result<FenceVerdict, TermStoreError> {
248 let current = self.store.load()?;
249 Ok(if term_is_stale(incoming_term, current) {
250 FenceVerdict::Fenced(StaleTermFenced {
251 boundary,
252 incoming_term,
253 current_term: current,
254 })
255 } else if incoming_term > current {
256 FenceVerdict::Adopt {
257 new_term: incoming_term,
258 }
259 } else {
260 FenceVerdict::Admit { term: current }
261 })
262 }
263
264 pub fn admit_record(&self, incoming_term: u64) -> Result<FenceVerdict, TermStoreError> {
267 self.admit(FenceBoundary::Apply, incoming_term)
268 }
269
270 pub fn admit_handshake(&self, incoming_term: u64) -> Result<FenceVerdict, TermStoreError> {
273 self.admit(FenceBoundary::Handshake, incoming_term)
274 }
275
276 pub fn admit_stream_handshake(
278 &self,
279 handshake: &StreamHandshake,
280 ) -> Result<FenceVerdict, TermStoreError> {
281 self.admit_handshake(handshake.term)
282 }
283
284 pub fn admit_lease_write(&self, lease_term: u64) -> Result<FenceVerdict, TermStoreError> {
287 self.admit(FenceBoundary::Lease, lease_term)
288 }
289
290 fn admit(
291 &self,
292 boundary: FenceBoundary,
293 incoming_term: u64,
294 ) -> Result<FenceVerdict, TermStoreError> {
295 let verdict = self.classify(boundary, incoming_term)?;
296 if let FenceVerdict::Adopt { new_term } = verdict {
297 self.store.persist(new_term)?;
300 }
301 Ok(verdict)
302 }
303
304 pub fn adopt(&self, new_term: u64) -> Result<(), TermStoreError> {
309 let current = self.store.load()?;
310 if new_term > current {
311 self.store.persist(new_term)?;
312 }
313 Ok(())
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn fence(term: u64) -> TermFence<MemoryTermStore> {
322 TermFence::new(MemoryTermStore::seeded(term))
323 }
324
325 #[test]
328 fn apply_boundary_fences_stale_term() {
329 let f = fence(5);
330 let verdict = f.admit_record(4).unwrap();
331 assert_eq!(
332 verdict,
333 FenceVerdict::Fenced(StaleTermFenced {
334 boundary: FenceBoundary::Apply,
335 incoming_term: 4,
336 current_term: 5,
337 })
338 );
339 assert!(verdict.is_fenced());
340 assert_eq!(f.current_term().unwrap(), 5);
342 }
343
344 #[test]
345 fn apply_boundary_admits_current_term() {
346 let f = fence(5);
347 assert_eq!(f.admit_record(5).unwrap(), FenceVerdict::Admit { term: 5 });
348 assert_eq!(f.current_term().unwrap(), 5);
349 }
350
351 #[test]
352 fn apply_boundary_adopts_higher_term_durably() {
353 let f = fence(5);
354 assert_eq!(
355 f.admit_record(8).unwrap(),
356 FenceVerdict::Adopt { new_term: 8 }
357 );
358 assert_eq!(f.current_term().unwrap(), 8);
360 assert!(f.admit_record(5).unwrap().is_fenced());
361 }
362
363 #[test]
366 fn handshake_boundary_fences_stale_term() {
367 let f = fence(7);
368 let verdict = f.admit_handshake(6).unwrap();
369 assert_eq!(
370 verdict,
371 FenceVerdict::Fenced(StaleTermFenced {
372 boundary: FenceBoundary::Handshake,
373 incoming_term: 6,
374 current_term: 7,
375 })
376 );
377 assert!(verdict.is_fenced());
378 }
379
380 #[test]
381 fn handshake_boundary_admits_current_and_adopts_higher() {
382 let f = fence(7);
383 assert_eq!(
384 f.admit_handshake(7).unwrap(),
385 FenceVerdict::Admit { term: 7 }
386 );
387 assert_eq!(
388 f.admit_handshake(9).unwrap(),
389 FenceVerdict::Adopt { new_term: 9 }
390 );
391 assert_eq!(f.current_term().unwrap(), 9);
392 }
393
394 #[test]
397 fn returning_ex_primary_is_fenced_until_it_adopts_new_term() {
398 let f = fence(5);
400 assert!(matches!(
401 f.admit_handshake(6).unwrap(),
402 FenceVerdict::Adopt { new_term: 6 }
403 ));
404
405 assert!(f.admit_handshake(5).unwrap().is_fenced());
408 assert!(f.admit_record(5).unwrap().is_fenced());
409
410 f.adopt(6).unwrap();
413 assert!(f.admit_record(6).unwrap().is_admitted());
414 }
415
416 #[test]
417 fn classify_is_pure_and_does_not_adopt() {
418 let f = fence(3);
419 assert_eq!(
421 f.classify(FenceBoundary::Apply, 9).unwrap(),
422 FenceVerdict::Adopt { new_term: 9 }
423 );
424 assert_eq!(f.current_term().unwrap(), 3, "classify must not mutate");
425 }
426
427 #[test]
428 fn adopt_never_moves_term_backwards() {
429 let f = fence(10);
430 f.adopt(4).unwrap();
431 assert_eq!(f.current_term().unwrap(), 10);
432 f.adopt(12).unwrap();
433 assert_eq!(f.current_term().unwrap(), 12);
434 }
435
436 #[test]
439 fn file_term_store_round_trips_and_defaults() {
440 let path = std::env::temp_dir().join(format!(
441 "reddb-term-fence-{}-{}.json",
442 std::process::id(),
443 crate::utils::now_unix_nanos()
444 ));
445 let _ = std::fs::remove_file(&path);
446
447 let store = FileTermStore::new(&path);
449 assert_eq!(
450 store.load().unwrap(),
451 crate::replication::DEFAULT_REPLICATION_TERM
452 );
453
454 {
457 let fence = TermFence::new(FileTermStore::new(&path));
458 assert!(matches!(
459 fence.admit_handshake(6).unwrap(),
460 FenceVerdict::Adopt { new_term: 6 }
461 ));
462 }
463 let reopened = TermFence::new(FileTermStore::new(&path));
464 assert_eq!(reopened.current_term().unwrap(), 6);
465 assert!(reopened.admit_record(5).unwrap().is_fenced());
466
467 let _ = std::fs::remove_file(&path);
468 }
469}