Skip to content

Commit a2a48c5

Browse files
committed
feature: make DefensiveCheck a reuseable trait
- Defensive checks in `MemStore` are moved out into a trait `DefensiveCheck`. - Let user impl a base `RaftStorage`. Then raft wraps it with a `StoreExt` thus the defensive checks apply to every impl of `RaftStorage`.
1 parent 1448c29 commit a2a48c5

9 files changed

Lines changed: 632 additions & 452 deletions

File tree

async-raft/src/defensive.rs

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
use std::collections::Bound;
2+
use std::fmt::Debug;
3+
use std::ops::RangeBounds;
4+
5+
use async_trait::async_trait;
6+
7+
use crate::raft::Entry;
8+
use crate::storage::HardState;
9+
use crate::AppData;
10+
use crate::AppDataResponse;
11+
use crate::DefensiveError;
12+
use crate::ErrorSubject;
13+
use crate::LogId;
14+
use crate::RaftStorage;
15+
use crate::StorageError;
16+
use crate::Violation;
17+
use crate::Wrapper;
18+
19+
/// Defines methods of defensive checks for RaftStorage.
20+
#[async_trait]
21+
pub trait DefensiveCheck<D, R, T>
22+
where
23+
D: AppData,
24+
R: AppDataResponse,
25+
T: RaftStorage<D, R>,
26+
Self: Wrapper<T>,
27+
{
28+
/// Enable or disable defensive check when calling storage APIs.
29+
fn set_defensive(&self, v: bool);
30+
31+
fn is_defensive(&self) -> bool;
32+
33+
/// Ensure that logs that have greater index than last_applied should have greater log_id.
34+
/// Invariant must hold: `log.log_id.index > last_applied.index` implies `log.log_id > last_applied`.
35+
async fn defensive_no_dirty_log(&self) -> Result<(), StorageError> {
36+
if !self.is_defensive() {
37+
return Ok(());
38+
}
39+
40+
let (last_applied, _) = self.inner().last_applied_state().await?;
41+
let last_log_id = self.inner().last_id_in_log().await?;
42+
43+
if last_log_id.index > last_applied.index && last_log_id < last_applied {
44+
return Err(
45+
DefensiveError::new(ErrorSubject::Log(last_log_id), Violation::DirtyLog {
46+
higher_index_log_id: last_log_id,
47+
lower_index_log_id: last_applied,
48+
})
49+
.into(),
50+
);
51+
}
52+
53+
Ok(())
54+
}
55+
56+
/// Ensure that current_term must increment for every update, and for every term there could be only one value for
57+
/// voted_for.
58+
async fn defensive_incremental_hard_state(&self, hs: &HardState) -> Result<(), StorageError> {
59+
if !self.is_defensive() {
60+
return Ok(());
61+
}
62+
63+
let h = self.inner().read_hard_state().await?;
64+
65+
let curr = h.unwrap_or_default();
66+
67+
if hs.current_term < curr.current_term {
68+
return Err(
69+
DefensiveError::new(ErrorSubject::HardState, Violation::TermNotAscending {
70+
curr: curr.current_term,
71+
to: hs.current_term,
72+
})
73+
.into(),
74+
);
75+
}
76+
77+
if hs.current_term == curr.current_term && curr.voted_for.is_some() && hs.voted_for != curr.voted_for {
78+
return Err(
79+
DefensiveError::new(ErrorSubject::HardState, Violation::VotedForChanged {
80+
curr,
81+
to: hs.clone(),
82+
})
83+
.into(),
84+
);
85+
}
86+
87+
Ok(())
88+
}
89+
90+
/// The log entries fed into a store must be consecutive otherwise it is a bug.
91+
async fn defensive_consecutive_input(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
92+
if !self.is_defensive() {
93+
return Ok(());
94+
}
95+
96+
if entries.is_empty() {
97+
return Ok(());
98+
}
99+
100+
let mut prev_log_id = entries[0].log_id;
101+
102+
for e in entries.iter().skip(1) {
103+
if e.log_id.index != prev_log_id.index + 1 {
104+
return Err(DefensiveError::new(ErrorSubject::Logs, Violation::LogsNonConsecutive {
105+
prev: prev_log_id,
106+
next: e.log_id,
107+
})
108+
.into());
109+
}
110+
111+
prev_log_id = e.log_id;
112+
}
113+
114+
Ok(())
115+
}
116+
117+
/// Trying to feed in emtpy entries slice is an inappropriate action.
118+
///
119+
/// The impl has to avoid this otherwise it may be a bug.
120+
async fn defensive_nonempty_input(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
121+
if !self.is_defensive() {
122+
return Ok(());
123+
}
124+
125+
if entries.is_empty() {
126+
return Err(DefensiveError::new(ErrorSubject::Logs, Violation::LogsEmpty).into());
127+
}
128+
129+
Ok(())
130+
}
131+
132+
/// The entries to append has to be last_log_id.index + 1
133+
async fn defensive_append_log_index_is_last_plus_one(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
134+
if !self.is_defensive() {
135+
return Ok(());
136+
}
137+
138+
let last_id = self.last_log_id().await?;
139+
140+
let first_id = entries[0].log_id;
141+
if last_id.index + 1 != first_id.index {
142+
return Err(
143+
DefensiveError::new(ErrorSubject::Log(first_id), Violation::LogsNonConsecutive {
144+
prev: last_id,
145+
next: first_id,
146+
})
147+
.into(),
148+
);
149+
}
150+
151+
Ok(())
152+
}
153+
154+
/// The entries to append has to be greater than any known log ids
155+
async fn defensive_append_log_id_gt_last(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
156+
if !self.is_defensive() {
157+
return Ok(());
158+
}
159+
160+
let last_id = self.last_log_id().await?;
161+
162+
let first_id = entries[0].log_id;
163+
if first_id < last_id {
164+
return Err(
165+
DefensiveError::new(ErrorSubject::Log(first_id), Violation::LogsNonConsecutive {
166+
prev: last_id,
167+
next: first_id,
168+
})
169+
.into(),
170+
);
171+
}
172+
173+
Ok(())
174+
}
175+
176+
/// Find the last known log id from log or state machine
177+
/// If no log id found, the default one `0,0` is returned.
178+
async fn last_log_id(&self) -> Result<LogId, StorageError> {
179+
let log_last_id = self.inner().last_id_in_log().await?;
180+
let (sm_last_id, _) = self.inner().last_applied_state().await?;
181+
182+
Ok(std::cmp::max(log_last_id, sm_last_id))
183+
}
184+
185+
/// The entries to apply to state machien has to be last_applied_log_id.index + 1
186+
async fn defensive_apply_index_is_last_applied_plus_one(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
187+
if !self.is_defensive() {
188+
return Ok(());
189+
}
190+
191+
let (last_id, _) = self.inner().last_applied_state().await?;
192+
193+
let first_id = entries[0].log_id;
194+
if last_id.index + 1 != first_id.index {
195+
return Err(
196+
DefensiveError::new(ErrorSubject::Apply(first_id), Violation::ApplyNonConsecutive {
197+
prev: last_id,
198+
next: first_id,
199+
})
200+
.into(),
201+
);
202+
}
203+
204+
Ok(())
205+
}
206+
207+
/// The range must not be empty otherwise it is an inappropriate action.
208+
async fn defensive_nonempty_range<RNG: RangeBounds<u64> + Clone + Debug + Send>(
209+
&self,
210+
range: RNG,
211+
) -> Result<(), StorageError> {
212+
if !self.is_defensive() {
213+
return Ok(());
214+
}
215+
let start = match range.start_bound() {
216+
Bound::Included(i) => Some(*i),
217+
Bound::Excluded(i) => Some(*i + 1),
218+
Bound::Unbounded => None,
219+
};
220+
221+
let end = match range.end_bound() {
222+
Bound::Included(i) => Some(*i),
223+
Bound::Excluded(i) => Some(*i - 1),
224+
Bound::Unbounded => None,
225+
};
226+
227+
if start.is_none() || end.is_none() {
228+
return Ok(());
229+
}
230+
231+
if start > end {
232+
return Err(DefensiveError::new(ErrorSubject::Logs, Violation::RangeEmpty { start, end }).into());
233+
}
234+
235+
Ok(())
236+
}
237+
238+
/// Requires a range must be at least half open: (-oo, n] or [n, +oo);
239+
/// In order to keep logs continuity.
240+
async fn defensive_half_open_range<RNG: RangeBounds<u64> + Clone + Debug + Send>(
241+
&self,
242+
range: RNG,
243+
) -> Result<(), StorageError> {
244+
if !self.is_defensive() {
245+
return Ok(());
246+
}
247+
248+
if let Bound::Unbounded = range.start_bound() {
249+
return Ok(());
250+
};
251+
252+
if let Bound::Unbounded = range.end_bound() {
253+
return Ok(());
254+
};
255+
256+
Err(DefensiveError::new(ErrorSubject::Logs, Violation::RangeNotHalfOpen {
257+
start: range.start_bound().cloned(),
258+
end: range.end_bound().cloned(),
259+
})
260+
.into())
261+
}
262+
263+
/// An range operation such as get or delete has to actually covers some log entries in store.
264+
async fn defensive_range_hits_logs<RNG: RangeBounds<u64> + Debug + Send>(
265+
&self,
266+
range: RNG,
267+
logs: &[Entry<D>],
268+
) -> Result<(), StorageError> {
269+
if !self.is_defensive() {
270+
return Ok(());
271+
}
272+
273+
{
274+
let want_first = match range.start_bound() {
275+
Bound::Included(i) => Some(*i),
276+
Bound::Excluded(i) => Some(*i + 1),
277+
Bound::Unbounded => None,
278+
};
279+
280+
let first = logs.first().map(|x| x.log_id.index);
281+
282+
if let Some(want) = want_first {
283+
if first != want_first {
284+
return Err(
285+
DefensiveError::new(ErrorSubject::LogIndex(want), Violation::LogIndexNotFound {
286+
want,
287+
got: first,
288+
})
289+
.into(),
290+
);
291+
}
292+
}
293+
}
294+
295+
{
296+
let want_last = match range.end_bound() {
297+
Bound::Included(i) => Some(*i),
298+
Bound::Excluded(i) => Some(*i - 1),
299+
Bound::Unbounded => None,
300+
};
301+
302+
let last = logs.last().map(|x| x.log_id.index);
303+
304+
if let Some(want) = want_last {
305+
if last != want_last {
306+
return Err(
307+
DefensiveError::new(ErrorSubject::LogIndex(want), Violation::LogIndexNotFound {
308+
want,
309+
got: last,
310+
})
311+
.into(),
312+
);
313+
}
314+
}
315+
}
316+
317+
Ok(())
318+
}
319+
320+
/// The log id of the entries to apply has to be greater than the last known one.
321+
async fn defensive_apply_log_id_gt_last(&self, entries: &[&Entry<D>]) -> Result<(), StorageError> {
322+
if !self.is_defensive() {
323+
return Ok(());
324+
}
325+
326+
let (last_id, _) = self.inner().last_applied_state().await?;
327+
328+
let first_id = entries[0].log_id;
329+
if first_id < last_id {
330+
return Err(
331+
DefensiveError::new(ErrorSubject::Apply(first_id), Violation::ApplyNonConsecutive {
332+
prev: last_id,
333+
next: first_id,
334+
})
335+
.into(),
336+
);
337+
}
338+
339+
Ok(())
340+
}
341+
}

