5151
5252import java .util .ArrayList ;
5353import java .util .Arrays ;
54+ import java .util .Collections ;
5455import java .util .HashMap ;
5556import java .util .HashSet ;
5657import java .util .List ;
8485import static org .elasticsearch .node .Node .NODE_NAME_SETTING ;
8586import static org .elasticsearch .transport .TransportService .HANDSHAKE_ACTION_NAME ;
8687import static org .elasticsearch .transport .TransportService .NOOP_TRANSPORT_INTERCEPTOR ;
88+ import static org .hamcrest .Matchers .containsString ;
8789import static org .hamcrest .Matchers .empty ;
90+ import static org .hamcrest .Matchers .endsWith ;
8891import static org .hamcrest .Matchers .equalTo ;
8992import static org .hamcrest .Matchers .greaterThanOrEqualTo ;
9093import static org .hamcrest .Matchers .is ;
9194import static org .hamcrest .Matchers .lessThanOrEqualTo ;
9295import static org .hamcrest .Matchers .not ;
96+ import static org .hamcrest .Matchers .startsWith ;
9397
9498@ TestLogging ("org.elasticsearch.cluster.coordination:TRACE,org.elasticsearch.discovery:TRACE" )
9599public class CoordinatorTests extends ESTestCase {
@@ -404,6 +408,61 @@ public void testAckListenerReceivesNacksFromFollowerInHigherTerm() {
404408// assertTrue("expected ack from " + follower1, ackCollector.hasAckedSuccessfully(follower1));
405409 }
406410
411+ public void testSettingInitialConfigurationTriggersElection () {
412+ final Cluster cluster = new Cluster (randomIntBetween (1 , 5 ));
413+ cluster .runFor (defaultMillis (DISCOVERY_FIND_PEERS_INTERVAL_SETTING ) * 2 + randomLongBetween (0 , 60000 ), "initial discovery phase" );
414+ for (final ClusterNode clusterNode : cluster .clusterNodes ) {
415+ final String nodeId = clusterNode .getId ();
416+ assertThat (nodeId + " is CANDIDATE" , clusterNode .coordinator .getMode (), is (CANDIDATE ));
417+ assertThat (nodeId + " is in term 0" , clusterNode .coordinator .getCurrentTerm (), is (0L ));
418+ assertThat (nodeId + " last accepted in term 0" , clusterNode .coordinator .getLastAcceptedState ().term (), is (0L ));
419+ assertThat (nodeId + " last accepted version 0" , clusterNode .coordinator .getLastAcceptedState ().version (), is (0L ));
420+ assertTrue (nodeId + " has an empty last-accepted configuration" ,
421+ clusterNode .coordinator .getLastAcceptedState ().getLastAcceptedConfiguration ().isEmpty ());
422+ assertTrue (nodeId + " has an empty last-committed configuration" ,
423+ clusterNode .coordinator .getLastAcceptedState ().getLastCommittedConfiguration ().isEmpty ());
424+ }
425+
426+ cluster .getAnyNode ().applyInitialConfiguration ();
427+ cluster .stabilise (defaultMillis (
428+ // the first election should succeed, because only one node knows of the initial configuration and therefore can win a
429+ // pre-voting round and proceed to an election, so there cannot be any collisions
430+ ELECTION_INITIAL_TIMEOUT_SETTING ) // TODO this wait is unnecessary, we could trigger the election immediately
431+ // Allow two round-trip for pre-voting and voting
432+ + 4 * DEFAULT_DELAY_VARIABILITY
433+ // Then a commit of the new leader's first cluster state
434+ + DEFAULT_CLUSTER_STATE_UPDATE_DELAY );
435+ }
436+
437+ public void testCannotSetInitialConfigurationTwice () {
438+ final Cluster cluster = new Cluster (randomIntBetween (1 , 5 ));
439+ cluster .runRandomly ();
440+ cluster .stabilise ();
441+
442+ final Coordinator coordinator = cluster .getAnyNode ().coordinator ;
443+ final CoordinationStateRejectedException exception = expectThrows (CoordinationStateRejectedException .class ,
444+ () -> coordinator .setInitialConfiguration (coordinator .getLastAcceptedState ().getLastCommittedConfiguration ()));
445+
446+ assertThat (exception .getMessage (), is ("Cannot set initial configuration: configuration has already been set" ));
447+ }
448+
449+ public void testCannotSetInitialConfigurationWithoutQuorum () {
450+ final Cluster cluster = new Cluster (randomIntBetween (1 , 5 ));
451+ final Coordinator coordinator = cluster .getAnyNode ().coordinator ;
452+ final VotingConfiguration unknownNodeConfiguration = new VotingConfiguration (Collections .singleton ("unknown-node" ));
453+ final String exceptionMessage = expectThrows (CoordinationStateRejectedException .class ,
454+ () -> coordinator .setInitialConfiguration (unknownNodeConfiguration )).getMessage ();
455+ assertThat (exceptionMessage ,
456+ startsWith ("not enough nodes discovered to form a quorum in the initial configuration [knownNodes=[" ));
457+ assertThat (exceptionMessage ,
458+ endsWith ("], VotingConfiguration{unknown-node}]" ));
459+ assertThat (exceptionMessage , containsString (coordinator .getLocalNode ().toString ()));
460+
461+ // This is VERY BAD: setting a _different_ initial configuration. Yet it works if the first attempt will never be a quorum.
462+ coordinator .setInitialConfiguration (new VotingConfiguration (Collections .singleton (coordinator .getLocalNode ().getId ())));
463+ cluster .stabilise ();
464+ }
465+
407466 private static long defaultMillis (Setting <TimeValue > setting ) {
408467 return setting .get (Settings .EMPTY ).millis () + Cluster .DEFAULT_DELAY_VARIABILITY ;
409468 }
@@ -555,6 +614,14 @@ void runRandomly() {
555614 }
556615 break ;
557616 }
617+ } else if (rarely ()) {
618+ final ClusterNode clusterNode = getAnyNode ();
619+ onNode (clusterNode .getLocalNode (),
620+ () -> {
621+ logger .debug ("----> [runRandomly {}] applying initial configuration {} to {}" ,
622+ thisStep , initialConfiguration , clusterNode .getId ());
623+ clusterNode .coordinator .setInitialConfiguration (initialConfiguration );
624+ }).run ();
558625 } else {
559626 if (deterministicTaskQueue .hasDeferredTasks () && randomBoolean ()) {
560627 deterministicTaskQueue .advanceTime ();
@@ -566,7 +633,6 @@ void runRandomly() {
566633 // TODO other random steps:
567634 // - reboot a node
568635 // - abdicate leadership
569- // - bootstrap
570636
571637 } catch (CoordinationStateRejectedException ignored ) {
572638 // This is ok: it just means a message couldn't currently be handled.
@@ -606,6 +672,17 @@ void stabilise() {
606672 void stabilise (long stabilisationDurationMillis ) {
607673 assertThat ("stabilisation requires default delay variability (and proper cleanup of raised variability)" ,
608674 deterministicTaskQueue .getExecutionDelayVariabilityMillis (), lessThanOrEqualTo (DEFAULT_DELAY_VARIABILITY ));
675+
676+ if (clusterNodes .stream ().allMatch (n -> n .coordinator .getLastAcceptedState ().getLastAcceptedConfiguration ().isEmpty ())) {
677+ assertThat ("setting initial configuration may fail with disconnected nodes" , disconnectedNodes , empty ());
678+ assertThat ("setting initial configuration may fail with blackholed nodes" , blackholedNodes , empty ());
679+ runFor (defaultMillis (DISCOVERY_FIND_PEERS_INTERVAL_SETTING ) * 2 , "discovery prior to setting initial configuration" );
680+ final ClusterNode bootstrapNode = getAnyNode ();
681+ bootstrapNode .applyInitialConfiguration ();
682+ } else {
683+ logger .info ("setting initial configuration not required" );
684+ }
685+
609686 runFor (stabilisationDurationMillis , "stabilising" );
610687 fixLag ();
611688 assertUniqueLeaderAndExpectedModes ();
@@ -706,7 +783,7 @@ private void assertUniqueLeaderAndExpectedModes() {
706783
707784 ClusterNode getAnyLeader () {
708785 List <ClusterNode > allLeaders = clusterNodes .stream ().filter (ClusterNode ::isLeader ).collect (Collectors .toList ());
709- assertThat (allLeaders , not (empty ()));
786+ assertThat ("leaders" , allLeaders , not (empty ()));
710787 return randomFrom (allLeaders );
711788 }
712789
@@ -759,8 +836,8 @@ class ClusterNode extends AbstractComponent {
759836 super (Settings .builder ().put (NODE_NAME_SETTING .getKey (), nodeIdFromIndex (nodeIndex )).build ());
760837 this .nodeIndex = nodeIndex ;
761838 localNode = createDiscoveryNode ();
762- persistedState = new InMemoryPersistedState (1L ,
763- clusterState (1L , 1L , localNode , initialConfiguration , initialConfiguration , 0L ));
839+ persistedState = new InMemoryPersistedState (0L ,
840+ clusterState (0L , 0L , localNode , VotingConfiguration . EMPTY_CONFIG , VotingConfiguration . EMPTY_CONFIG , 0L ));
764841 onNode (localNode , this ::setUp ).run ();
765842 }
766843
@@ -917,6 +994,17 @@ ClusterState getLastAppliedClusterState() {
917994 return clusterApplier .lastAppliedClusterState ;
918995 }
919996
997+ void applyInitialConfiguration () {
998+ onNode (localNode , () -> {
999+ try {
1000+ coordinator .setInitialConfiguration (initialConfiguration );
1001+ logger .info ("successfully set initial configuration to {}" , initialConfiguration );
1002+ } catch (CoordinationStateRejectedException e ) {
1003+ logger .info (new ParameterizedMessage ("failed to set initial configuration to {}" , initialConfiguration ), e );
1004+ }
1005+ }).run ();
1006+ }
1007+
9201008 private class FakeClusterApplier implements ClusterApplier {
9211009
9221010 final ClusterName clusterName ;
0 commit comments