Skip to content

Commit a372b54

Browse files
authored
Enforce external id uniqueness during DesiredNode construction (#84227)
This commit introduces some small refactorings to improve the desired nodes codebase. - DesiredNode must contain a valid external id, otherwise it cannot be built. - DesiredNodes now stores desired nodes as a map that uses desired nodes external id as the key. This fixes a small bug around idempotent updates, as before we were using a list and comparing the desired nodes using that list.
1 parent 9af6856 commit a372b54

File tree

13 files changed

+379
-151
lines changed

13 files changed

+379
-151
lines changed

docs/changelog/84227.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 84227
2+
summary: Enforce external id uniqueness during `DesiredNode` construction
3+
area: Distributed
4+
type: bug
5+
issues: []

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ teardown:
99
_internal.delete_desired_nodes: {}
1010
---
1111
"Test update desired nodes":
12+
- skip:
13+
reason: "contains is a newly added assertion"
14+
features: contains
1215
- do:
1316
cluster.state: {}
1417

@@ -49,15 +52,17 @@ teardown:
4952

5053
- do:
5154
_internal.get_desired_nodes: {}
52-
- match:
53-
$body:
54-
history_id: "test"
55-
version: 2
56-
nodes:
57-
- { settings: { node: { name: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
58-
- { settings: { node: { name: "instance-000188" } }, processors: 16, memory: "128gb", storage: "1tb", node_version: $es_version }
55+
56+
- match: { history_id: "test" }
57+
- match: { version: 2 }
58+
- length: { nodes: 2 }
59+
- contains: { nodes: { settings: { node: { name: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
60+
- contains: { nodes: { settings: { node: { name: "instance-000188" } }, processors: 16, memory: "128gb", storage: "1tb", node_version: $es_version } }
5961
---
6062
"Test update move to a new history id":
63+
- skip:
64+
reason: "contains is a newly added assertion"
65+
features: contains
6166
- do:
6267
cluster.state: {}
6368

@@ -97,13 +102,11 @@ teardown:
97102

98103
- do:
99104
_internal.get_desired_nodes: {}
100-
- match:
101-
$body:
102-
history_id: "new_history"
103-
version: 1
104-
nodes:
105-
- { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
106-
- { settings: { node: { external_id: "instance-000188" } }, processors: 16, memory: "128gb", storage: "1tb", node_version: $es_version }
105+
- match: { history_id: "new_history" }
106+
- match: { version: 1 }
107+
- length: { nodes: 2 }
108+
- contains: { nodes: { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
109+
- contains: { nodes: { settings: { node: { external_id: "instance-000188" } }, processors: 16, memory: "128gb", storage: "1tb", node_version: $es_version } }
107110
---
108111
"Test delete desired nodes":
109112
- do:
@@ -142,6 +145,9 @@ teardown:
142145
- match: { status: 404 }
143146
---
144147
"Test update desired nodes is idempotent":
148+
- skip:
149+
reason: "contains is a newly added assertion"
150+
features: contains
145151
- do:
146152
cluster.state: {}
147153

@@ -158,16 +164,51 @@ teardown:
158164
body:
159165
nodes:
160166
- { settings: { "node.external_id": "instance-000187" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
167+
- { settings: { "node.external_id": "instance-000188" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
161168
- match: { replaced_existing_history_id: false }
162169

163170
- do:
164171
_internal.get_desired_nodes: {}
165-
- match:
166-
$body:
172+
173+
- match: { history_id: "test" }
174+
- match: { version: 1 }
175+
- length: { nodes: 2 }
176+
- contains: { nodes: { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
177+
- contains: { nodes: { settings: { node: { external_id: "instance-000188" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
178+
179+
- do:
180+
_internal.update_desired_nodes:
167181
history_id: "test"
168182
version: 1
169-
nodes:
170-
- { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
183+
body:
184+
nodes:
185+
- { settings: { "node.external_id": "instance-000187" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
186+
- { settings: { "node.external_id": "instance-000188" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
187+
188+
- match: { replaced_existing_history_id: false }
189+
190+
- do:
191+
_internal.get_desired_nodes: {}
192+
193+
- match: { history_id: "test" }
194+
- match: { version: 1 }
195+
- length: { nodes: 2 }
196+
- contains: { nodes: { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
197+
- contains: { nodes: { settings: { node: { external_id: "instance-000188" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
198+
---
199+
"Test update desired nodes is idempotent with different order":
200+
- skip:
201+
version: " - 8.2.99"
202+
features: contains
203+
reason: "Bug fixed in 8.3.0 and uses contains feature"
204+
- do:
205+
cluster.state: {}
206+
207+
- set: { master_node: master }
208+
209+
- do:
210+
nodes.info: {}
211+
- set: { nodes.$master.version: es_version }
171212

172213
- do:
173214
_internal.update_desired_nodes:
@@ -176,16 +217,37 @@ teardown:
176217
body:
177218
nodes:
178219
- { settings: { "node.external_id": "instance-000187" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
220+
- { settings: { "node.external_id": "instance-000188" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
179221
- match: { replaced_existing_history_id: false }
180222

181223
- do:
182224
_internal.get_desired_nodes: {}
183-
- match:
184-
$body:
225+
226+
- match: { history_id: "test" }
227+
- match: { version: 1 }
228+
- length: { nodes: 2 }
229+
- contains: { nodes: { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
230+
- contains: { nodes: { settings: { node: { external_id: "instance-000188" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
231+
232+
- do:
233+
_internal.update_desired_nodes:
185234
history_id: "test"
186235
version: 1
187-
nodes:
188-
- { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
236+
body:
237+
nodes:
238+
- { settings: { "node.external_id": "instance-000188" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
239+
- { settings: { "node.external_id": "instance-000187" }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
240+
241+
- match: { replaced_existing_history_id: false }
242+
243+
- do:
244+
_internal.get_desired_nodes: {}
245+
246+
- match: { history_id: "test" }
247+
- match: { version: 1 }
248+
- length: { nodes: 2 }
249+
- contains: { nodes: { settings: { node: { external_id: "instance-000187" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
250+
- contains: { nodes: { settings: { node: { external_id: "instance-000188" } }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version } }
189251
---
190252
"Test going backwards within the same history is forbidden":
191253
- do:
@@ -343,6 +405,9 @@ teardown:
343405
- match: { replaced_existing_history_id: false }
344406
---
345407
"Test external_id or node.name is required":
408+
- skip:
409+
version: " - 8.2.99"
410+
reason: "Change error code in 8.3"
346411
- do:
347412
cluster.state: {}
348413

@@ -361,11 +426,13 @@ teardown:
361426
nodes:
362427
- { settings: { }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
363428
- match: { status: 400 }
364-
- match: { error.type: illegal_argument_exception }
365-
- match: { error.reason: "Nodes with ids [<missing>] in positions [0] contain invalid settings" }
366-
- match: { error.suppressed.0.reason: "[node.name] or [node.external_id] is missing or empty" }
429+
- match: { error.type: x_content_parse_exception }
430+
- match: { error.caused_by.caused_by.caused_by.reason: "[node.name] or [node.external_id] is missing or empty" }
367431
---
368432
"Test external_id must have content":
433+
- skip:
434+
version: " - 8.2.99"
435+
reason: "Change error code in 8.3"
369436
- do:
370437
cluster.state: {}
371438

@@ -384,9 +451,8 @@ teardown:
384451
nodes:
385452
- { settings: { "node.external_id": " " }, processors: 8, memory: "64gb", storage: "128gb", node_version: $es_version }
386453
- match: { status: 400 }
387-
- match: { error.type: illegal_argument_exception }
388-
- match: { error.reason: "Nodes with ids [<missing>] in positions [0] contain invalid settings" }
389-
- match: { error.suppressed.0.reason: "[node.name] or [node.external_id] is missing or empty" }
454+
- match: { error.type: x_content_parse_exception }
455+
- match: { error.caused_by.caused_by.caused_by.reason: "[node.name] or [node.external_id] is missing or empty" }
390456
---
391457
"Test duplicated external ids are not allowed":
392458
- do:

server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/desirednodes/TransportDesiredNodesActionsIT.java

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.cluster.ClusterStateTaskExecutor;
1717
import org.elasticsearch.cluster.ClusterStateUpdateTask;
1818
import org.elasticsearch.cluster.desirednodes.VersionConflictException;
19+
import org.elasticsearch.cluster.metadata.DesiredNode;
1920
import org.elasticsearch.cluster.metadata.DesiredNodes;
2021
import org.elasticsearch.cluster.metadata.DesiredNodesMetadata;
2122
import org.elasticsearch.cluster.metadata.DesiredNodesTestCase;
@@ -27,6 +28,7 @@
2728
import org.junit.After;
2829

2930
import java.util.ArrayList;
31+
import java.util.Collections;
3032
import java.util.List;
3133
import java.util.Locale;
3234
import java.util.concurrent.CountDownLatch;
@@ -37,7 +39,6 @@
3739
import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.randomDesiredNodes;
3840
import static org.elasticsearch.common.util.concurrent.EsExecutors.NODE_PROCESSORS_SETTING;
3941
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_TCP_KEEP_IDLE;
40-
import static org.elasticsearch.node.Node.NODE_EXTERNAL_ID_SETTING;
4142
import static org.elasticsearch.node.NodeRoleSettings.NODE_ROLES_SETTING;
4243
import static org.hamcrest.Matchers.containsString;
4344
import static org.hamcrest.Matchers.equalTo;
@@ -64,10 +65,16 @@ public void testUpdateDesiredNodes() {
6465

6566
public void testUpdateDesiredNodesIsIdempotent() {
6667
final DesiredNodes desiredNodes = putRandomDesiredNodes();
67-
updateDesiredNodes(desiredNodes);
68+
69+
final List<DesiredNode> desiredNodesList = new ArrayList<>(desiredNodes.nodes());
70+
if (randomBoolean()) {
71+
Collections.shuffle(desiredNodesList, random());
72+
}
73+
74+
updateDesiredNodes(new DesiredNodes(desiredNodes.historyID(), desiredNodes.version(), desiredNodesList));
6875

6976
final ClusterState state = client().admin().cluster().prepareState().get().getState();
70-
final DesiredNodesMetadata metadata = state.metadata().custom(DesiredNodesMetadata.TYPE);
77+
final DesiredNodesMetadata metadata = DesiredNodesMetadata.fromClusterState(state);
7178
assertThat(metadata, is(notNullValue()));
7279
final DesiredNodes latestDesiredNodes = metadata.getLatestDesiredNodes();
7380
assertThat(latestDesiredNodes, is(equalTo(desiredNodes)));
@@ -247,10 +254,9 @@ public void testNodeProcessorsGetValidatedWithDesiredNodeProcessors() {
247254
final DesiredNodes latestDesiredNodes = metadata.getLatestDesiredNodes();
248255
assertThat(latestDesiredNodes, is(equalTo(desiredNodes)));
249256
assertThat(latestDesiredNodes.nodes().isEmpty(), is(equalTo(false)));
250-
assertThat(
251-
latestDesiredNodes.nodes().get(0).settings().get(NODE_PROCESSORS_SETTING.getKey()),
252-
is(equalTo(Integer.toString(numProcessors)))
253-
);
257+
for (DesiredNode desiredNode : latestDesiredNodes.nodes()) {
258+
assertThat(desiredNode.settings().get(NODE_PROCESSORS_SETTING.getKey()), is(equalTo(Integer.toString(numProcessors))));
259+
}
254260
}
255261
}
256262

@@ -263,7 +269,7 @@ public void testUpdateDesiredNodesTasksAreBatchedCorrectly() throws Exception {
263269
final UpdateDesiredNodesRequest request = new UpdateDesiredNodesRequest(
264270
desiredNodes.historyID(),
265271
desiredNodes.version(),
266-
desiredNodes.nodes()
272+
List.copyOf(desiredNodes.nodes())
267273
);
268274
// Use the master client to ensure the same updates ordering as in proposedDesiredNodesList
269275
updateDesiredNodesFutures.add(internalCluster().masterClient().execute(UpdateDesiredNodesAction.INSTANCE, request));
@@ -280,7 +286,7 @@ public void testUpdateDesiredNodesTasksAreBatchedCorrectly() throws Exception {
280286
}
281287

282288
final ClusterState state = client().admin().cluster().prepareState().get().getState();
283-
final DesiredNodes latestDesiredNodes = DesiredNodesMetadata.latestFromClusterState(state);
289+
final DesiredNodes latestDesiredNodes = DesiredNodes.latestFromClusterState(state);
284290
final DesiredNodes latestProposedDesiredNodes = proposedDesiredNodes.get(proposedDesiredNodes.size() - 1);
285291
assertThat(latestDesiredNodes, equalTo(latestProposedDesiredNodes));
286292
}
@@ -308,7 +314,7 @@ public void testDeleteDesiredNodesTasksAreBatchedCorrectly() throws Exception {
308314
}
309315

310316
final ClusterState state = client().admin().cluster().prepareState().get().getState();
311-
final DesiredNodes latestDesiredNodes = DesiredNodesMetadata.latestFromClusterState(state);
317+
final DesiredNodes latestDesiredNodes = DesiredNodes.latestFromClusterState(state);
312318
assertThat(latestDesiredNodes, is(nullValue()));
313319
}
314320

@@ -328,21 +334,6 @@ public void testDeleteDesiredNodes() {
328334
expectThrows(ResourceNotFoundException.class, this::getLatestDesiredNodes);
329335
}
330336

331-
public void testEmptyExternalIDIsInvalid() {
332-
final Consumer<Settings.Builder> settingsConsumer = (settings) -> settings.put(NODE_EXTERNAL_ID_SETTING.getKey(), " ");
333-
final DesiredNodes desiredNodes = new DesiredNodes(
334-
UUIDs.randomBase64UUID(),
335-
randomIntBetween(1, 20),
336-
randomList(1, 20, () -> randomDesiredNode(Version.CURRENT, settingsConsumer))
337-
);
338-
339-
final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> updateDesiredNodes(desiredNodes));
340-
assertThat(exception.getMessage(), containsString("Nodes with ids"));
341-
assertThat(exception.getMessage(), containsString("contain invalid settings"));
342-
assertThat(exception.getSuppressed().length > 0, is(equalTo(true)));
343-
assertThat(exception.getSuppressed()[0].getMessage(), containsString("[node.external_id] is missing or empty"));
344-
}
345-
346337
private void deleteDesiredNodes() {
347338
final DeleteDesiredNodesAction.Request request = new DeleteDesiredNodesAction.Request();
348339
client().execute(DeleteDesiredNodesAction.INSTANCE, request).actionGet();
@@ -364,7 +355,7 @@ private UpdateDesiredNodesResponse updateDesiredNodes(DesiredNodes desiredNodes)
364355
final UpdateDesiredNodesRequest request = new UpdateDesiredNodesRequest(
365356
desiredNodes.historyID(),
366357
desiredNodes.version(),
367-
desiredNodes.nodes()
358+
List.copyOf(desiredNodes.nodes())
368359
);
369360
return client().execute(UpdateDesiredNodesAction.INSTANCE, request).actionGet();
370361
}

server/src/main/java/org/elasticsearch/action/admin/cluster/desirednodes/TransportGetDesiredNodesAction.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import org.elasticsearch.cluster.block.ClusterBlockException;
1717
import org.elasticsearch.cluster.block.ClusterBlockLevel;
1818
import org.elasticsearch.cluster.metadata.DesiredNodes;
19-
import org.elasticsearch.cluster.metadata.DesiredNodesMetadata;
2019
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
2120
import org.elasticsearch.cluster.service.ClusterService;
2221
import org.elasticsearch.common.inject.Inject;
@@ -55,7 +54,7 @@ protected void masterOperation(
5554
ClusterState state,
5655
ActionListener<GetDesiredNodesAction.Response> listener
5756
) throws Exception {
58-
final DesiredNodes latestDesiredNodes = DesiredNodesMetadata.latestFromClusterState(state);
57+
final DesiredNodes latestDesiredNodes = DesiredNodes.latestFromClusterState(state);
5958
if (latestDesiredNodes == null) {
6059
listener.onFailure(new ResourceNotFoundException("Desired nodes not found"));
6160
} else {

server/src/main/java/org/elasticsearch/action/admin/cluster/desirednodes/TransportUpdateDesiredNodesAction.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ protected void masterOperation(
7474
ActionListener<UpdateDesiredNodesResponse> listener
7575
) throws Exception {
7676
try {
77-
DesiredNodes proposedDesiredNodes = new DesiredNodes(request.getHistoryID(), request.getVersion(), request.getNodes());
78-
settingsValidator.validate(proposedDesiredNodes);
77+
settingsValidator.validate(request.getNodes());
7978

8079
clusterService.submitStateUpdateTask(
8180
"update-desired-nodes",
@@ -85,8 +84,8 @@ protected void masterOperation(
8584
@Override
8685
public ClusterState execute(ClusterState currentState) {
8786
final ClusterState updatedState = updateDesiredNodes(currentState, request);
88-
final DesiredNodes previousDesiredNodes = DesiredNodesMetadata.latestFromClusterState(currentState);
89-
final DesiredNodes latestDesiredNodes = DesiredNodesMetadata.latestFromClusterState(updatedState);
87+
final DesiredNodes previousDesiredNodes = DesiredNodes.latestFromClusterState(currentState);
88+
final DesiredNodes latestDesiredNodes = DesiredNodes.latestFromClusterState(updatedState);
9089
replacedExistingHistoryId = previousDesiredNodes != null
9190
&& previousDesiredNodes.hasSameHistoryId(latestDesiredNodes) == false;
9291
return updatedState;
@@ -110,9 +109,9 @@ public void clusterStateProcessed(ClusterState oldState, ClusterState newState)
110109
}
111110

112111
static ClusterState updateDesiredNodes(ClusterState currentState, UpdateDesiredNodesRequest request) {
113-
DesiredNodesMetadata desiredNodesMetadata = currentState.metadata().custom(DesiredNodesMetadata.TYPE, DesiredNodesMetadata.EMPTY);
114-
DesiredNodes latestDesiredNodes = desiredNodesMetadata.getLatestDesiredNodes();
115-
DesiredNodes proposedDesiredNodes = new DesiredNodes(request.getHistoryID(), request.getVersion(), request.getNodes());
112+
final DesiredNodesMetadata desiredNodesMetadata = DesiredNodesMetadata.fromClusterState(currentState);
113+
final DesiredNodes latestDesiredNodes = desiredNodesMetadata.getLatestDesiredNodes();
114+
final DesiredNodes proposedDesiredNodes = new DesiredNodes(request.getHistoryID(), request.getVersion(), request.getNodes());
116115

117116
if (latestDesiredNodes != null) {
118117
if (latestDesiredNodes.equals(proposedDesiredNodes)) {

0 commit comments

Comments
 (0)