9
9
10
10
package org .elasticsearch .cluster .coordination ;
11
11
12
+ import org .elasticsearch .TransportVersion ;
12
13
import org .elasticsearch .cluster .ClusterModule ;
14
+ import org .elasticsearch .cluster .ClusterState ;
13
15
import org .elasticsearch .cluster .metadata .IndexGraveyard ;
14
16
import org .elasticsearch .cluster .metadata .IndexMetadata ;
15
17
import org .elasticsearch .cluster .metadata .Metadata ;
16
18
import org .elasticsearch .cluster .metadata .ProjectId ;
17
19
import org .elasticsearch .cluster .metadata .ProjectMetadata ;
18
20
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 ;
20
23
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 ;
21
28
import org .elasticsearch .index .Index ;
22
29
import org .elasticsearch .indices .IndicesModule ;
23
30
import org .elasticsearch .test .ESTestCase ;
31
+ import org .elasticsearch .test .TestClusterCustomMetadata ;
32
+ import org .elasticsearch .test .TestProjectCustomMetadata ;
24
33
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 ;
29
35
30
36
import java .io .IOException ;
31
37
import java .nio .file .Path ;
38
+ import java .util .EnumSet ;
32
39
import java .util .List ;
33
40
import java .util .function .Function ;
34
41
import java .util .stream .Stream ;
41
48
42
49
public class ElasticsearchNodeCommandTests extends ESTestCase {
43
50
44
- public void testLoadStateWithoutMissingCustoms () throws IOException {
45
- runLoadStateTest (false , false );
46
- }
47
-
48
51
public void testLoadStateWithoutMissingCustomsButPreserved () throws IOException {
49
- runLoadStateTest (false , true );
52
+ runLoadStateTest (false );
50
53
}
51
54
52
55
public void testLoadStateWithMissingCustomsButPreserved () throws IOException {
53
- runLoadStateTest (true , true );
56
+ runLoadStateTest (true );
54
57
}
55
58
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 ;
58
63
}
59
64
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 ();
77
71
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 ()));
80
105
assertNotNull (loadedMetadata .getProject ().custom (IndexGraveyard .TYPE ));
81
106
assertThat (
82
107
loadedMetadata .getProject ().custom (IndexGraveyard .TYPE ),
83
108
instanceOf (ElasticsearchNodeCommand .UnknownProjectCustom .class )
84
109
);
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
+ }
85
128
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 ));
92
141
}
93
- } else {
94
- assertThat (loadedMetadata .getProject ().indexGraveyard (), equalTo (latestMetadata .getProject ().indexGraveyard ()));
95
142
}
96
143
}
97
144
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" )
100
149
ProjectMetadata .Builder projectBuilder = ProjectMetadata .builder (ProjectId .DEFAULT );
101
150
int numDelIndices = randomIntBetween (0 , 5 );
102
151
final IndexGraveyard .Builder graveyard = IndexGraveyard .builder ();
@@ -112,15 +161,90 @@ private Metadata randomMeta() {
112
161
}
113
162
}
114
163
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 ();
116
175
}
117
176
118
177
@ Override
119
178
protected NamedXContentRegistry xContentRegistry () {
120
179
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 ()
124
196
);
125
197
}
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
+ }
126
250
}
0 commit comments