Skip to content

Commit d629dbe

Browse files
authored
Feature: add ChangeMembers::SetNodes (#876)
During dynamic cluster changes, we sometimes need to update an existing node, for example changing its network address. Adding `SetNodes` variant to `ChangeMembers` allows replacing an existing node directly. However, this also carries risk of creating a split brain scenario if used incorrectly. - Fix: #875
1 parent 6098f5c commit d629dbe

File tree

6 files changed

+141
-1
lines changed

6 files changed

+141
-1
lines changed

openraft/src/change_members.rs

+15
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,23 @@ pub enum ChangeMembers<NID: NodeId, N: Node> {
2626
ReplaceAllVoters(BTreeSet<NID>),
2727

2828
/// Add nodes to membership, as learners.
29+
///
30+
/// it **WONT** replace existing node.
31+
///
32+
/// Prefer using this variant instead of `SetNodes` whenever possible, as `AddNodes` ensures
33+
/// safety, whereas incorrect usage of `SetNodes` can result in a brain split.
34+
/// See: [Update-Node](`crate::docs::cluster_control::dynamic_membership#update-node`)
2935
AddNodes(BTreeMap<NID, N>),
3036

37+
/// Add or replace nodes in membership config.
38+
///
39+
/// it **WILL** replace existing node.
40+
///
41+
/// Prefer using `AddNodes` instead of `SetNodes` whenever possible, as `AddNodes` ensures
42+
/// safety, whereas incorrect usage of `SetNodes` can result in a brain split.
43+
/// See: [Update-Node](`crate::docs::cluster_control::dynamic_membership#update-node`)
44+
SetNodes(BTreeMap<NID, N>),
45+
3146
/// Remove nodes from membership.
3247
///
3348
/// If a node is still a voter, it returns

openraft/src/docs/cluster_control/dynamic-membership.md

+45
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,51 @@ tolerates a minority member crash.
7272

7373
To read more about Openraft's [Extended Membership Algorithm][`extended_membership`].
7474

75+
76+
## Update Node
77+
78+
To update a node, such as altering its network address,
79+
the application calls [`Raft::change_membership()`][].
80+
The initial argument should be set to [`ChangeMembers::SetNodes(BTreeMap<NodeId,Node>)`][`ChangeMembers::SetNodes`].
81+
82+
**Warning: Misusing `SetNodes` could lead to a split-brain situation**:
83+
84+
### Brain split
85+
86+
When Updating node network addresses,
87+
brain split could occur if the new address belongs to another node,
88+
leading to two elected leaders.
89+
90+
Consider a 3-node cluster (`a, b, c`, with addresses `x, y, z`) and an
91+
uninitialized node `d` with address `w`:
92+
93+
```text
94+
a: x
95+
b: y
96+
c: z
97+
98+
d: w
99+
```
100+
101+
Mistakenly updating `b`'s address from `y` to `w` would enable both `x, y` and `z, w` to form quorums and elect leaders:
102+
103+
- `c` proposes ChangeMembership: `{a:x, b:w, c:z}`;
104+
- `c, d` grant `c`;
105+
106+
- `c` elects itself as leader
107+
- `c, d` confirm `c` as leader
108+
109+
- `a` elects itself as leader
110+
- `a, b` confirm `a` as leader
111+
112+
113+
Directly updating node addresses with `ChangeMembers::SetNodes`
114+
should be replaced with `ChangeMembers::RemoveNodes` and `ChangeMembers::RemoveNodes` whenever possible.
115+
116+
Do not use `ChangeMembers::SetNodes` unless you know what you are doing.
117+
118+
119+
[`ChangeMembers::SetNodes`]: `crate::change_members::ChangeMembers::SetNodes`
75120
[`Raft::add_learner()`]: `crate::Raft::add_learner`
76121
[`Raft::change_membership()`]: `crate::Raft::change_membership`
77122
[`extended_membership`]: `crate::docs::data::extended_membership`

openraft/src/membership/membership.rs

+23
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,12 @@ where
321321
}
322322
self
323323
}
324+
ChangeMembers::SetNodes(set_nodes) => {
325+
for (node_id, node) in set_nodes.into_iter() {
326+
self.nodes.insert(node_id, node);
327+
}
328+
self
329+
}
324330
ChangeMembers::RemoveNodes(remove_node_ids) => {
325331
for node_id in remove_node_ids.iter() {
326332
self.nodes.remove(node_id);
@@ -517,6 +523,23 @@ mod tests {
517523
);
518524
}
519525

526+
// SetNodes: Ok
527+
{
528+
let m = || Membership::<u64, u64> {
529+
configs: vec![btreeset! {1,2}],
530+
nodes: btreemap! {1=>1,2=>2,3=>3},
531+
};
532+
533+
let res = m().change(ChangeMembers::SetNodes(btreemap! {3=>30, 4=>40}), false);
534+
assert_eq!(
535+
Ok(Membership::<u64, u64> {
536+
configs: vec![btreeset! {1,2}],
537+
nodes: btreemap! {1=>1,2=>2,3=>30, 4=>40}
538+
}),
539+
res
540+
);
541+
}
542+
520543
// RemoveNodes: can not remove node for voter
521544
{
522545
let res = m().change(ChangeMembers::RemoveNodes(btreeset! {2}), false);

openraft/src/membership/membership_test.rs

+24
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,30 @@ fn test_membership_add_learner() -> anyhow::Result<()> {
154154
Ok(())
155155
}
156156

157+
#[test]
158+
fn test_membership_update_nodes() -> anyhow::Result<()> {
159+
let node = |s: &str| TestNode {
160+
addr: s.to_string(),
161+
data: Default::default(),
162+
};
163+
164+
let m_1_2 = Membership::<u64, TestNode>::new_unchecked(
165+
vec![btreeset! {1}, btreeset! {2}],
166+
btreemap! {1=>node("1"), 2=>node("2")},
167+
);
168+
169+
let m_1_2_3 = m_1_2.change(ChangeMembers::SetNodes(btreemap! {2=>node("20"), 3=>node("30")}), true)?;
170+
assert_eq!(
171+
Membership::<u64, TestNode>::new_unchecked(
172+
vec![btreeset! {1}, btreeset! {2}],
173+
btreemap! {1=>node("1"), 2=>node("20"), 3=>node("30")}
174+
),
175+
m_1_2_3
176+
);
177+
178+
Ok(())
179+
}
180+
157181
#[test]
158182
fn test_membership_extend_nodes() -> anyhow::Result<()> {
159183
let node = |s: &str| TestNode {

tests/tests/membership/t11_add_learner.rs

+33
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use std::sync::Arc;
22
use std::time::Duration;
33

44
use anyhow::Result;
5+
use maplit::btreemap;
56
use maplit::btreeset;
67
use openraft::error::ChangeMembershipError;
78
use openraft::error::ClientWriteError;
89
use openraft::error::InProgress;
910
use openraft::storage::RaftLogReaderExt;
11+
use openraft::ChangeMembers;
1012
use openraft::CommittedLeaderId;
1113
use openraft::Config;
1214
use openraft::LogId;
@@ -141,6 +143,37 @@ async fn add_learner_non_blocking() -> Result<()> {
141143
Ok(())
142144
}
143145

146+
#[async_entry::test(worker_threads = 8, init = "init_default_ut_tracing()", tracing_span = "debug")]
147+
async fn add_learner_with_set_nodes() -> Result<()> {
148+
// Add learners and update nodes with ChangeMembers::SetNodes
149+
// Node updating is ensured by unit tests of ChangeMembers
150+
let config = Arc::new(
151+
Config {
152+
replication_lag_threshold: 0,
153+
enable_tick: false,
154+
..Default::default()
155+
}
156+
.validate()?,
157+
);
158+
let mut router = RaftRouter::new(config.clone());
159+
160+
let log_index = router.new_cluster(btreeset! {0,1,2}, btreeset! {}).await?;
161+
162+
tracing::info!(log_index, "--- set node 2 and 4");
163+
{
164+
router.new_raft_node(4).await;
165+
166+
let raft = router.get_raft_handle(&0)?;
167+
raft.change_membership(ChangeMembers::SetNodes(btreemap! {2=>(), 4=>()}), true).await?;
168+
169+
let metrics = router.get_raft_handle(&0)?.metrics().borrow().clone();
170+
let node_ids = metrics.membership_config.membership().nodes().map(|x| *x.0).collect::<Vec<_>>();
171+
assert_eq!(vec![0, 1, 2, 4], node_ids);
172+
}
173+
174+
Ok(())
175+
}
176+
144177
/// When the previous membership is not yet committed, add-learner should fail.
145178
///
146179
/// Because adding learner is also a change-membership operation, a new membership config log will

tests/tests/membership/t20_change_membership.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use openraft::ServerState;
1212
use crate::fixtures::init_default_ut_tracing;
1313
use crate::fixtures::RaftRouter;
1414

15-
/// When a change-membership log is committed, the membership_state should be updated.
15+
/// When a change-membership log is committed, the `RaftState.membership_state` should be updated.
1616
#[async_entry::test(worker_threads = 3, init = "init_default_ut_tracing()", tracing_span = "debug")]
1717
async fn update_membership_state() -> anyhow::Result<()> {
1818
let config = Arc::new(

0 commit comments

Comments
 (0)