/
IndexMetadataVerifier.java
298 lines (275 loc) · 13.8 KB
/
IndexMetadataVerifier.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.cluster.metadata;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.similarities.BM25Similarity;
import org.apache.lucene.search.similarities.Similarity;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.TriFunction;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.MapperRegistry;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.similarity.SimilarityService;
import org.elasticsearch.script.ScriptCompiler;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import static org.elasticsearch.core.Strings.format;
/**
* This service is responsible for verifying index metadata when an index is introduced
* to the cluster, for example when restarting nodes, importing dangling indices, or restoring
* an index from a snapshot repository.
*
* It performs the following:
* - Verifies the index version is not too old.
* - Tries to parse the mappings to catch compatibility bugs early.
* - Identifies unknown and invalid settings and archives them.
*/
public class IndexMetadataVerifier {
private static final Logger logger = LogManager.getLogger(IndexMetadataVerifier.class);
private final Settings settings;
private final ClusterService clusterService;
private final XContentParserConfiguration parserConfiguration;
private final MapperRegistry mapperRegistry;
private final IndexScopedSettings indexScopedSettings;
private final ScriptCompiler scriptService;
public IndexMetadataVerifier(
Settings settings,
ClusterService clusterService,
NamedXContentRegistry xContentRegistry,
MapperRegistry mapperRegistry,
IndexScopedSettings indexScopedSettings,
ScriptCompiler scriptCompiler
) {
this.settings = settings;
this.clusterService = clusterService;
this.parserConfiguration = XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry)
.withDeprecationHandler(LoggingDeprecationHandler.INSTANCE);
this.mapperRegistry = mapperRegistry;
this.indexScopedSettings = indexScopedSettings;
this.scriptService = scriptCompiler;
}
/**
* Checks that the index can be upgraded to the current version of the master node.
*
* <p>
* If the index does not need upgrade it returns the index metadata unchanged, otherwise it returns a modified index metadata. If index
* cannot be updated the method throws an exception.
*/
public IndexMetadata verifyIndexMetadata(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
checkSupportedVersion(indexMetadata, minimumIndexCompatibilityVersion);
// First convert any shared_cache searchable snapshot indices to only use _tier_preference: data_frozen
IndexMetadata newMetadata = convertSharedCacheTierPreference(indexMetadata);
// Remove _tier routing settings if available, because though these are technically not
// invalid settings, since they are now removed the FilterAllocationDecider treats them as
// regular attribute filters, and shards cannot be allocated.
newMetadata = removeTierFiltering(newMetadata);
// Next we have to run this otherwise if we try to create IndexSettings
// with broken settings it would fail in checkMappingsCompatibility
newMetadata = archiveOrDeleteBrokenIndexSettings(newMetadata);
checkMappingsCompatibility(newMetadata);
return newMetadata;
}
/**
* Check that the index version is compatible. Elasticsearch does not support indices created before the
* previous major version.
*/
private static void checkSupportedVersion(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
boolean isSupportedVersion = indexMetadata.getCompatibilityVersion().onOrAfter(minimumIndexCompatibilityVersion);
if (isSupportedVersion == false) {
throw new IllegalStateException(
"The index "
+ indexMetadata.getIndex()
+ " has current compatibility version ["
+ indexMetadata.getCompatibilityVersion().toReleaseVersion()
+ "] but the minimum compatible version is ["
+ minimumIndexCompatibilityVersion.toReleaseVersion()
+ "]. It should be re-indexed in Elasticsearch "
+ (Version.CURRENT.major - 1)
+ ".x before upgrading to "
+ Build.current().version()
+ "."
);
}
}
/**
* Check that we can parse the mappings.
*
* This is not strictly necessary, since we parse the mappings later when loading the index and will
* catch issues then. But it lets us fail very quickly and clearly: if there is a mapping incompatibility,
* the node refuses to start instead of starting but having unallocated shards.
*
* Note that we don't expect users to encounter mapping incompatibilities, since our index compatibility
* policy guarantees we can read mappings from previous compatible index versions. A failure here would
* indicate a compatibility bug (which are unfortunately not that uncommon).
*/
private void checkMappingsCompatibility(IndexMetadata indexMetadata) {
try {
// We cannot instantiate real analysis server or similarity service at this point because the node
// might not have been started yet. However, we don't really need real analyzers or similarities at
// this stage - so we can fake it using constant maps accepting every key.
// This is ok because all used similarities and analyzers for this index were known before the upgrade.
// Missing analyzers and similarities plugin will still trigger the appropriate error during the
// actual upgrade.
IndexSettings indexSettings = new IndexSettings(indexMetadata, this.settings);
final Map<String, TriFunction<Settings, IndexVersion, ScriptService, Similarity>> similarityMap = new AbstractMap<>() {
@Override
public boolean containsKey(Object key) {
return true;
}
@Override
public TriFunction<Settings, IndexVersion, ScriptService, Similarity> get(Object key) {
assert key instanceof String : "key must be a string but was: " + key.getClass();
return (settings, version, scriptService) -> new BM25Similarity();
}
// this entrySet impl isn't fully correct but necessary as SimilarityService will iterate
// over all similarities
@Override
public Set<Entry<String, TriFunction<Settings, IndexVersion, ScriptService, Similarity>>> entrySet() {
return Collections.emptySet();
}
};
SimilarityService similarityService = new SimilarityService(indexSettings, null, similarityMap);
final NamedAnalyzer fakeDefault = new NamedAnalyzer("default", AnalyzerScope.INDEX, new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String fieldName) {
throw new UnsupportedOperationException("shouldn't be here");
}
});
try (
MapperService mapperService = new MapperService(
clusterService,
indexSettings,
(type, name) -> new NamedAnalyzer(name, AnalyzerScope.INDEX, fakeDefault.analyzer()),
parserConfiguration,
similarityService,
mapperRegistry,
() -> null,
indexSettings.getMode().idFieldMapperWithoutFieldData(),
scriptService
)
) {
mapperService.merge(indexMetadata, MapperService.MergeReason.MAPPING_RECOVERY);
}
} catch (Exception ex) {
// Wrap the inner exception so we have the index name in the exception message
throw new IllegalStateException("Failed to parse mappings for index [" + indexMetadata.getIndex() + "]", ex);
}
}
/**
* Identify invalid or unknown index settings and archive them. This leniency allows Elasticsearch to load
* indices even if they contain old settings that are no longer valid.
*
* When we find an invalid setting on a system index, we simply remove it instead of archiving. System indices
* are managed by Elasticsearch and manual modification of settings is limited and sometimes impossible.
*/
IndexMetadata archiveOrDeleteBrokenIndexSettings(IndexMetadata indexMetadata) {
final Settings settings = indexMetadata.getSettings();
final Settings newSettings;
if (indexMetadata.isSystem()) {
newSettings = indexScopedSettings.deleteUnknownOrInvalidSettings(
settings,
e -> logger.warn(
"{} deleting unknown system index setting: [{}] with value [{}]",
indexMetadata.getIndex(),
e.getKey(),
e.getValue()
),
(e, ex) -> logger.warn(
() -> format(
"%s deleting invalid system index setting: [%s] with value [%s]",
indexMetadata.getIndex(),
e.getKey(),
e.getValue()
),
ex
)
);
} else {
newSettings = indexScopedSettings.archiveUnknownOrInvalidSettings(
settings,
e -> logger.warn(
"{} ignoring unknown index setting: [{}] with value [{}]; archiving",
indexMetadata.getIndex(),
e.getKey(),
e.getValue()
),
(e, ex) -> logger.warn(
() -> format(
"%s ignoring invalid index setting: [%s] with value [%s]; archiving",
indexMetadata.getIndex(),
e.getKey(),
e.getValue()
),
ex
)
);
}
if (newSettings != settings) {
return IndexMetadata.builder(indexMetadata).settings(newSettings).build();
} else {
return indexMetadata;
}
}
/**
* Convert shared_cache searchable snapshot indices to only specify
* _tier_preference: data_frozen, removing any pre-existing tier allocation rules.
*/
static IndexMetadata convertSharedCacheTierPreference(IndexMetadata indexMetadata) {
// Only remove these settings for a shared_cache searchable snapshot
if (indexMetadata.isPartialSearchableSnapshot()) {
final Settings settings = indexMetadata.getSettings();
final Settings.Builder settingsBuilder = Settings.builder().put(settings);
// Clear any allocation rules other than preference for tier
settingsBuilder.remove("index.routing.allocation.include._tier");
settingsBuilder.remove("index.routing.allocation.exclude._tier");
settingsBuilder.remove("index.routing.allocation.require._tier");
// Override the tier preference to be only on frozen nodes, regardless of its current setting
settingsBuilder.put("index.routing.allocation.include._tier_preference", "data_frozen");
final Settings newSettings = settingsBuilder.build();
if (settings.equals(newSettings)) {
return indexMetadata;
} else {
return IndexMetadata.builder(indexMetadata).settings(newSettings).build();
}
} else {
return indexMetadata;
}
}
/**
* Removes index level ._tier allocation filters, if they exist
*/
static IndexMetadata removeTierFiltering(IndexMetadata indexMetadata) {
final Settings settings = indexMetadata.getSettings();
final Settings.Builder settingsBuilder = Settings.builder().put(settings);
// Clear any allocation rules other than preference for tier
settingsBuilder.remove("index.routing.allocation.include._tier");
settingsBuilder.remove("index.routing.allocation.exclude._tier");
settingsBuilder.remove("index.routing.allocation.require._tier");
final Settings newSettings = settingsBuilder.build();
if (settings.equals(newSettings)) {
return indexMetadata;
} else {
return IndexMetadata.builder(indexMetadata).settings(newSettings).build();
}
}
}