Skip to content

Commit 30f1ae6

Browse files
committed
feat: add ChangeMembers::Batch(Vec<ChangeMembers>) for batched membership changes
This commit introduces the `ChangeMembers::Batch(Vec<ChangeMembers>)` variant, allowing `Raft::change_membership()` to accept an arbitrary series of membership configuration changes as a single input command. This enables users to define complex membership changes by combining multiple `ChangeMembers` operations. ### Example: The following example demonstrates removing nodes 1 and 2, and adding node 3 in a two-step joint membership configuration change: ```rust my_raft.change_membership(ChangeMembers::Batch(vec![ ChangeMembers::RemoveVoters(btreeset! {1, 2}), ChangeMembers::AddVoters(btreemap! {3 => ()}), ])); ```
1 parent c09290f commit 30f1ae6

File tree

4 files changed

+94
-17
lines changed

4 files changed

+94
-17
lines changed

openraft/src/change_members.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ where C: RaftTypeConfig
5555
/// Every voter has to have a corresponding node in the new
5656
/// set, otherwise it returns [`error::LearnerNotFound`](`crate::error::LearnerNotFound`) error.
5757
ReplaceAllNodes(BTreeMap<C::NodeId, C::Node>),
58+
59+
/// Apply multiple changes to membership config.
60+
///
61+
/// The changes are applied in the order they are given.
62+
/// And it still finishes in a two-step joint config change.
63+
Batch(Vec<ChangeMembers<C>>),
5864
}
5965

6066
/// Convert a series of ids to a `Replace` operation.

openraft/src/core/sm/worker.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use crate::entry::RaftEntry;
2020
use crate::entry::RaftPayload;
2121
use crate::raft::responder::Responder;
2222
use crate::raft::ClientWriteResponse;
23+
#[cfg(doc)]
24+
use crate::storage::RaftLogStorage;
2325
use crate::storage::RaftStateMachine;
2426
use crate::storage::Snapshot;
2527
use crate::type_config::alias::JoinHandleOf;
@@ -43,8 +45,6 @@ where
4345
state_machine: SM,
4446

4547
/// Read logs from the [`RaftLogStorage`] implementation to apply them to state machine.
46-
///
47-
/// [`RaftLogStorage`]: `crate::storage::RaftLogStorage`
4848
log_reader: LR,
4949

5050
/// Read command from RaftCore to execute.

