99
1010package org .elasticsearch .cluster .coordination ;
1111
12+ import org .elasticsearch .TransportVersion ;
1213import org .elasticsearch .cluster .ClusterModule ;
14+ import org .elasticsearch .cluster .ClusterState ;
1315import org .elasticsearch .cluster .metadata .IndexGraveyard ;
1416import org .elasticsearch .cluster .metadata .IndexMetadata ;
1517import org .elasticsearch .cluster .metadata .Metadata ;
1618import org .elasticsearch .cluster .metadata .ProjectId ;
1719import org .elasticsearch .cluster .metadata .ProjectMetadata ;
1820import 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 ;
2023import 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 ;
2128import org .elasticsearch .index .Index ;
2229import org .elasticsearch .indices .IndicesModule ;
2330import org .elasticsearch .test .ESTestCase ;
31+ import org .elasticsearch .test .TestClusterCustomMetadata ;
32+ import org .elasticsearch .test .TestProjectCustomMetadata ;
2433import 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
3036import java .io .IOException ;
3137import java .nio .file .Path ;
38+ import java .util .EnumSet ;
3239import java .util .List ;
3340import java .util .function .Function ;
3441import java .util .stream .Stream ;
4148
4249public 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