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