@@ -315,26 +315,59 @@ where C: RaftTypeConfig
315
315
pub ( crate ) fn change ( mut self , change : ChangeMembers < C > , retain : bool ) -> Result < Self , ChangeMembershipError < C > > {
316
316
tracing:: debug!( change = debug( & change) , "{}" , func_name!( ) ) ;
317
317
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 > {
318
345
let last = self . get_joint_config ( ) . last ( ) . cloned ( ) . unwrap_or_default ( ) ;
319
346
320
- let new_membership = match change {
347
+ match change {
321
348
ChangeMembers :: AddVoterIds ( add_voter_ids) => {
322
349
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
324
352
}
325
353
ChangeMembers :: AddVoters ( add_voters) => {
326
354
// Add nodes without overriding existent
327
355
self . nodes = Self :: extend_nodes ( self . nodes , & add_voters) ;
328
356
329
357
let add_voter_ids = add_voters. keys ( ) . cloned ( ) . collect :: < BTreeSet < _ > > ( ) ;
330
358
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
332
361
}
333
362
ChangeMembers :: RemoveVoters ( remove_voter_ids) => {
334
363
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
336
370
}
337
- ChangeMembers :: ReplaceAllVoters ( all_voter_ids) => self . next_coherent ( all_voter_ids, retain) ,
338
371
ChangeMembers :: AddNodes ( add_nodes) => {
339
372
// When adding nodes, do not override existing node
340
373
for ( node_id, node) in add_nodes. into_iter ( ) {
@@ -358,13 +391,13 @@ where C: RaftTypeConfig
358
391
self . nodes = all_nodes;
359
392
self
360
393
}
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
+ }
368
401
}
369
402
370
403
/// Build a QuorumSet from current joint config
@@ -597,4 +630,39 @@ mod tests {
597
630
598
631
Ok ( ( ) )
599
632
}
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
+ }
600
668
}
0 commit comments