raft_io/log.rs
1//! The log-storage seam and its in-memory implementation.
2//!
3//! [`RaftLog`] is the boundary between the protocol and where the log actually
4//! lives. The node reads through it (last index, term at an index, an entry)
5//! and writes through it (append, truncate, hard state), and treats a returned
6//! `Ok` from [`sync`](RaftLog::sync) as the durability point: everything
7//! written before a successful `sync` will survive a crash. That contract is
8//! what lets the same protocol run over a throwaway [`MemoryLog`] in tests and a
9//! `wal-db`-backed store in production (arriving in `v0.4`) without the core
10//! knowing the difference.
11//!
12//! Implementors map their own failures into [`Error::Storage`] via
13//! [`Error::storage`](crate::Error::storage), so the trait's error type stays
14//! the crate's own — no associated error type for callers to name.
15
16use crate::error::{Error, Result};
17use crate::types::{HardState, Index, LogEntry, NodeId, Snapshot, Term};
18
19/// Storage for a node's persistent state: its log entries and its
20/// [`HardState`].
21///
22/// Indices are 1-based and contiguous. Index `0` is the sentinel "before the
23/// first entry": [`term_at`](RaftLog::term_at) returns `Some(0)` for it so the
24/// `prev_log_index` consistency check at the head of the log needs no special
25/// case.
26///
27/// # Durability contract
28///
29/// A backend may buffer writes, but once [`sync`](RaftLog::sync) returns `Ok`,
30/// every preceding [`append`](RaftLog::append),
31/// [`truncate`](RaftLog::truncate), and
32/// [`set_hard_state`](RaftLog::set_hard_state) must be durable. The node always
33/// calls `sync` before emitting any message that depends on that state, which
34/// is how it honours Raft's "persist before you respond" rule.
35///
36/// # Examples
37///
38/// Implementing a custom backend means forwarding to your store and mapping its
39/// errors. The in-memory [`MemoryLog`] is the reference implementation; see its
40/// source for the full shape. A read-through usage example:
41///
42/// ```
43/// use raft_io::{LogEntry, MemoryLog, RaftLog};
44///
45/// let mut log = MemoryLog::new();
46/// log.append(&[LogEntry::new(1, 1, b"a".to_vec())]).unwrap();
47/// log.sync().unwrap();
48///
49/// assert_eq!(log.last_index(), 1);
50/// assert_eq!(log.last_term(), 1);
51/// assert_eq!(log.term_at(1), Some(1));
52/// assert_eq!(log.term_at(0), Some(0)); // sentinel
53/// assert_eq!(log.entry(1).unwrap().command, b"a");
54/// ```
55pub trait RaftLog {
56 /// Returns the index of the last entry, or `0` if the log is empty.
57 fn last_index(&self) -> Index;
58
59 /// Returns the term of the last entry, or `0` if the log is empty.
60 fn last_term(&self) -> Term;
61
62 /// Returns the term of the entry at `index`.
63 ///
64 /// Returns `Some(0)` for the sentinel index `0`, `Some(term)` for an entry
65 /// that exists, and `None` for an index past the end of the log.
66 fn term_at(&self, index: Index) -> Option<Term>;
67
68 /// Returns the entry at `index`, or `None` if there is none.
69 fn entry(&self, index: Index) -> Option<LogEntry>;
70
71 /// Returns the entries in the inclusive index range `[from, to]`.
72 ///
73 /// Indices outside the log are skipped, and an empty range (`to < from`, or
74 /// `from == 0`) yields an empty vector. The leader uses this to assemble a
75 /// replication batch. The default implementation reads each index through
76 /// [`entry`](RaftLog::entry); a backend that stores entries contiguously
77 /// should override it with a single bulk read.
78 fn entries(&self, from: Index, to: Index) -> Vec<LogEntry> {
79 if from == 0 || to < from {
80 return Vec::new();
81 }
82 let mut out = Vec::with_capacity((to - from + 1) as usize);
83 let mut index = from;
84 while index <= to {
85 if let Some(entry) = self.entry(index) {
86 out.push(entry);
87 }
88 index += 1;
89 }
90 out
91 }
92
93 /// Appends `entries` to the end of the log.
94 ///
95 /// The first entry's index must be exactly `last_index() + 1` and the
96 /// entries must be contiguous; an implementation must reject a gap or
97 /// overlap rather than corrupt the log. Appending an empty slice is a no-op.
98 ///
99 /// # Errors
100 ///
101 /// Returns [`Error::Storage`] if the entries are not contiguous with the log
102 /// or the backend fails to store them.
103 fn append(&mut self, entries: &[LogEntry]) -> Result<()>;
104
105 /// Removes every entry whose index is `>= from`.
106 ///
107 /// Used to resolve a conflict when a follower's log diverges from the
108 /// leader's (the replication path, `v0.3`). `from` must be `>= 1`; the
109 /// sentinel at index `0` cannot be removed.
110 ///
111 /// # Errors
112 ///
113 /// Returns [`Error::Storage`] if `from` is `0` or the backend fails.
114 fn truncate(&mut self, from: Index) -> Result<()>;
115
116 /// Returns the persisted [`HardState`] (current term and vote).
117 fn hard_state(&self) -> HardState;
118
119 /// Persists `state` as the new [`HardState`].
120 ///
121 /// # Errors
122 ///
123 /// Returns [`Error::Storage`] if the backend fails to store it.
124 fn set_hard_state(&mut self, state: HardState) -> Result<()>;
125
126 /// Flushes all preceding writes to durable storage.
127 ///
128 /// After this returns `Ok`, the durability contract holds for everything
129 /// written so far. The in-memory log has nothing to flush and returns `Ok`
130 /// immediately.
131 ///
132 /// # Errors
133 ///
134 /// Returns [`Error::Storage`] if the backend cannot make its writes durable.
135 fn sync(&mut self) -> Result<()>;
136
137 /// Returns the index the log has been compacted up to — the last index a
138 /// snapshot includes — or `0` if there is no snapshot.
139 ///
140 /// Entries at or below this index are no longer individually available;
141 /// [`snapshot`](RaftLog::snapshot) covers them, and
142 /// [`term_at`](RaftLog::term_at) still answers for the boundary index itself.
143 /// Defaults to `0` for backends without snapshot support.
144 fn snapshot_index(&self) -> Index {
145 0
146 }
147
148 /// Returns the current snapshot, if one exists.
149 ///
150 /// A leader reads this to send a far-behind follower an `InstallSnapshot`
151 /// instead of replaying entries it has already compacted away. Defaults to
152 /// `None`.
153 fn snapshot(&self) -> Option<Snapshot> {
154 None
155 }
156
157 /// Installs `snapshot`, replacing the prefix it subsumes.
158 ///
159 /// Entries up to `snapshot.index` are discarded and the snapshot becomes the
160 /// log's new base. A matching tail — an entry at `snapshot.index` whose term
161 /// is `snapshot.term` — is preserved; otherwise the remaining entries are
162 /// cleared because the snapshot supersedes them. A snapshot no newer than the
163 /// current one is a no-op.
164 ///
165 /// The default implementation returns an error, so a backend that does not
166 /// support snapshots fails loudly rather than silently dropping compaction.
167 ///
168 /// # Errors
169 ///
170 /// Returns [`Error::Storage`] if the backend does not support snapshots or
171 /// fails to store it.
172 fn apply_snapshot(&mut self, snapshot: &Snapshot) -> Result<()> {
173 let _ = snapshot;
174 Err(Error::storage(
175 "apply snapshot",
176 "this log does not support snapshots",
177 ))
178 }
179}
180
181/// An in-memory [`RaftLog`] backed by a `Vec`.
182///
183/// This is the default store and the one [`RaftNode::new`](crate::RaftNode::new)
184/// uses. It keeps entries in a vector (entry at index `i` lives at slot `i - 1`)
185/// and the hard state in a field. Nothing is durable across a process restart —
186/// it is for tests, examples, and the single-node path, not production. Its
187/// operations never fail except on a misuse that would corrupt the log
188/// (a non-contiguous append or a `truncate(0)`).
189///
190/// After a snapshot is installed the log is compacted: entries up to the
191/// snapshot's index are dropped and `base_index` / `base_term` become the log's
192/// new starting boundary, so reads below the boundary return `None` while
193/// [`term_at`](RaftLog::term_at) still answers for the boundary index itself.
194///
195/// # Examples
196///
197/// ```
198/// use raft_io::{HardState, LogEntry, MemoryLog, RaftLog};
199///
200/// let mut log = MemoryLog::new();
201/// assert_eq!(log.last_index(), 0);
202///
203/// log.append(&[LogEntry::new(1, 1, b"x".to_vec())]).unwrap();
204/// log.set_hard_state(HardState { term: 1, voted_for: Some(1) }).unwrap();
205/// log.sync().unwrap();
206///
207/// assert_eq!(log.last_index(), 1);
208/// assert_eq!(log.hard_state().voted_for, Some(1));
209/// ```
210#[derive(Clone, Debug, Default)]
211pub struct MemoryLog {
212 /// Entries with index in `(base_index, base_index + entries.len()]`.
213 entries: Vec<LogEntry>,
214 /// Index of the snapshot boundary (last included), or `0` if none.
215 base_index: Index,
216 /// Term at the snapshot boundary, or `0` if none.
217 base_term: Term,
218 /// Snapshot bytes, present once a snapshot has been installed.
219 snapshot: Option<Vec<u8>>,
220 /// Voting membership recorded by the current snapshot.
221 snapshot_config: Vec<NodeId>,
222 hard: HardState,
223}
224
225impl MemoryLog {
226 /// Creates an empty in-memory log.
227 ///
228 /// # Examples
229 ///
230 /// ```
231 /// use raft_io::{MemoryLog, RaftLog};
232 ///
233 /// let log = MemoryLog::new();
234 /// assert_eq!(log.last_index(), 0);
235 /// ```
236 #[must_use]
237 pub fn new() -> Self {
238 Self::default()
239 }
240
241 /// Returns the number of entries currently stored.
242 ///
243 /// # Examples
244 ///
245 /// ```
246 /// use raft_io::{LogEntry, MemoryLog, RaftLog};
247 ///
248 /// let mut log = MemoryLog::new();
249 /// log.append(&[LogEntry::new(1, 1, vec![])]).unwrap();
250 /// assert_eq!(log.len(), 1);
251 /// ```
252 #[inline]
253 #[must_use]
254 pub fn len(&self) -> usize {
255 self.entries.len()
256 }
257
258 /// Returns `true` if the log holds no entries.
259 ///
260 /// # Examples
261 ///
262 /// ```
263 /// use raft_io::MemoryLog;
264 ///
265 /// assert!(MemoryLog::new().is_empty());
266 /// ```
267 #[inline]
268 #[must_use]
269 pub fn is_empty(&self) -> bool {
270 self.entries.is_empty()
271 }
272}
273
274impl MemoryLog {
275 /// Slot in `entries` for `index`, if it is in range `(base_index, last]`.
276 #[inline]
277 fn slot(&self, index: Index) -> Option<usize> {
278 if index <= self.base_index || index > self.last_index() {
279 None
280 } else {
281 Some((index - self.base_index - 1) as usize)
282 }
283 }
284}
285
286impl RaftLog for MemoryLog {
287 #[inline]
288 fn last_index(&self) -> Index {
289 self.base_index + self.entries.len() as Index
290 }
291
292 #[inline]
293 fn last_term(&self) -> Term {
294 self.entries.last().map_or(self.base_term, |e| e.term)
295 }
296
297 fn term_at(&self, index: Index) -> Option<Term> {
298 if index == self.base_index {
299 return Some(self.base_term);
300 }
301 self.slot(index).map(|s| self.entries[s].term)
302 }
303
304 fn entry(&self, index: Index) -> Option<LogEntry> {
305 self.slot(index).map(|s| self.entries[s].clone())
306 }
307
308 fn entries(&self, from: Index, to: Index) -> Vec<LogEntry> {
309 if from == 0 {
310 return Vec::new();
311 }
312 let from = from.max(self.base_index + 1);
313 if to < from {
314 return Vec::new();
315 }
316 let start = (from - self.base_index - 1) as usize;
317 let end = ((to - self.base_index) as usize).min(self.entries.len());
318 if start >= end {
319 return Vec::new();
320 }
321 self.entries[start..end].to_vec()
322 }
323
324 fn append(&mut self, entries: &[LogEntry]) -> Result<()> {
325 if entries.is_empty() {
326 return Ok(());
327 }
328 let expected = self.last_index() + 1;
329 if entries[0].index != expected {
330 return Err(Error::storage(
331 "append entries",
332 format!(
333 "non-contiguous append: expected index {expected}, got {}",
334 entries[0].index
335 ),
336 ));
337 }
338 // The slice itself must be internally contiguous too.
339 for pair in entries.windows(2) {
340 if pair[1].index != pair[0].index + 1 {
341 return Err(Error::storage(
342 "append entries",
343 "entries within the batch are not contiguous",
344 ));
345 }
346 }
347 self.entries.extend_from_slice(entries);
348 Ok(())
349 }
350
351 fn truncate(&mut self, from: Index) -> Result<()> {
352 if from <= self.base_index {
353 return Err(Error::storage(
354 "truncate log",
355 "cannot truncate into the snapshot",
356 ));
357 }
358 let keep = (from - self.base_index - 1) as usize;
359 if keep < self.entries.len() {
360 self.entries.truncate(keep);
361 }
362 Ok(())
363 }
364
365 #[inline]
366 fn hard_state(&self) -> HardState {
367 self.hard
368 }
369
370 #[inline]
371 fn set_hard_state(&mut self, state: HardState) -> Result<()> {
372 self.hard = state;
373 Ok(())
374 }
375
376 #[inline]
377 fn sync(&mut self) -> Result<()> {
378 Ok(())
379 }
380
381 #[inline]
382 fn snapshot_index(&self) -> Index {
383 self.base_index
384 }
385
386 fn snapshot(&self) -> Option<Snapshot> {
387 self.snapshot.as_ref().map(|data| {
388 Snapshot::with_config(
389 self.base_index,
390 self.base_term,
391 self.snapshot_config.clone(),
392 data.clone(),
393 )
394 })
395 }
396
397 fn apply_snapshot(&mut self, snapshot: &Snapshot) -> Result<()> {
398 // A snapshot no newer than the one we hold tells us nothing.
399 if snapshot.index <= self.base_index {
400 return Ok(());
401 }
402 // Keep the tail only if our log agrees with the snapshot at its boundary.
403 if self.term_at(snapshot.index) == Some(snapshot.term) {
404 let drop = ((snapshot.index - self.base_index) as usize).min(self.entries.len());
405 let _ = self.entries.drain(0..drop);
406 } else {
407 self.entries.clear();
408 }
409 self.base_index = snapshot.index;
410 self.base_term = snapshot.term;
411 self.snapshot = Some(snapshot.data.clone());
412 self.snapshot_config = snapshot.config.clone();
413 Ok(())
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 #![allow(clippy::unwrap_used, clippy::expect_used)]
420
421 use super::*;
422
423 fn entry(term: Term, index: Index) -> LogEntry {
424 LogEntry::new(term, index, vec![index as u8])
425 }
426
427 #[test]
428 fn test_empty_log_reports_zero() {
429 let log = MemoryLog::new();
430 assert_eq!(log.last_index(), 0);
431 assert_eq!(log.last_term(), 0);
432 assert!(log.is_empty());
433 assert_eq!(log.entry(0), None);
434 assert_eq!(log.entry(1), None);
435 }
436
437 #[test]
438 fn test_term_at_sentinel_is_zero() {
439 assert_eq!(MemoryLog::new().term_at(0), Some(0));
440 }
441
442 #[test]
443 fn test_append_and_read_back() {
444 let mut log = MemoryLog::new();
445 log.append(&[entry(1, 1), entry(1, 2)]).unwrap();
446 log.append(&[entry(2, 3)]).unwrap();
447 assert_eq!(log.last_index(), 3);
448 assert_eq!(log.last_term(), 2);
449 assert_eq!(log.term_at(2), Some(1));
450 assert_eq!(log.term_at(3), Some(2));
451 assert_eq!(log.term_at(4), None);
452 assert_eq!(log.entry(3).unwrap().term, 2);
453 }
454
455 #[test]
456 fn test_append_empty_is_noop() {
457 let mut log = MemoryLog::new();
458 log.append(&[]).unwrap();
459 assert_eq!(log.last_index(), 0);
460 }
461
462 #[test]
463 fn test_append_rejects_gap() {
464 let mut log = MemoryLog::new();
465 let err = log.append(&[entry(1, 2)]).unwrap_err();
466 assert!(matches!(err, Error::Storage { .. }));
467 }
468
469 #[test]
470 fn test_append_rejects_internally_noncontiguous_batch() {
471 let mut log = MemoryLog::new();
472 let err = log.append(&[entry(1, 1), entry(1, 3)]).unwrap_err();
473 assert!(matches!(err, Error::Storage { .. }));
474 }
475
476 #[test]
477 fn test_entries_range_inclusive() {
478 let mut log = MemoryLog::new();
479 log.append(&[entry(1, 1), entry(1, 2), entry(2, 3), entry(2, 4)])
480 .unwrap();
481 let mid = log.entries(2, 3);
482 assert_eq!(mid.len(), 2);
483 assert_eq!(mid[0].index, 2);
484 assert_eq!(mid[1].index, 3);
485 }
486
487 #[test]
488 fn test_entries_range_clamps_and_handles_empty() {
489 let mut log = MemoryLog::new();
490 log.append(&[entry(1, 1), entry(1, 2)]).unwrap();
491 // Past the end is clamped.
492 assert_eq!(log.entries(1, 99).len(), 2);
493 // Empty / degenerate ranges yield nothing.
494 assert!(log.entries(3, 2).is_empty());
495 assert!(log.entries(0, 5).is_empty());
496 assert!(log.entries(5, 9).is_empty());
497 }
498
499 #[test]
500 fn test_default_entries_matches_override() {
501 // Drive the trait's default `entries` impl through a wrapper that does
502 // not override it, and confirm it agrees with `MemoryLog`'s bulk read.
503 struct Wrap(MemoryLog);
504 impl RaftLog for Wrap {
505 fn last_index(&self) -> Index {
506 self.0.last_index()
507 }
508 fn last_term(&self) -> Term {
509 self.0.last_term()
510 }
511 fn term_at(&self, index: Index) -> Option<Term> {
512 self.0.term_at(index)
513 }
514 fn entry(&self, index: Index) -> Option<LogEntry> {
515 self.0.entry(index)
516 }
517 fn append(&mut self, entries: &[LogEntry]) -> Result<()> {
518 self.0.append(entries)
519 }
520 fn truncate(&mut self, from: Index) -> Result<()> {
521 self.0.truncate(from)
522 }
523 fn hard_state(&self) -> HardState {
524 self.0.hard_state()
525 }
526 fn set_hard_state(&mut self, state: HardState) -> Result<()> {
527 self.0.set_hard_state(state)
528 }
529 fn sync(&mut self) -> Result<()> {
530 self.0.sync()
531 }
532 }
533 let mut inner = MemoryLog::new();
534 inner
535 .append(&[entry(1, 1), entry(1, 2), entry(2, 3)])
536 .unwrap();
537 let wrap = Wrap(inner.clone());
538 assert_eq!(wrap.entries(1, 3), inner.entries(1, 3));
539 assert_eq!(wrap.entries(2, 2), inner.entries(2, 2));
540 }
541
542 #[test]
543 fn test_truncate_removes_tail() {
544 let mut log = MemoryLog::new();
545 log.append(&[entry(1, 1), entry(1, 2), entry(1, 3)])
546 .unwrap();
547 log.truncate(2).unwrap();
548 assert_eq!(log.last_index(), 1);
549 assert_eq!(log.entry(2), None);
550 }
551
552 #[test]
553 fn test_truncate_past_end_is_noop() {
554 let mut log = MemoryLog::new();
555 log.append(&[entry(1, 1)]).unwrap();
556 log.truncate(5).unwrap();
557 assert_eq!(log.last_index(), 1);
558 }
559
560 #[test]
561 fn test_truncate_zero_is_rejected() {
562 let mut log = MemoryLog::new();
563 assert!(log.truncate(0).is_err());
564 }
565
566 #[test]
567 fn test_hard_state_round_trips() {
568 let mut log = MemoryLog::new();
569 let hs = HardState {
570 term: 4,
571 voted_for: Some(2),
572 };
573 log.set_hard_state(hs).unwrap();
574 assert_eq!(log.hard_state(), hs);
575 }
576
577 #[test]
578 fn test_sync_is_ok() {
579 assert!(MemoryLog::new().sync().is_ok());
580 }
581
582 // ---- compaction / snapshots -------------------------------------------
583
584 #[test]
585 fn test_apply_snapshot_compacts_and_keeps_matching_tail() {
586 let mut log = MemoryLog::new();
587 log.append(&[entry(1, 1), entry(1, 2), entry(2, 3), entry(2, 4)])
588 .unwrap();
589 // Snapshot through index 2 (term 1) — our log matches there, keep the tail.
590 log.apply_snapshot(&Snapshot::new(2, 1, b"state@2".to_vec()))
591 .unwrap();
592
593 assert_eq!(log.snapshot_index(), 2);
594 assert_eq!(log.last_index(), 4);
595 // Compacted entries are gone, the boundary term is still answerable.
596 assert_eq!(log.entry(1), None);
597 assert_eq!(log.entry(2), None);
598 assert_eq!(log.term_at(2), Some(1)); // boundary
599 assert_eq!(log.term_at(1), None); // below boundary
600 // The tail survived.
601 assert_eq!(log.entry(3).unwrap().term, 2);
602 assert_eq!(log.entry(4).unwrap().index, 4);
603 // The snapshot is retrievable.
604 assert_eq!(log.snapshot().unwrap().data, b"state@2");
605 }
606
607 #[test]
608 fn test_apply_snapshot_clears_log_on_mismatch() {
609 let mut log = MemoryLog::new();
610 log.append(&[entry(1, 1), entry(1, 2)]).unwrap();
611 // A snapshot at index 5 our log cannot match supersedes everything.
612 log.apply_snapshot(&Snapshot::new(5, 3, b"state@5".to_vec()))
613 .unwrap();
614 assert_eq!(log.snapshot_index(), 5);
615 assert_eq!(log.last_index(), 5);
616 assert_eq!(log.last_term(), 3);
617 assert!(log.entries(1, 5).is_empty());
618 assert_eq!(log.term_at(5), Some(3));
619 }
620
621 #[test]
622 fn test_append_continues_after_snapshot() {
623 let mut log = MemoryLog::new();
624 log.apply_snapshot(&Snapshot::new(7, 2, b"base".to_vec()))
625 .unwrap();
626 assert_eq!(log.last_index(), 7);
627 // Next append must be contiguous with the snapshot boundary.
628 assert!(log.append(&[entry(2, 7)]).is_err()); // 7 already covered
629 log.append(&[entry(3, 8), entry(3, 9)]).unwrap();
630 assert_eq!(log.last_index(), 9);
631 assert_eq!(log.entry(8).unwrap().term, 3);
632 assert_eq!(log.term_at(7), Some(2)); // boundary term preserved
633 }
634
635 #[test]
636 fn test_stale_snapshot_is_ignored() {
637 let mut log = MemoryLog::new();
638 log.apply_snapshot(&Snapshot::new(5, 2, b"new".to_vec()))
639 .unwrap();
640 log.apply_snapshot(&Snapshot::new(3, 1, b"old".to_vec()))
641 .unwrap();
642 assert_eq!(log.snapshot_index(), 5);
643 assert_eq!(log.snapshot().unwrap().data, b"new");
644 }
645
646 #[test]
647 fn test_truncate_into_snapshot_is_rejected() {
648 let mut log = MemoryLog::new();
649 log.apply_snapshot(&Snapshot::new(5, 2, b"s".to_vec()))
650 .unwrap();
651 assert!(log.truncate(5).is_err());
652 assert!(log.truncate(3).is_err());
653 }
654
655 #[test]
656 fn test_default_apply_snapshot_errors() {
657 // The trait's default `apply_snapshot` rejects, so a snapshot-unaware
658 // backend fails loudly.
659 struct NoSnap(MemoryLog);
660 impl RaftLog for NoSnap {
661 fn last_index(&self) -> Index {
662 self.0.last_index()
663 }
664 fn last_term(&self) -> Term {
665 self.0.last_term()
666 }
667 fn term_at(&self, index: Index) -> Option<Term> {
668 self.0.term_at(index)
669 }
670 fn entry(&self, index: Index) -> Option<LogEntry> {
671 self.0.entry(index)
672 }
673 fn append(&mut self, entries: &[LogEntry]) -> Result<()> {
674 self.0.append(entries)
675 }
676 fn truncate(&mut self, from: Index) -> Result<()> {
677 self.0.truncate(from)
678 }
679 fn hard_state(&self) -> HardState {
680 self.0.hard_state()
681 }
682 fn set_hard_state(&mut self, state: HardState) -> Result<()> {
683 self.0.set_hard_state(state)
684 }
685 fn sync(&mut self) -> Result<()> {
686 self.0.sync()
687 }
688 }
689 let mut log = NoSnap(MemoryLog::new());
690 assert_eq!(log.snapshot_index(), 0);
691 assert!(log.snapshot().is_none());
692 assert!(log.apply_snapshot(&Snapshot::new(1, 1, vec![])).is_err());
693 }
694}