async-raft/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![doc = include_str!("../README.md")]
22
#![feature(backtrace)]
3+
#![feature(bound_cloned)]
34

45
pub mod config;
56
mod core;
@@ -16,17 +17,23 @@ pub mod storage;
1617
mod storage_error;
1718
mod summary;
1819

20+
mod defensive;
1921
#[cfg(test)]
2022
mod membership_test;
23+
mod store_ext;
24+
mod store_wrapper;
2125

2226
pub use async_trait;
2327
use serde::de::DeserializeOwned;
2428
use serde::Serialize;
29+
pub use store_ext::StoreExt;
30+
pub use store_wrapper::Wrapper;
2531

2632
pub use crate::config::Config;
2733
pub use crate::config::SnapshotPolicy;
2834
pub use crate::core::EffectiveMembership;
2935
pub use crate::core::State;
36+
pub use crate::defensive::DefensiveCheck;
3037
pub use crate::error::ChangeMembershipError;
3138
pub use crate::error::ClientWriteError;
3239
pub use crate::error::ConfigError;

async-raft/src/storage.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,6 @@ where
113113
/// for details on where and how this is used.
114114
type SnapshotData: AsyncRead + AsyncWrite + AsyncSeek + Send + Sync + Unpin + 'static;
115115

116-
/// Set if to turn on defensive check to unexpected input.
117-
/// E.g. discontinuous log appending.
118-
/// The default impl returns `false` to indicate it does impl any defensive check.
119-
async fn defensive(&self, _d: bool) -> bool {
120-
false
121-
}
122-
123116
/// Get the latest membership config found in the log or in state machine.
124117
///
125118
/// This must always be implemented as a reverse search through the log to find the most

0 commit comments

Comments
 (0)