openraft/src/membership/membership.rs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,26 +315,59 @@ where C: RaftTypeConfig
315315
pub(crate) fn change(mut self, change: ChangeMembers<C>, retain: bool) -> Result<Self, ChangeMembershipError<C>> {
316316
tracing::debug!(change = debug(&change), "{}", func_name!());
317317

318+
let Membership { mut configs, nodes } = self.clone().compute_target_membership(change);
319+
320+
// Safe unwrap(): `calculate_goal()` yields a uniform config.
321+
let target_voter_ids = configs.pop().unwrap();
322+
323+
self.nodes = nodes;
324+
let new_membership = self.next_coherent(target_voter_ids, retain);
325+
326+
tracing::debug!(new_membership = display(&new_membership), "new membership");
327+
328+
new_membership.ensure_valid()?;
329+
330+
Ok(new_membership)
331+
}
332+
333+
/// Compute the target membership configuration by applying a membership change.
334+
///
335+
/// This method:
336+
/// - Uses only the last config entry from the current membership. If there are multiple
337+
/// entries, it indicates an ongoing joint consensus change. The last entry represents the
338+
/// target configuration toward which the cluster is transitioning.
339+
/// - Applies the specified membership change to create a new target configuration
340+
/// - Returns a new `Membership` with the target voter IDs and nodes
341+
///
342+
/// Note: This is an intermediate step in membership changes. The result may need to be
343+
/// transformed into a coherent configuration before being applied.
344+
fn compute_target_membership(mut self, change: ChangeMembers<C>) -> Membership<C> {
318345
let last = self.get_joint_config().last().cloned().unwrap_or_default();
319346

320-
let new_membership = match change {
347+
match change {
321348
ChangeMembers::AddVoterIds(add_voter_ids) => {
322349
let new_voter_ids = last.union(&add_voter_ids).cloned().collect::<BTreeSet<_>>();
323-
self.next_coherent(new_voter_ids, retain)
350+
self.configs = vec![new_voter_ids];
351+
self
324352
}
325353
ChangeMembers::AddVoters(add_voters) => {
326354
// Add nodes without overriding existent
327355
self.nodes = Self::extend_nodes(self.nodes, &add_voters);
328356

329357
let add_voter_ids = add_voters.keys().cloned().collect::<BTreeSet<_>>();
330358
let new_voter_ids = last.union(&add_voter_ids).cloned().collect::<BTreeSet<_>>();
331-
self.next_coherent(new_voter_ids, retain)
359+
self.configs = vec![new_voter_ids];
360+
self
332361
}
333362
ChangeMembers::RemoveVoters(remove_voter_ids) => {
334363
let new_voter_ids = last.difference(&remove_voter_ids).cloned().collect::<BTreeSet<_>>();
335-
self.next_coherent(new_voter_ids, retain)
364+
self.configs = vec![new_voter_ids];
365+
self
366+
}
367+
ChangeMembers::ReplaceAllVoters(all_voter_ids) => {
368+
self.configs = vec![all_voter_ids];
369+
self
336370
}
337-
ChangeMembers::ReplaceAllVoters(all_voter_ids) => self.next_coherent(all_voter_ids, retain),
338371
ChangeMembers::AddNodes(add_nodes) => {
339372
// When adding nodes, do not override existing node
340373
for (node_id, node) in add_nodes.into_iter() {
@@ -358,13 +391,13 @@ where C: RaftTypeConfig
358391
self.nodes = all_nodes;
359392
self
360393
}
361-
};
362-
363-
tracing::debug!(new_membership = display(&new_membership), "new membership");
364-
365-
new_membership.ensure_valid()?;
366-
367-
Ok(new_membership)
394+
ChangeMembers::Batch(batch) => {
395+
for change in batch {
396+
self = self.compute_target_membership(change);
397+
}
398+
self
399+
}
400+
}
368401
}
369402

370403
/// Build a QuorumSet from current joint config
@@ -597,4 +630,39 @@ mod tests {
597630

598631
Ok(())
599632
}
633+
634+
/// Test membership change desribed by a batch operations.
635+
///
636+
/// The batch operations add one voter and remove another.
637+
/// It still finish in a two step joint config change.
638+
#[test]
639+
fn test_membership_change_batch() -> anyhow::Result<()> {
640+
let m = || Membership::<UTConfig> {
641+
configs: vec![btreeset! {1,2}],
642+
nodes: btreemap! {1=>(),2=>(),3=>()},
643+
};
644+
645+
let rm_2_add_5 = || {
646+
ChangeMembers::Batch(vec![
647+
ChangeMembers::RemoveVoters(btreeset! {2}),
648+
ChangeMembers::AddVoters(btreemap! {5=>()}),
649+
])
650+
};
651+
652+
let step1 = m().change(rm_2_add_5(), false)?;
653+
654+
assert_eq!(step1, Membership::<UTConfig> {
655+
configs: vec![btreeset! {1,2}, btreeset! {1,5}],
656+
nodes: btreemap! {1=>(),2=>(),3=>(),5=>()}
657+
});
658+
659+
let step2 = step1.change(rm_2_add_5(), false)?;
660+
661+
assert_eq!(step2, Membership::<UTConfig> {
662+
configs: vec![btreeset! {1,5}],
663+
nodes: btreemap! {1=>(),3=>(), 5=>()}
664+
});
665+
666+
Ok(())
667+
}
600668
}

openraft/src/membership/membership_test.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,17 @@ fn test_membership_add_learner() -> anyhow::Result<()> {
141141
// Add learner that presents in old cluster has no effect.
142142

143143
let res = m_1_2.clone().change(ChangeMembers::AddNodes(btreemap! {1=>node("3")}), true)?;
144-
assert_eq!(m_1_2, res);
144+
assert_eq!(
145+
Membership::<UTConfig<TestNode>>::new_unchecked(vec![btreeset! {2}], btreemap! {1=>node("1"), 2=>node("2")},),
146+
res
147+
);
145148

146149
// Success to add a learner
147150

148151
let m_1_2_3 = m_1_2.change(ChangeMembers::AddNodes(btreemap! {3=>node("3")}), true)?;
149152
assert_eq!(
150153
Membership::<UTConfig<TestNode>>::new_unchecked(
151-
vec![btreeset! {1}, btreeset! {2}],
154+
vec![btreeset! {2}],
152155
btreemap! {1=>node("1"), 2=>node("2"), 3=>node("3")}
153156
),
154157
m_1_2_3
@@ -172,7 +175,7 @@ fn test_membership_update_nodes() -> anyhow::Result<()> {
172175
let m_1_2_3 = m_1_2.change(ChangeMembers::SetNodes(btreemap! {2=>node("20"), 3=>node("30")}), true)?;
173176
assert_eq!(
174177
Membership::<UTConfig<TestNode>>::new_unchecked(
175-
vec![btreeset! {1}, btreeset! {2}],
178+
vec![btreeset! {2}],
176179
btreemap! {1=>node("1"), 2=>node("20"), 3=>node("30")}
177180
),
178181
m_1_2_3

0 commit comments

Comments
 (0)