diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 6a7c219c58a79..89edc46a4f18d 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -45,6 +45,8 @@ import org.elasticsearch.action.admin.cluster.node.tasks.list.TransportListTasksAction; import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageAction; import org.elasticsearch.action.admin.cluster.node.usage.TransportNodesUsageAction; +import org.elasticsearch.action.admin.cluster.reelect.ClusterReelectAction; +import org.elasticsearch.action.admin.cluster.reelect.TransportClusterReelectAction; import org.elasticsearch.action.admin.cluster.remote.RemoteInfoAction; import org.elasticsearch.action.admin.cluster.remote.TransportRemoteInfoAction; import org.elasticsearch.action.admin.cluster.repositories.cleanup.CleanupRepositoryAction; @@ -269,6 +271,7 @@ import org.elasticsearch.rest.action.admin.cluster.RestClusterAllocationExplainAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterGetSettingsAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterHealthAction; +import org.elasticsearch.rest.action.admin.cluster.RestClusterReelectAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterRerouteAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterSearchShardsAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterStateAction; @@ -500,6 +503,7 @@ public void reg actions.register(ClusterHealthAction.INSTANCE, TransportClusterHealthAction.class); actions.register(ClusterUpdateSettingsAction.INSTANCE, TransportClusterUpdateSettingsAction.class); actions.register(ClusterRerouteAction.INSTANCE, TransportClusterRerouteAction.class); + actions.register(ClusterReelectAction.INSTANCE, TransportClusterReelectAction.class); actions.register(ClusterSearchShardsAction.INSTANCE, TransportClusterSearchShardsAction.class); actions.register(PendingClusterTasksAction.INSTANCE, TransportPendingClusterTasksAction.class); actions.register(PutRepositoryAction.INSTANCE, TransportPutRepositoryAction.class); @@ -650,6 +654,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestClusterHealthAction()); registerHandler.accept(new RestClusterUpdateSettingsAction()); registerHandler.accept(new RestClusterGetSettingsAction(settings, clusterSettings, settingsFilter)); + registerHandler.accept(new RestClusterReelectAction()); registerHandler.accept(new RestClusterRerouteAction(settingsFilter)); registerHandler.accept(new RestClusterSearchShardsAction()); registerHandler.accept(new RestPendingClusterTasksAction()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectAction.java new file mode 100644 index 0000000000000..692b5bebe0b1d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectAction.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.reelect; + +import org.elasticsearch.action.ActionType; + +public class ClusterReelectAction extends ActionType { + + public static final ClusterReelectAction INSTANCE = new ClusterReelectAction(); + public static final String NAME = "cluster:admin/reelect"; + + private ClusterReelectAction() { + super(NAME, ClusterReelectResponse::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequest.java new file mode 100644 index 0000000000000..f9f2524c7a381 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequest.java @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.reelect; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +public class ClusterReelectRequest extends AcknowledgedRequest { + + public ClusterReelectRequest(StreamInput in) throws IOException { + super(in); + } + + public ClusterReelectRequest() { } + + @Override + public ActionRequestValidationException validate() { return null; } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequestBuilder.java new file mode 100644 index 0000000000000..4f7f3b02f0db2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectRequestBuilder.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.reelect; + +import org.elasticsearch.action.support.master.AcknowledgedRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +public class ClusterReelectRequestBuilder extends AcknowledgedRequestBuilder { + + public ClusterReelectRequestBuilder(ElasticsearchClient client, ClusterReelectAction action) { + super(client, action, new ClusterReelectRequest()); + } + +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectResponse.java new file mode 100644 index 0000000000000..cc8c62a4d3e8d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/ClusterReelectResponse.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.reelect; + +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Response returned after a cluster reelect request + */ +public class ClusterReelectResponse extends AcknowledgedResponse implements ToXContentObject { + + private String message; + + ClusterReelectResponse(StreamInput in) throws IOException { + super(in); + message = in.readOptionalString(); + } + + ClusterReelectResponse(boolean acknowledged) { + super(acknowledged); + } + + ClusterReelectResponse(boolean acknowledged, String message) { + super(acknowledged); + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(message); + } + + @Override + protected void addCustomFields(XContentBuilder builder, Params params) throws IOException { + if (message != null && !message.isEmpty()) { + builder.field("message", message); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/TransportClusterReelectAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/TransportClusterReelectAction.java new file mode 100644 index 0000000000000..8b5000a4251dc --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/reelect/TransportClusterReelectAction.java @@ -0,0 +1,166 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.reelect; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.master.TransportMasterNodeAction; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.coordination.Coordinator; +import org.elasticsearch.cluster.coordination.PreVoteRequest; +import org.elasticsearch.cluster.coordination.PreVoteResponse; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.discovery.Discovery; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportService; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.elasticsearch.cluster.coordination.PreVoteCollector.REQUEST_PRE_VOTE_ACTION_NAME; + +public class TransportClusterReelectAction extends TransportMasterNodeAction { + + private final Logger logger = LogManager.getLogger(getClass()); + private final Discovery discovery; + + @Inject + public TransportClusterReelectAction(TransportService transportService, ClusterService clusterService, + ThreadPool threadPool, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, + Discovery discovery) throws UnsupportedOperationException { + super(ClusterReelectAction.NAME, transportService, clusterService, threadPool, actionFilters, + ClusterReelectRequest::new, indexNameExpressionResolver); + + this.discovery = discovery; + } + + @Override + protected String executor() { + // we go async right away + return ThreadPool.Names.SAME; + } + + @Override + protected ClusterReelectResponse read(StreamInput in) throws IOException { + return new ClusterReelectResponse(in); + } + + @Override + protected ClusterBlockException checkBlock(ClusterReelectRequest request, ClusterState state) { + return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); + } + + @Override + protected void masterOperation(Task task, final ClusterReelectRequest request, final ClusterState state, + final ActionListener listener) { + assert discovery instanceof Coordinator; + final Coordinator coordinator = ((Coordinator) discovery); + final String[] preferredMasters = coordinator.getPreferredMasters(); + final ClusterState lastAcceptedState = coordinator.getLastAcceptedState(); + if (String.join(",", preferredMasters).equals("")) { + // preferred master has not been set, reelect randomly + final List masterCandidates = coordinator.getMasterCandidates(lastAcceptedState, + Arrays.asList(lastAcceptedState.getNodes().getMasterNodes().values().toArray(DiscoveryNode.class))); + coordinator.atomicAbdicateTo(masterCandidates.get(new Random(Randomness.get().nextLong()).nextInt(masterCandidates.size()))); + listener.onResponse(new ClusterReelectResponse(true)); + return; + } + + ArrayList preferredNodes = new ArrayList<>(); + for (String preferredMaster : preferredMasters) { + if (preferredMaster.equals(lastAcceptedState.nodes().getMasterNode().getName())) { + listener.onResponse(new ClusterReelectResponse(true, "preferred master node has already been elected")); + return; + } + + Iterator iterator = lastAcceptedState.getNodes().getMasterNodes().valuesIt(); + while (iterator.hasNext()) { + DiscoveryNode node = iterator.next(); + if (node.getName().equals(preferredMaster) && Coordinator.nodeMayWinElection(lastAcceptedState, node)) { + preferredNodes.add(node); + } + } + } + + if (preferredNodes.size() == 0) { + listener.onResponse(new ClusterReelectResponse(false, "none of preferred master node can be elected")); + return; + } + + // check prefer master's cluster state + AtomicInteger lagPreferredNodesCount = new AtomicInteger(0); + for (DiscoveryNode node : preferredNodes) { + // leader send preVote request to preferred master nodes and compare last accepted state version to avoid useless waiting + PreVoteRequest preVoteRequest = new PreVoteRequest(transportService.getLocalNode(), lastAcceptedState.term()); + transportService.sendRequest(node, REQUEST_PRE_VOTE_ACTION_NAME, preVoteRequest, + new TransportResponseHandler() { + @Override + public PreVoteResponse read(StreamInput in) throws IOException { + return new PreVoteResponse(in); + } + + @Override + public void handleResponse(PreVoteResponse response) { + if (response.getLastAcceptedVersion() < lastAcceptedState.version()) { + logger.info("ignoring {} from {} as cluster version is older than leader", response, node); + if (lagPreferredNodesCount.incrementAndGet() == preferredNodes.size()) { + listener.onResponse(new ClusterReelectResponse(false, + "none of prefer master's state is catching up with leader")); + } + return; + } + + logger.info("the preferred master {} has the newest state", node); + coordinator.atomicAbdicateTo(node); + listener.onResponse(new ClusterReelectResponse(true)); + } + + @Override + public void handleException(TransportException exp) { + listener.onResponse(new ClusterReelectResponse(false, "check cluster state failed: " + exp)); + } + + @Override + public String executor() { + return ThreadPool.Names.GENERIC; + } + }); + } + } + +} diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/Coordinator.java b/server/src/main/java/org/elasticsearch/cluster/coordination/Coordinator.java index 55aa7012ffe18..699bc0dca465b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/Coordinator.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/Coordinator.java @@ -79,6 +79,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Random; @@ -109,6 +110,12 @@ public class Coordinator extends AbstractLifecycleComponent implements Discovery Setting.timeSetting("cluster.publish.timeout", TimeValue.timeValueMillis(30000), TimeValue.timeValueMillis(1), Setting.Property.NodeScope); + public static final Setting PREFERRED_MASTER_NAME_SETTING = + Setting.simpleString("preferred_master_name", new Setting.Property[]{Setting.Property.Dynamic, Setting.Property.NodeScope}); + + private static volatile String[] preferredMasters; + private boolean hasSkippedOneRoundElection; + private final Settings settings; private final boolean singleNodeDiscovery; private final ElectionStrategy electionStrategy; @@ -201,6 +208,19 @@ public Coordinator(String nodeName, Settings settings, ClusterSettings clusterSe this.clusterFormationFailureHelper = new ClusterFormationFailureHelper(settings, this::getClusterFormationState, transportService.getThreadPool(), joinHelper::logLastFailedJoinAttempt); this.nodeHealthService = nodeHealthService; + + this.hasSkippedOneRoundElection = false; + + preferredMasters = PREFERRED_MASTER_NAME_SETTING.get(settings).split(","); + clusterSettings.addSettingsUpdateConsumer(PREFERRED_MASTER_NAME_SETTING, + value -> { + if (value == null) { + setPreferredMasters(null); + return; + } + String[] masters = value.split(","); + setPreferredMasters(masters); + }); } private ClusterFormationState getClusterFormationState() { @@ -386,6 +406,39 @@ private void startElection() { return; } + // If a preferred master node participates in the election, + // other non-preferred master nodes need to skip 1 round of election. + if (preferredMasters != null) { + boolean preferredMasterInvolved = false; + boolean preferLocalNode = false; + for (String preferredMaster : preferredMasters) { + Iterator iterator = getLastAcceptedState().getNodes().getMasterNodes().valuesIt(); + while (iterator.hasNext()) { + DiscoveryNode node = iterator.next(); + if (node.getName().equals(preferredMaster) && nodeMayWinElection(getLastAcceptedState(), node)) { + preferredMasterInvolved = true; + } + } + + if (getLocalNode().getName().equals(preferredMaster)) { + preferLocalNode = true; + } + } + + if (preferredMasterInvolved) { + if (preferLocalNode) { + logger.info("start normal election as local node is preferred master"); + } else { + if (!hasSkippedOneRoundElection) { + logger.info("skip current election as local node is not preferred master"); + hasSkippedOneRoundElection = true; + return; + } + logger.info("start normal election as local node has skipped one round of election"); + } + } + } + final StartJoinRequest startJoinRequest = new StartJoinRequest(getLocalNode(), Math.max(getCurrentTerm(), maxTermSeen) + 1); logger.debug("starting election with {}", startJoinRequest); @@ -394,6 +447,14 @@ private void startElection() { } } + public void atomicAbdicateTo(DiscoveryNode newMaster) { + synchronized (mutex) { + if (mode == Mode.LEADER) { + abdicateTo(newMaster); + } + } + } + private void abdicateTo(DiscoveryNode newMaster) { assert Thread.holdsLock(mutex); assert mode == Mode.LEADER : "expected to be leader on abdication but was " + mode; @@ -413,7 +474,7 @@ private static boolean localNodeMayWinElection(ClusterState lastAcceptedState) { return nodeMayWinElection(lastAcceptedState, localNode); } - private static boolean nodeMayWinElection(ClusterState lastAcceptedState, DiscoveryNode node) { + public static boolean nodeMayWinElection(ClusterState lastAcceptedState, DiscoveryNode node) { final String nodeId = node.getId(); return lastAcceptedState.getLastCommittedConfiguration().getNodeIds().contains(nodeId) || lastAcceptedState.getLastAcceptedConfiguration().getNodeIds().contains(nodeId) @@ -559,6 +620,8 @@ void becomeLeader(String method) { method, getCurrentTerm(), mode, lastKnownLeader); mode = Mode.LEADER; + hasSkippedOneRoundElection = false; + joinAccumulator.close(mode); joinAccumulator = joinHelper.new LeaderJoinAccumulator(); @@ -589,6 +652,8 @@ void becomeFollower(String method, DiscoveryNode leaderNode) { if (mode != Mode.FOLLOWER) { mode = Mode.FOLLOWER; + hasSkippedOneRoundElection = false; + joinAccumulator.close(mode); joinAccumulator = new JoinHelper.FollowerJoinAccumulator(); leaderChecker.setCurrentNodes(DiscoveryNodes.EMPTY_NODES); @@ -1387,22 +1452,7 @@ public void onSuccess(String source) { boolean attemptReconfiguration = true; final ClusterState state = getLastAcceptedState(); // committed state if (localNodeMayWinElection(state) == false) { - final List masterCandidates = completedNodes().stream() - .filter(DiscoveryNode::isMasterNode) - .filter(node -> nodeMayWinElection(state, node)) - .filter(node -> { - // check if master candidate would be able to get an election quorum if we were to - // abdicate to it. Assume that every node that completed the publication can provide - // a vote in that next election and has the latest state. - final long futureElectionTerm = state.term() + 1; - final VoteCollection futureVoteCollection = new VoteCollection(); - completedNodes().forEach(completedNode -> futureVoteCollection.addJoinVote( - new Join(completedNode, node, futureElectionTerm, state.term(), state.version()))); - return electionStrategy.isElectionQuorum(node, futureElectionTerm, - state.term(), state.version(), state.getLastCommittedConfiguration(), - state.getLastAcceptedConfiguration(), futureVoteCollection); - }) - .collect(Collectors.toList()); + final List masterCandidates = getMasterCandidates(state, completedNodes()); if (masterCandidates.isEmpty() == false) { abdicateTo(masterCandidates.get(random.nextInt(masterCandidates.size()))); attemptReconfiguration = false; @@ -1500,4 +1550,29 @@ protected void sendApplyCommit(DiscoveryNode destination, ApplyCommitRequest app publicationContext.sendApplyCommit(destination, applyCommit, wrapWithMutex(responseActionListener)); } } + + public List getMasterCandidates(ClusterState state, List completedNodes) { + final List masterCandidates = completedNodes.stream() + .filter(DiscoveryNode::isMasterNode) + .filter(node -> nodeMayWinElection(state, node)) + .filter(node -> { + // check if master candidate would be able to get an election quorum if we were to + // abdicate to it. Assume that every node that completed the publication can provide + // a vote in that next election and has the latest state. + final long futureElectionTerm = state.term() + 1; + final VoteCollection futureVoteCollection = new VoteCollection(); + completedNodes.forEach(completedNode -> futureVoteCollection.addJoinVote( + new Join(completedNode, node, futureElectionTerm, state.term(), state.version()))); + return electionStrategy.isElectionQuorum(node, futureElectionTerm, + state.term(), state.version(), state.getLastCommittedConfiguration(), + state.getLastAcceptedConfiguration(), futureVoteCollection); + }) + .collect(Collectors.toList()); + + return masterCandidates; + } + + public void setPreferredMasters(String[] preferredMasterNodes) { preferredMasters = preferredMasterNodes; } + + public String[] getPreferredMasters() { return preferredMasters; } } diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 1f816aa52e298..aa12e430a1533 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -469,6 +469,7 @@ public void apply(Settings value, Settings current, Settings previous) { ElectionSchedulerFactory.ELECTION_DURATION_SETTING, Coordinator.PUBLISH_TIMEOUT_SETTING, Coordinator.PUBLISH_INFO_TIMEOUT_SETTING, + Coordinator.PREFERRED_MASTER_NAME_SETTING, JoinHelper.JOIN_TIMEOUT_SETTING, FollowersChecker.FOLLOWER_CHECK_TIMEOUT_SETTING, FollowersChecker.FOLLOWER_CHECK_INTERVAL_SETTING, diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterReelectAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterReelectAction.java new file mode 100644 index 0000000000000..40ac90af618e4 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterReelectAction.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.rest.action.admin.cluster; + +import org.elasticsearch.action.admin.cluster.reelect.ClusterReelectAction; +import org.elasticsearch.action.admin.cluster.reelect.ClusterReelectRequest; +import org.elasticsearch.action.admin.cluster.reelect.ClusterReelectResponse; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + + +public class RestClusterReelectAction extends BaseRestHandler { + + @Override + public List routes() { return List.of(new Route(POST, "/_cluster/reelect")); } + + @Override + public String getName() { return "cluster_reelect_action"; } + + @Override + public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { + final ClusterReelectRequest clusterReelectRequest = new ClusterReelectRequest(); + clusterReelectRequest.timeout(request.paramAsTime("timeout", clusterReelectRequest.timeout())); + clusterReelectRequest.masterNodeTimeout(request.paramAsTime("master_timeout", clusterReelectRequest.masterNodeTimeout())); + return channel -> client.execute(ClusterReelectAction.INSTANCE, clusterReelectRequest, + new RestToXContentListener(channel)); + } +}