Skip to content

Commit 9334f34

Browse files
authored
Update tests for ElasticsearchNodeCommand (#129240)
The PR updates `ElasticsearchNodeCommandTests` so that they test the actual behaviours after multi-project related changes: 1. Persisted cluster state is always written with multi-project enabled 2. The CLI tools are expected to run on the same node version, i.e. no BWC 3. Unknown (unregistered) customs can be read and write by the CLI tools without modification Resolves: ES-11328
1 parent c823358 commit 9334f34

File tree

2 files changed

+171
-47
lines changed

2 files changed

+171
-47
lines changed

server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,7 @@ private static BytesReference uncompress(BytesReference bytesReference) throws I
783783
private static final ToXContent.Params FORMAT_PARAMS;
784784

785785
static {
786-
Map<String, String> params = Maps.newMapWithExpectedSize(2);
786+
Map<String, String> params = Maps.newMapWithExpectedSize(4);
787787
params.put("binary", "true");
788788
params.put(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_GATEWAY);
789789
params.put(Metadata.DEDUPLICATED_MAPPINGS_PARAM, Boolean.TRUE.toString());

server/src/test/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommandTests.java

Lines changed: 170 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,33 @@
99

1010
package org.elasticsearch.cluster.coordination;
1111

12+
import org.elasticsearch.TransportVersion;
1213
import org.elasticsearch.cluster.ClusterModule;
14+
import org.elasticsearch.cluster.ClusterState;
1315
import org.elasticsearch.cluster.metadata.IndexGraveyard;
1416
import org.elasticsearch.cluster.metadata.IndexMetadata;
1517
import org.elasticsearch.cluster.metadata.Metadata;
1618
import org.elasticsearch.cluster.metadata.ProjectId;
1719
import org.elasticsearch.cluster.metadata.ProjectMetadata;
1820
import org.elasticsearch.common.UUIDs;
19-
import org.elasticsearch.common.bytes.BytesReference;
21+
import org.elasticsearch.common.settings.ClusterSettings;
22+
import org.elasticsearch.common.settings.Settings;
2023
import org.elasticsearch.core.FixForMultiProject;
24+
import org.elasticsearch.core.Tuple;
25+
import org.elasticsearch.env.Environment;
26+
import org.elasticsearch.env.TestEnvironment;
27+
import org.elasticsearch.gateway.PersistedClusterStateService;
2128
import org.elasticsearch.index.Index;
2229
import org.elasticsearch.indices.IndicesModule;
2330
import org.elasticsearch.test.ESTestCase;
31+
import org.elasticsearch.test.TestClusterCustomMetadata;
32+
import org.elasticsearch.test.TestProjectCustomMetadata;
2433
import org.elasticsearch.xcontent.NamedXContentRegistry;
25-
import org.elasticsearch.xcontent.XContentBuilder;
26-
import org.elasticsearch.xcontent.XContentParser;
27-
import org.elasticsearch.xcontent.XContentParserConfiguration;
28-
import org.elasticsearch.xcontent.json.JsonXContent;
34+
import org.elasticsearch.xcontent.ParseField;
2935

3036
import java.io.IOException;
3137
import java.nio.file.Path;
38+
import java.util.EnumSet;
3239
import java.util.List;
3340
import java.util.function.Function;
3441
import java.util.stream.Stream;
@@ -41,62 +48,104 @@
4148

4249
public class ElasticsearchNodeCommandTests extends ESTestCase {
4350

44-
public void testLoadStateWithoutMissingCustoms() throws IOException {
45-
runLoadStateTest(false, false);
46-
}
47-
4851
public void testLoadStateWithoutMissingCustomsButPreserved() throws IOException {
49-
runLoadStateTest(false, true);
52+
runLoadStateTest(false);
5053
}
5154

5255
public void testLoadStateWithMissingCustomsButPreserved() throws IOException {
53-
runLoadStateTest(true, true);
56+
runLoadStateTest(true);
5457
}
5558

56-
public void testLoadStateWithMissingCustomsAndNotPreserved() throws IOException {
57-
runLoadStateTest(true, false);
59+
@Override
60+
public Settings buildEnvSettings(Settings settings) {
61+
// we control the data path in the tests, so we don't need to set it here
62+
return settings;
5863
}
5964

60-
private void runLoadStateTest(boolean hasMissingCustoms, boolean preserveUnknownCustoms) throws IOException {
61-
final Metadata latestMetadata = randomMeta();
62-
final XContentBuilder builder = JsonXContent.contentBuilder();
63-
builder.startObject();
64-
Metadata.FORMAT.toXContent(builder, latestMetadata);
65-
builder.endObject();
66-
67-
XContentParserConfiguration parserConfig = hasMissingCustoms
68-
? parserConfig().withRegistry(ElasticsearchNodeCommand.namedXContentRegistry)
69-
: parserConfig();
70-
Metadata loadedMetadata;
71-
try (XContentParser parser = createParser(parserConfig, JsonXContent.jsonXContent, BytesReference.bytes(builder))) {
72-
loadedMetadata = Metadata.fromXContent(parser);
73-
}
74-
assertThat(loadedMetadata.clusterUUID(), not(equalTo("_na_")));
75-
assertThat(loadedMetadata.clusterUUID(), equalTo(latestMetadata.clusterUUID()));
76-
assertThat(loadedMetadata.getProject().dataStreams(), equalTo(latestMetadata.getProject().dataStreams()));
65+
private void runLoadStateTest(boolean hasMissingCustoms) throws IOException {
66+
final var dataPath = createTempDir();
67+
final Settings settings = Settings.builder()
68+
.putList(Environment.PATH_DATA_SETTING.getKey(), List.of(dataPath.toString()))
69+
.put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toAbsolutePath())
70+
.build();
7771

78-
// make sure the index tombstones are the same too
79-
if (hasMissingCustoms) {
72+
try (var nodeEnvironment = newNodeEnvironment(settings)) {
73+
final var persistedClusterStateServiceWithFullRegistry = new PersistedClusterStateService(
74+
nodeEnvironment,
75+
xContentRegistry(),
76+
new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS),
77+
() -> 0L,
78+
() -> false
79+
);
80+
81+
// 1. Simulating persisting cluster state by a running node
82+
final long initialTerm = randomNonNegativeLong();
83+
final Metadata initialMetadata = randomMeta(hasMissingCustoms);
84+
final ClusterState initialState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(initialMetadata).build();
85+
try (var writer = persistedClusterStateServiceWithFullRegistry.createWriter()) {
86+
writer.writeFullStateAndCommit(initialTerm, initialState);
87+
}
88+
89+
// 2. Simulating loading the persisted cluster state by the CLI
90+
final var persistedClusterStateServiceForNodeCommand = ElasticsearchNodeCommand.createPersistedClusterStateService(
91+
Settings.EMPTY,
92+
new Path[] { dataPath }
93+
);
94+
final Tuple<Long, ClusterState> loadedTermAndClusterState = ElasticsearchNodeCommand.loadTermAndClusterState(
95+
persistedClusterStateServiceForNodeCommand,
96+
TestEnvironment.newEnvironment(settings)
97+
);
98+
99+
assertThat(loadedTermAndClusterState.v1(), equalTo(initialTerm));
100+
101+
final var loadedMetadata = loadedTermAndClusterState.v2().metadata();
102+
assertThat(loadedMetadata.clusterUUID(), not(equalTo("_na_")));
103+
assertThat(loadedMetadata.clusterUUID(), equalTo(initialMetadata.clusterUUID()));
104+
assertThat(loadedMetadata.getProject().dataStreams(), equalTo(initialMetadata.getProject().dataStreams()));
80105
assertNotNull(loadedMetadata.getProject().custom(IndexGraveyard.TYPE));
81106
assertThat(
82107
loadedMetadata.getProject().custom(IndexGraveyard.TYPE),
83108
instanceOf(ElasticsearchNodeCommand.UnknownProjectCustom.class)
84109
);
110+
if (hasMissingCustoms) {
111+
assertThat(
112+
loadedMetadata.getProject().custom(TestMissingProjectCustomMetadata.TYPE),
113+
instanceOf(ElasticsearchNodeCommand.UnknownProjectCustom.class)
114+
);
115+
assertThat(
116+
loadedMetadata.custom(TestMissingClusterCustomMetadata.TYPE),
117+
instanceOf(ElasticsearchNodeCommand.UnknownClusterCustom.class)
118+
);
119+
} else {
120+
assertNull(loadedMetadata.getProject().custom(TestMissingProjectCustomMetadata.TYPE));
121+
assertNull(loadedMetadata.custom(TestMissingClusterCustomMetadata.TYPE));
122+
}
123+
124+
final long newTerm = initialTerm + 1;
125+
try (var writer = persistedClusterStateServiceForNodeCommand.createWriter()) {
126+
writer.writeFullStateAndCommit(newTerm, ClusterState.builder(ClusterState.EMPTY_STATE).metadata(loadedMetadata).build());
127+
}
85128

86-
if (preserveUnknownCustoms) {
87-
// check that we reserialize unknown metadata correctly again
88-
final Path tempdir = createTempDir();
89-
Metadata.FORMAT.write(loadedMetadata, tempdir);
90-
final Metadata reloadedMetadata = Metadata.FORMAT.loadLatestState(logger, xContentRegistry(), tempdir);
91-
assertThat(reloadedMetadata.getProject().indexGraveyard(), equalTo(latestMetadata.getProject().indexGraveyard()));
129+
// 3. Simulate node restart after updating on-disk state with the CLI tool
130+
final var bestOnDiskState = persistedClusterStateServiceWithFullRegistry.loadBestOnDiskState();
131+
assertThat(bestOnDiskState.currentTerm, equalTo(newTerm));
132+
final Metadata reloadedMetadata = bestOnDiskState.metadata;
133+
assertThat(reloadedMetadata.getProject().indexGraveyard(), equalTo(initialMetadata.getProject().indexGraveyard()));
134+
if (hasMissingCustoms) {
135+
assertThat(
136+
reloadedMetadata.getProject().custom(TestMissingProjectCustomMetadata.TYPE),
137+
equalTo(initialMetadata.getProject().custom(TestMissingProjectCustomMetadata.TYPE))
138+
);
139+
} else {
140+
assertNull(reloadedMetadata.getProject().custom(TestMissingProjectCustomMetadata.TYPE));
92141
}
93-
} else {
94-
assertThat(loadedMetadata.getProject().indexGraveyard(), equalTo(latestMetadata.getProject().indexGraveyard()));
95142
}
96143
}
97144

98-
private Metadata randomMeta() {
99-
@FixForMultiProject // Pass random project ID when usages are namespaced.
145+
private Metadata randomMeta(boolean hasMissingCustoms) {
146+
Metadata.Builder mdBuilder = Metadata.builder();
147+
mdBuilder.generateClusterUuidIfNeeded();
148+
@FixForMultiProject(description = "Pass random project ID when usages are namespaced")
100149
ProjectMetadata.Builder projectBuilder = ProjectMetadata.builder(ProjectId.DEFAULT);
101150
int numDelIndices = randomIntBetween(0, 5);
102151
final IndexGraveyard.Builder graveyard = IndexGraveyard.builder();
@@ -112,15 +161,90 @@ private Metadata randomMeta() {
112161
}
113162
}
114163
projectBuilder.indexGraveyard(graveyard.build());
115-
return Metadata.builder().generateClusterUuidIfNeeded().put(projectBuilder).build();
164+
if (hasMissingCustoms) {
165+
projectBuilder.putCustom(
166+
TestMissingProjectCustomMetadata.TYPE,
167+
new TestMissingProjectCustomMetadata("test missing project custom metadata")
168+
);
169+
mdBuilder.putCustom(
170+
TestMissingClusterCustomMetadata.TYPE,
171+
new TestMissingClusterCustomMetadata("test missing cluster custom metadata")
172+
);
173+
}
174+
return mdBuilder.put(projectBuilder).build();
116175
}
117176

118177
@Override
119178
protected NamedXContentRegistry xContentRegistry() {
120179
return new NamedXContentRegistry(
121-
Stream.of(ClusterModule.getNamedXWriteables().stream(), IndicesModule.getNamedXContents().stream())
122-
.flatMap(Function.identity())
123-
.toList()
180+
Stream.of(
181+
ClusterModule.getNamedXWriteables().stream(),
182+
IndicesModule.getNamedXContents().stream(),
183+
Stream.of(
184+
new NamedXContentRegistry.Entry(
185+
Metadata.ProjectCustom.class,
186+
new ParseField(TestMissingProjectCustomMetadata.TYPE),
187+
parser -> TestMissingProjectCustomMetadata.fromXContent(TestMissingProjectCustomMetadata::new, parser)
188+
),
189+
new NamedXContentRegistry.Entry(
190+
Metadata.ClusterCustom.class,
191+
new ParseField(TestMissingClusterCustomMetadata.TYPE),
192+
parser -> TestMissingClusterCustomMetadata.fromXContent(TestMissingClusterCustomMetadata::new, parser)
193+
)
194+
)
195+
).flatMap(Function.identity()).toList()
124196
);
125197
}
198+
199+
// "Missing" really means _not_ registered in ElasticsearchNodeCommand's xContentRegistry so that it will be read and written
200+
// as a generic unknown project custom, see also ElasticsearchNodeCommand.UnknownProjectCustom. Note it is registered in the
201+
// full xContentRegistry used by the Elasticsearch node. That is how it got written in the first place.
202+
private static class TestMissingProjectCustomMetadata extends TestProjectCustomMetadata {
203+
204+
static final String TYPE = "missing_project_custom_metadata";
205+
206+
TestMissingProjectCustomMetadata(String data) {
207+
super(data);
208+
}
209+
210+
@Override
211+
public String getWriteableName() {
212+
return TYPE;
213+
}
214+
215+
@Override
216+
public TransportVersion getMinimalSupportedVersion() {
217+
return TransportVersion.current();
218+
}
219+
220+
@Override
221+
public EnumSet<Metadata.XContentContext> context() {
222+
return EnumSet.of(Metadata.XContentContext.GATEWAY);
223+
}
224+
}
225+
226+
// Similar "Missing" custom but in the cluster scope. See also the comment above for TestMissingProjectCustomMetadata.
227+
private static class TestMissingClusterCustomMetadata extends TestClusterCustomMetadata {
228+
229+
static final String TYPE = "missing_cluster_custom_metadata";
230+
231+
TestMissingClusterCustomMetadata(String data) {
232+
super(data);
233+
}
234+
235+
@Override
236+
public String getWriteableName() {
237+
return TYPE;
238+
}
239+
240+
@Override
241+
public TransportVersion getMinimalSupportedVersion() {
242+
return TransportVersion.current();
243+
}
244+
245+
@Override
246+
public EnumSet<Metadata.XContentContext> context() {
247+
return EnumSet.of(Metadata.XContentContext.GATEWAY);
248+
}
249+
}
126250
}

0 commit comments

Comments
 (0)