Skip to content

Commit 674e78a

Browse files
committed
Fix: potential inconsistency when installing snapshot
The conflicting logs that are before `snapshot_meta.last_log_Id` should be deleted before installing a snapshot. Otherwise there is chance the snapshot is installed but conflicting logs are left in the store, when a node crashes.
1 parent a613ac5 commit 674e78a

File tree

6 files changed

+76
-25
lines changed

6 files changed

+76
-25
lines changed

guide/src/replication.md

+30-14
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ R5 2 4 4
3333
If log 5 is committed by R1, and log 3 is not removed, R5 in future could become a new leader and overrides log
3434
5 on R3.
3535

36-
### Caveat: deleting all entries after `prev_log_id` get committed log lost
36+
### Caveat: deleting all entries after `prev_log_id` will get committed log lost
3737

38-
One of the mistake is to delete all entries after `prev_log_id` when a matching `prev_log_id` is found, e.g.:
38+
One of the mistakes is to delete all entries after `prev_log_id` when a matching `prev_log_id` is found, e.g.:
3939
```
4040
fn handle_append_entries(req) {
4141
if store.has(req.prev_log_id) {
@@ -95,23 +95,39 @@ Similar to append-entry:
9595
- (1) If the logs contained in the snapshot matches logs that are stored on a
9696
Follower/Learner, nothing is done.
9797

98-
- (2) If the logs conflicts with the local logs, local conflicting logs will be
99-
deleted. And effective membership has to be reverted to some previous
100-
non-conflicting one.
98+
- (2) If the logs conflicts with the local logs, **ALL** non-committed logs will be
99+
deleted, because we do not know which logs conflict.
100+
And effective membership has to be reverted to some previous non-conflicting one.
101101

102102

103-
### Necessity to delete conflicting logs
103+
### Delete conflicting logs
104104

105-
**The `(2)` mentioned above is not necessary to do to achieve correctness.
106-
It is done only for clarity**.
107-
108-
If the `last_applied`(`snapshot_meta.last_log_id`) conflict with the local log at `last_applied.index`,
109-
It does **NOT** need to delete the conflicting logs.
105+
If `snapshot_meta.last_log_id` conflicts with the local log,
110106

111107
Because the node that has conflicting logs won't become a leader:
112108
If this node can become a leader, according to raft spec, it has to contain all committed logs.
113109
But the log entry at `last_applied.index` is not committed, thus it can never become a leader.
114110

115-
But deleting conflicting logs make the state cleaner. :)
116-
This way method such as `get_initial_state()` does not need to deal with
117-
conditions such as that `last_log_id` can be smaller than `last_applied`.
111+
But, it could become a leader when more logs are received.
112+
At this time, the logs after `snapshot_meta.last_log_id` will all be cleaned.
113+
The logs before or equal `snapshot_meta.last_log_id` will not be cleaned.
114+
115+
Then there is chance this node becomes leader and uses these log for replication.
116+
117+
#### Delete all non-committed
118+
119+
It just truncates **ALL** non-committed logs here,
120+
because `snapshot_meta.last_log_id` is committed, if the local log id conflicts
121+
with `snapshot_meta.last_log_id`, there must be a quorum that contains `snapshot_meta.last_log_id`.
122+
Thus, it is **safe to remove all logs** on this node.
123+
124+
But removing committed logs leads to some trouble with membership management.
125+
Thus, we just remove logs since `committed+1`.
126+
127+
#### Not safe to clean conflicting logs after installing snapshot
128+
129+
It's not safe to remove the conflicting logs that are less than `snap_last_log_id` after installing
130+
snapshot.
131+
132+
If the node crashes, dirty logs may remain there. These logs may be forwarded to other nodes if this nodes
133+
becomes a leader.

openraft/src/engine/engine_impl.rs

+19-5
Original file line numberDiff line numberDiff line change
@@ -760,17 +760,31 @@ where
760760
}
761761

762762
// Do install:
763-
// 1. Truncate conflicting logs.
763+
// 1. Truncate all logs if conflict
764764
// Unlike normal append-entries RPC, if conflicting logs are found, it is not **necessary** to delete them.
765765
// But cleaning them make the assumption of incremental-log-id always hold, which makes it easier to debug.
766766
// See: [Snapshot-replication](https://datafuselabs.github.io/openraft/replication.html#snapshot-replication)
767+
//
768+
// Truncate all:
769+
//
770+
// It just truncate **ALL** logs here, because `snap_last_log_id` is committed, if the local log id conflicts
771+
// with `snap_last_log_id`, there must be a quorum that contains `snap_last_log_id`.
772+
// Thus it is safe to remove all logs on this node.
773+
//
774+
// The logs before `snap_last_log_id` may conflicts with the leader too.
775+
// It's not safe to remove the conflicting logs that are less than `snap_last_log_id` after installing
776+
// snapshot.
777+
//
778+
// If the node crashes, dirty logs may remain there. These logs may be forwarded to other nodes if this nodes
779+
// becomes a leader.
780+
//
767781
// 2. Install snapshot.
768-
// 3. purge maybe conflicting logs upto snapshot.last_log_id.
769782

770783
let local = self.state.get_log_id(snap_last_log_id.index);
771784
if let Some(local) = local {
772785
if local != snap_last_log_id {
773-
self.truncate_logs(snap_last_log_id.index);
786+
// Delete non-committed logs.
787+
self.truncate_logs(self.state.committed.next_index());
774788
}
775789
}
776790

@@ -783,8 +797,8 @@ where
783797
// leader to synchronize its snapshot data.
784798
self.push_command(Command::InstallSnapshot { snapshot_meta: meta });
785799

786-
// A local log that is <= snap_last_log_id may conflict with the leader.
787-
// It has to purge all of them to prevent these log form being replicated, when this node becomes leader.
800+
// A local log that is <= snap_last_log_id can not conflict with the leader.
801+
// But there will be a hole in the logs. Thus it's better remove all logs.
788802
self.purge_log(snap_last_log_id)
789803
}
790804

openraft/src/engine/install_snapshot_test.rs

+24-3
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,29 @@ fn test_install_snapshot_not_conflict() -> anyhow::Result<()> {
196196

197197
#[test]
198198
fn test_install_snapshot_conflict() -> anyhow::Result<()> {
199-
// Snapshot will be installed and there are no conflicting logs.
200-
let mut eng = eng();
199+
// Snapshot will be installed, all non-committed log will be deleted.
200+
// And there should be no conflicting logs left.
201+
let mut eng = {
202+
let mut eng = Engine::<u64, ()> {
203+
snapshot_meta: SnapshotMeta {
204+
last_log_id: Some(log_id(2, 2)),
205+
last_membership: EffectiveMembership::new(Some(log_id(1, 1)), m12()),
206+
snapshot_id: "1-2-3-4".to_string(),
207+
},
208+
..Default::default()
209+
};
210+
211+
eng.state.committed = Some(log_id(2, 3));
212+
eng.state.log_ids = LogIdList::new(vec![
213+
//
214+
log_id(2, 2),
215+
log_id(3, 5),
216+
log_id(4, 6),
217+
log_id(4, 8),
218+
]);
219+
220+
eng
221+
};
201222

202223
eng.install_snapshot(SnapshotMeta {
203224
last_log_id: Some(log_id(5, 6)),
@@ -231,7 +252,7 @@ fn test_install_snapshot_conflict() -> anyhow::Result<()> {
231252

232253
assert_eq!(
233254
vec![
234-
Command::DeleteConflictLog { since: log_id(4, 6) },
255+
Command::DeleteConflictLog { since: log_id(2, 4) },
235256
Command::UpdateMembership {
236257
membership: Arc::new(EffectiveMembership::new(Some(log_id(1, 1)), m1234()))
237258
},

openraft/tests/snapshot/t43_snapshot_delete_conflict_logs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async fn snapshot_delete_conflicting_logs() -> Result<()> {
113113
payload: EntryPayload::Membership(Membership::new(vec![btreeset! {4,5}], None)),
114114
},
115115
],
116-
leader_commit: Some(LogId::new(LeaderId::new(0, 0), 0)),
116+
leader_commit: Some(LogId::new(LeaderId::new(1, 0), 2)),
117117
};
118118
router.new_client(1, &()).await?.send_append_entries(req).await?;
119119

rust-toolchain

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
nightly-2022-08-11
1+
nightly-2022-09-19

sledstore/src/test.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl StoreBuilder<ExampleTypeConfig, Arc<SledStore>> for SledBuilder {
4646
let test_res = t(store).await;
4747

4848
if db_dir.exists() {
49-
std::fs::remove_dir_all(&db_dir).expect("Could not clean up test directory");
49+
std::fs::remove_dir_all(db_dir).expect("Could not clean up test directory");
5050
}
5151
test_res
5252
};

0 commit comments

Comments
 (0)