Skip to content

Commit cb55f63

Browse files
committed
Allow CCR on nodes with legacy roles only (#60093)
CCR will stop functioning if the master node is on 7.8, but data nodes are before that version because the master node considers that all data nodes do not have the remote cluster client role. This commit allows CCR work on data nodes with legacy roles only. Relates #54146 Relates #59375
1 parent 1479a9e commit cb55f63

File tree

5 files changed

+82
-28
lines changed

5 files changed

+82
-28
lines changed

server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodeRole.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.cluster.node;
2121

22+
import org.elasticsearch.Version;
2223
import org.elasticsearch.common.settings.Setting;
2324
import org.elasticsearch.common.settings.Setting.Property;
2425
import org.elasticsearch.common.settings.Settings;
@@ -172,6 +173,12 @@ public Setting<Boolean> legacySetting() {
172173
public static SortedSet<DiscoveryNodeRole> BUILT_IN_ROLES = Collections.unmodifiableSortedSet(
173174
new TreeSet<>(Arrays.asList(DATA_ROLE, INGEST_ROLE, MASTER_ROLE, REMOTE_CLUSTER_CLIENT_ROLE)));
174175

176+
/**
177+
* The version that {@link #REMOTE_CLUSTER_CLIENT_ROLE} is introduced. Nodes before this version do not have that role even
178+
* they can connect to remote clusters.
179+
*/
180+
public static final Version REMOTE_CLUSTER_CLIENT_ROLE_VERSION = Version.V_7_8_0;
181+
175182
static SortedSet<DiscoveryNodeRole> LEGACY_ROLES =
176183
Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(DATA_ROLE, INGEST_ROLE, MASTER_ROLE)));
177184

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.cluster.metadata.IndexMetadata;
3333
import org.elasticsearch.cluster.metadata.MappingMetadata;
3434
import org.elasticsearch.cluster.node.DiscoveryNode;
35+
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
3536
import org.elasticsearch.cluster.routing.IndexRoutingTable;
3637
import org.elasticsearch.cluster.routing.ShardRouting;
3738
import org.elasticsearch.cluster.service.ClusterService;
@@ -124,14 +125,18 @@ public void validate(ShardFollowTask params, ClusterState clusterState) {
124125

125126
@Override
126127
public Assignment getAssignment(final ShardFollowTask params, final ClusterState clusterState) {
127-
final DiscoveryNode node = selectLeastLoadedNode(
128-
clusterState,
128+
DiscoveryNode selectedNode = selectLeastLoadedNode(clusterState,
129129
((Predicate<DiscoveryNode>) DiscoveryNode::isDataNode).and(DiscoveryNode::isRemoteClusterClient)
130130
);
131-
if (node == null) {
131+
if (selectedNode == null) {
132+
// best effort as nodes before 7.8 might not be able to connect to remote clusters
133+
selectedNode = selectLeastLoadedNode(clusterState,
134+
node -> node.isDataNode() && node.getVersion().before(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE_VERSION));
135+
}
136+
if (selectedNode == null) {
132137
return NO_ASSIGNMENT;
133138
} else {
134-
return new Assignment(node.getId(), "node is the least loaded data node and remote cluster client");
139+
return new Assignment(selectedNode.getId(), "node is the least loaded data node and remote cluster client");
135140
}
136141
}
137142

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/allocation/CcrPrimaryFollowerAllocationDecider.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,14 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, Routing
5757
return allocation.decision(Decision.YES, NAME,
5858
"shard is a primary follower but was bootstrapped already; hence is not under the purview of this decider");
5959
}
60-
if (node.node().isRemoteClusterClient() == false) {
61-
return allocation.decision(Decision.NO, NAME, "shard is a primary follower and being bootstrapped, but node does not have the "
62-
+ DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE.roleName() + " role");
60+
if (node.node().isRemoteClusterClient()) {
61+
return allocation.decision(Decision.YES, NAME,
62+
"shard is a primary follower and node has the " + DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE.roleName() + " role");
63+
}
64+
if (node.node().getVersion().before(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE_VERSION)) {
65+
return allocation.decision(Decision.YES, NAME, "shard is a primary follower and node has only the legacy roles");
6366
}
64-
return allocation.decision(Decision.YES, NAME,
65-
"shard is a primary follower and node has the " + DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE.roleName() + " role");
67+
return allocation.decision(Decision.NO, NAME, "shard is a primary follower and being bootstrapped, but node does not have the "
68+
+ DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE.roleName() + " role");
6669
}
6770
}

x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutorAssignmentTests.java

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import org.elasticsearch.common.settings.ClusterSettings;
1919
import org.elasticsearch.common.settings.Settings;
2020
import org.elasticsearch.common.settings.SettingsModule;
21+
import org.elasticsearch.common.util.set.Sets;
2122
import org.elasticsearch.persistent.PersistentTasksCustomMetadata.Assignment;
2223
import org.elasticsearch.test.ESTestCase;
24+
import org.elasticsearch.test.VersionUtils;
2325
import org.elasticsearch.threadpool.ThreadPool;
2426
import org.elasticsearch.xpack.ccr.CcrSettings;
2527

@@ -38,9 +40,13 @@ public class ShardFollowTasksExecutorAssignmentTests extends ESTestCase {
3840

3941
public void testAssignmentToNodeWithDataAndRemoteClusterClientRoles() {
4042
runAssignmentTest(
41-
new HashSet<>(Arrays.asList(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE)),
42-
randomIntBetween(0, 8),
43-
() -> new HashSet<>(randomSubsetOf(new HashSet<>(Arrays.asList(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.INGEST_ROLE)))),
43+
newNode(
44+
Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE),
45+
VersionUtils.randomVersion(random())),
46+
newNodes(
47+
between(0, 8),
48+
() -> Sets.newHashSet(randomSubsetOf(Arrays.asList(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.INGEST_ROLE))),
49+
Version.CURRENT),
4450
(theSpecial, assignment) -> {
4551
assertTrue(assignment.isAssigned());
4652
assertThat(assignment.getExecutorNode(), equalTo(theSpecial.getId()));
@@ -56,11 +62,26 @@ public void testRemoteClusterClientRoleWithoutDataRole() {
5662
runNoAssignmentTest(Collections.singleton(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE));
5763
}
5864

65+
public void testNodeWithLegacyRolesOnly() {
66+
final Version oldVersion = VersionUtils.randomVersionBetween(random(),
67+
Version.V_6_0_0, VersionUtils.getPreviousVersion(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE_VERSION));
68+
runAssignmentTest(
69+
newNode(Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE), oldVersion),
70+
newNodes(
71+
between(0, 8),
72+
() -> Sets.newHashSet(randomSubsetOf(Arrays.asList(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.INGEST_ROLE))),
73+
Version.CURRENT),
74+
(theSpecial, assignment) -> {
75+
assertTrue(assignment.isAssigned());
76+
assertThat(assignment.getExecutorNode(), equalTo(theSpecial.getId()));
77+
}
78+
);
79+
}
80+
5981
private void runNoAssignmentTest(final Set<DiscoveryNodeRole> roles) {
6082
runAssignmentTest(
61-
roles,
62-
0,
63-
Collections::emptySet,
83+
newNode(roles, Version.CURRENT),
84+
Collections.emptySet(),
6485
(theSpecial, assignment) -> {
6586
assertFalse(assignment.isAssigned());
6687
assertThat(assignment.getExplanation(), equalTo("no nodes found with data and remote cluster client roles"));
@@ -69,9 +90,8 @@ private void runNoAssignmentTest(final Set<DiscoveryNodeRole> roles) {
6990
}
7091

7192
private void runAssignmentTest(
72-
final Set<DiscoveryNodeRole> theSpecialRoles,
73-
final int numberOfOtherNodes,
74-
final Supplier<Set<DiscoveryNodeRole>> otherNodesRolesSupplier,
93+
final DiscoveryNode targetNode,
94+
final Set<DiscoveryNode> otherNodes,
7595
final BiConsumer<DiscoveryNode, Assignment> consumer
7696
) {
7797
final ClusterService clusterService = mock(ClusterService.class);
@@ -82,25 +102,30 @@ private void runAssignmentTest(
82102
final ShardFollowTasksExecutor executor =
83103
new ShardFollowTasksExecutor(mock(Client.class), mock(ThreadPool.class), clusterService, settingsModule);
84104
final ClusterState.Builder clusterStateBuilder = ClusterState.builder(new ClusterName("test"));
85-
final DiscoveryNodes.Builder nodesBuilder = DiscoveryNodes.builder();
86-
final DiscoveryNode theSpecial = newNode(theSpecialRoles);
87-
nodesBuilder.add(theSpecial);
88-
for (int i = 0; i < numberOfOtherNodes; i++) {
89-
nodesBuilder.add(newNode(otherNodesRolesSupplier.get()));
105+
final DiscoveryNodes.Builder nodesBuilder = DiscoveryNodes.builder().add(targetNode);
106+
for (DiscoveryNode node : otherNodes) {
107+
nodesBuilder.add(node);
90108
}
91109
clusterStateBuilder.nodes(nodesBuilder);
92110
final Assignment assignment = executor.getAssignment(mock(ShardFollowTask.class), clusterStateBuilder.build());
93-
consumer.accept(theSpecial, assignment);
111+
consumer.accept(targetNode, assignment);
94112
}
95113

96-
private static DiscoveryNode newNode(final Set<DiscoveryNodeRole> roles) {
114+
private static DiscoveryNode newNode(final Set<DiscoveryNodeRole> roles, final Version version) {
97115
return new DiscoveryNode(
98116
"node_" + UUIDs.randomBase64UUID(random()),
99117
buildNewFakeTransportAddress(),
100118
Collections.emptyMap(),
101119
roles,
102-
Version.CURRENT
120+
version
103121
);
104122
}
105123

124+
private static Set<DiscoveryNode> newNodes(int numberOfNodes, Supplier<Set<DiscoveryNodeRole>> rolesSupplier, Version version) {
125+
Set<DiscoveryNode> nodes = new HashSet<>();
126+
for (int i = 0; i < numberOfNodes; i++) {
127+
nodes.add(newNode(rolesSupplier.get(), version));
128+
}
129+
return nodes;
130+
}
106131
}

x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/allocation/CcrPrimaryFollowerAllocationDeciderTests.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.elasticsearch.repositories.IndexId;
5353
import org.elasticsearch.snapshots.Snapshot;
5454
import org.elasticsearch.snapshots.SnapshotId;
55+
import org.elasticsearch.test.VersionUtils;
5556
import org.elasticsearch.xpack.ccr.CcrSettings;
5657

5758
import java.util.ArrayList;
@@ -60,6 +61,7 @@
6061
import java.util.List;
6162
import java.util.Set;
6263

64+
import static java.util.Collections.emptyMap;
6365
import static org.elasticsearch.cluster.routing.ShardRoutingState.UNASSIGNED;
6466
import static org.hamcrest.Matchers.equalTo;
6567

@@ -151,9 +153,10 @@ public void testBootstrappingFollowerIndex() {
151153
IndexMetadata.Builder indexMetadata = IndexMetadata.builder(index)
152154
.settings(settings(Version.CURRENT).put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true))
153155
.numberOfShards(1).numberOfReplicas(1);
154-
DiscoveryNode dataOnlyNode = newNode("d1", Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE));
155-
DiscoveryNode dataAndRemoteNode = newNode("dr1",
156+
final DiscoveryNode dataOnlyNode = newNode("data_role_only", Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE));
157+
final DiscoveryNode dataAndRemoteNode = newNode("data_and_remote_cluster_client_role",
156158
Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE, DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE));
159+
final DiscoveryNode nodeWithLegacyRolesOnly = newNodeWithLegacyRoles("legacy_roles_only");
157160
DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().add(dataOnlyNode).add(dataAndRemoteNode).build();
158161
Metadata metadata = Metadata.builder().put(indexMetadata).build();
159162
RoutingTable.Builder routingTable = RoutingTable.builder()
@@ -171,6 +174,11 @@ public void testBootstrappingFollowerIndex() {
171174
Decision yesDecision = executeAllocation(clusterState, shardRouting.primaryShard(), dataAndRemoteNode);
172175
assertThat(yesDecision.type(), equalTo(Decision.Type.YES));
173176
assertThat(yesDecision.getExplanation(), equalTo("shard is a primary follower and node has the remote_cluster_client role"));
177+
178+
yesDecision = executeAllocation(clusterState, shardRouting.primaryShard(), nodeWithLegacyRolesOnly);
179+
assertThat(yesDecision.type(), equalTo(Decision.Type.YES));
180+
assertThat(yesDecision.getExplanation(), equalTo("shard is a primary follower and node has only the legacy roles"));
181+
174182
for (ShardRouting replica : shardRouting.replicaShards()) {
175183
assertThat(replica.state(), equalTo(UNASSIGNED));
176184
yesDecision = executeAllocation(clusterState, replica, randomFrom(dataOnlyNode, dataAndRemoteNode));
@@ -181,6 +189,12 @@ public void testBootstrappingFollowerIndex() {
181189
}
182190
}
183191

192+
static DiscoveryNode newNodeWithLegacyRoles(String id) {
193+
final Version version = VersionUtils.randomVersionBetween(random(),
194+
Version.V_6_0_0, VersionUtils.getPreviousVersion(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE_VERSION));
195+
return new DiscoveryNode(id, buildNewFakeTransportAddress(), emptyMap(), Sets.newHashSet(DiscoveryNodeRole.DATA_ROLE), version);
196+
}
197+
184198
static Decision executeAllocation(ClusterState clusterState, ShardRouting shardRouting, DiscoveryNode node) {
185199
final AllocationDecider decider = new CcrPrimaryFollowerAllocationDecider();
186200
final RoutingAllocation routingAllocation = new RoutingAllocation(new AllocationDeciders(Collections.singletonList(decider)),

0 commit comments

Comments
 (0)