Skip to content

Commit fd2492f

Browse files
authored
Optimize usage calculation in ILM policies retrieval API (elastic#106953)
Optimize calculating the usage of ILM policies in the `GET _ilm/policy` and `GET _ilm/policy/<policy_id>` endpoints by xtracting a separate class that pre-computes some parts on initialization (i.e. only once per request) and then uses those pre-computed parts when calculating the usage for an individual policy. By precomputing all the usages, the class makes a tradeoff by using a little bit more memory to significantly improve the overall processing time.
1 parent 242a195 commit fd2492f

File tree

7 files changed

+430
-236
lines changed

7 files changed

+430
-236
lines changed

docs/changelog/106953.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 106953
2+
summary: Optimize usage calculation in ILM policies retrieval API
3+
area: ILM+SLM
4+
type: enhancement
5+
issues:
6+
- 105773

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import java.time.Instant;
6363
import java.util.ArrayList;
6464
import java.util.Arrays;
65+
import java.util.Collection;
6566
import java.util.Collections;
6667
import java.util.Comparator;
6768
import java.util.HashMap;
@@ -1087,9 +1088,8 @@ static Set<String> dataStreamsExclusivelyUsingTemplates(final ProjectMetadata pr
10871088
.map(templateName -> projectMetadata.templatesV2().get(templateName))
10881089
.filter(Objects::nonNull)
10891090
.map(ComposableIndexTemplate::indexPatterns)
1090-
.map(Set::copyOf)
1091-
.reduce(Sets::union)
1092-
.orElse(Set.of());
1091+
.flatMap(List::stream)
1092+
.collect(Collectors.toSet());
10931093

10941094
return projectMetadata.dataStreams()
10951095
.values()
@@ -1099,9 +1099,10 @@ static Set<String> dataStreamsExclusivelyUsingTemplates(final ProjectMetadata pr
10991099
.filter(ds -> {
11001100
// Retrieve the templates that match the data stream name ordered by priority
11011101
List<Tuple<String, ComposableIndexTemplate>> candidates = findV2CandidateTemplates(
1102-
projectMetadata,
1102+
projectMetadata.templatesV2().entrySet(),
11031103
ds.getName(),
1104-
ds.isHidden()
1104+
ds.isHidden(),
1105+
false
11051106
);
11061107
if (candidates.isEmpty()) {
11071108
throw new IllegalStateException("Data stream " + ds.getName() + " did not match any composable index templates.");
@@ -1110,13 +1111,10 @@ static Set<String> dataStreamsExclusivelyUsingTemplates(final ProjectMetadata pr
11101111
// Limit data streams that can ONLY use any of the specified templates, we do this by filtering
11111112
// the matching templates that are others than the ones requested and could be a valid template to use.
11121113
return candidates.stream()
1113-
.filter(
1114+
.noneMatch(
11141115
template -> templateNames.contains(template.v1()) == false
11151116
&& isGlobalAndHasIndexHiddenSetting(projectMetadata, template.v2(), template.v1()) == false
1116-
)
1117-
.map(Tuple::v1)
1118-
.toList()
1119-
.isEmpty();
1117+
);
11201118
})
11211119
.map(DataStream::getName)
11221120
.collect(Collectors.toSet());
@@ -1357,7 +1355,41 @@ public static List<IndexTemplateMetadata> findV1Templates(
13571355
*/
13581356
@Nullable
13591357
public static String findV2Template(ProjectMetadata projectMetadata, String indexName, boolean isHidden) {
1360-
final List<Tuple<String, ComposableIndexTemplate>> candidates = findV2CandidateTemplates(projectMetadata, indexName, isHidden);
1358+
return findV2Template(projectMetadata, projectMetadata.templatesV2().entrySet(), indexName, isHidden, false);
1359+
}
1360+
1361+
/**
1362+
* Return the name (id) of the highest matching index template out of the provided templates (that <i>need</i> to be sorted descending
1363+
* on priority beforehand), or the given index name. In the event that no templates are matched, {@code null} is returned.
1364+
*/
1365+
@Nullable
1366+
public static String findV2TemplateFromSortedList(
1367+
ProjectMetadata projectMetadata,
1368+
Collection<Map.Entry<String, ComposableIndexTemplate>> templates,
1369+
String indexName,
1370+
boolean isHidden
1371+
) {
1372+
return findV2Template(projectMetadata, templates, indexName, isHidden, true);
1373+
}
1374+
1375+
/**
1376+
* Return the name (id) of the highest matching index template, out of the provided templates, for the given index name. In
1377+
* the event that no templates are matched, {@code null} is returned.
1378+
*/
1379+
@Nullable
1380+
private static String findV2Template(
1381+
ProjectMetadata projectMetadata,
1382+
Collection<Map.Entry<String, ComposableIndexTemplate>> templates,
1383+
String indexName,
1384+
boolean isHidden,
1385+
boolean exitOnFirstMatch
1386+
) {
1387+
final List<Tuple<String, ComposableIndexTemplate>> candidates = findV2CandidateTemplates(
1388+
templates,
1389+
indexName,
1390+
isHidden,
1391+
exitOnFirstMatch
1392+
);
13611393
if (candidates.isEmpty()) {
13621394
return null;
13631395
}
@@ -1386,33 +1418,39 @@ public static String findV2Template(ProjectMetadata projectMetadata, String inde
13861418
* Return an ordered list of the name (id) and composable index templates that would apply to an index. The first
13871419
* one is the winner template that is applied to this index. In the event that no templates are matched,
13881420
* an empty list is returned.
1421+
* @param templates a list of template entries (name, template) - needs to be sorted when {@code exitOnFirstMatch} is {@code true}
1422+
* @param indexName the index (or data stream) name that should be used for matching the index patterns on the templates
1423+
* @param isHidden whether {@code indexName} belongs to a hidden index - this option is redundant for data streams, as backing indices
1424+
* of data streams will always be returned, regardless of whether the data stream is hidden or not
1425+
* @param exitOnFirstMatch if true, we return immediately after finding a match. That means that the <code>templates</code>
1426+
* parameter needs to be sorted based on priority (descending) for this method to return a sensible result,
1427+
* otherwise this method would just return the first template that matches the name, in an unspecified order
13891428
*/
1390-
static List<Tuple<String, ComposableIndexTemplate>> findV2CandidateTemplates(
1391-
ProjectMetadata projectMetadata,
1429+
private static List<Tuple<String, ComposableIndexTemplate>> findV2CandidateTemplates(
1430+
Collection<Map.Entry<String, ComposableIndexTemplate>> templates,
13921431
String indexName,
1393-
boolean isHidden
1432+
boolean isHidden,
1433+
boolean exitOnFirstMatch
13941434
) {
1435+
assert exitOnFirstMatch == false || areTemplatesSorted(templates) : "Expected templates to be sorted";
13951436
final String resolvedIndexName = IndexNameExpressionResolver.DateMathExpressionResolver.resolveExpression(indexName);
13961437
final Predicate<String> patternMatchPredicate = pattern -> Regex.simpleMatch(pattern, resolvedIndexName);
13971438
final List<Tuple<String, ComposableIndexTemplate>> candidates = new ArrayList<>();
1398-
for (Map.Entry<String, ComposableIndexTemplate> entry : projectMetadata.templatesV2().entrySet()) {
1439+
for (Map.Entry<String, ComposableIndexTemplate> entry : templates) {
13991440
final String name = entry.getKey();
14001441
final ComposableIndexTemplate template = entry.getValue();
14011442
/*
14021443
* We do not ordinarily return match-all templates for hidden indices. But all backing indices for data streams are hidden,
14031444
* and we do want to return even match-all templates for those. Not doing so can result in a situation where a data stream is
14041445
* built with a template that none of its indices match.
14051446
*/
1406-
if (isHidden == false || template.getDataStreamTemplate() != null) {
1407-
if (anyMatch(template.indexPatterns(), patternMatchPredicate)) {
1408-
candidates.add(Tuple.tuple(name, template));
1409-
}
1410-
} else {
1411-
final boolean isNotMatchAllTemplate = noneMatch(template.indexPatterns(), Regex::isMatchAllPattern);
1412-
if (isNotMatchAllTemplate) {
1413-
if (anyMatch(template.indexPatterns(), patternMatchPredicate)) {
1414-
candidates.add(Tuple.tuple(name, template));
1415-
}
1447+
if (anyMatch(template.indexPatterns(), Regex::isMatchAllPattern) && isHidden && template.getDataStreamTemplate() == null) {
1448+
continue;
1449+
}
1450+
if (anyMatch(template.indexPatterns(), patternMatchPredicate)) {
1451+
candidates.add(Tuple.tuple(name, template));
1452+
if (exitOnFirstMatch) {
1453+
return candidates;
14161454
}
14171455
}
14181456
}
@@ -1421,6 +1459,17 @@ static List<Tuple<String, ComposableIndexTemplate>> findV2CandidateTemplates(
14211459
return candidates;
14221460
}
14231461

1462+
private static boolean areTemplatesSorted(Collection<Map.Entry<String, ComposableIndexTemplate>> templates) {
1463+
ComposableIndexTemplate previousTemplate = null;
1464+
for (Map.Entry<String, ComposableIndexTemplate> template : templates) {
1465+
if (previousTemplate != null && template.getValue().priorityOrZero() > previousTemplate.priorityOrZero()) {
1466+
return false;
1467+
}
1468+
previousTemplate = template.getValue();
1469+
}
1470+
return true;
1471+
}
1472+
14241473
// Checks if a global template specifies the `index.hidden` setting. This check is important because a global
14251474
// template shouldn't specify the `index.hidden` setting, we leave it up to the caller to handle this situation.
14261475
private static boolean isGlobalAndHasIndexHiddenSetting(
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.ilm;
9+
10+
import org.apache.lucene.util.CollectionUtil;
11+
import org.elasticsearch.action.support.IndicesOptions;
12+
import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
13+
import org.elasticsearch.cluster.metadata.IndexMetadata;
14+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
15+
import org.elasticsearch.cluster.metadata.ItemUsage;
16+
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
17+
import org.elasticsearch.cluster.metadata.ProjectMetadata;
18+
import org.elasticsearch.common.regex.Regex;
19+
import org.elasticsearch.common.settings.Settings;
20+
import org.elasticsearch.common.util.Maps;
21+
22+
import java.util.ArrayList;
23+
import java.util.Comparator;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
/**
29+
* A class that can be used to calculate the usages of ILM policies across the cluster. By precomputing all the usages,
30+
* the class makes a tradeoff by using a little bit more memory to significantly improve the overall processing time.
31+
*/
32+
public class LifecyclePolicyUsageCalculator {
33+
34+
/** A map from policy name to list of composable templates that use that policy. */
35+
private final Map<String, List<String>> policyToTemplates;
36+
/** A map from policy name to list of data streams that use that policy. */
37+
private final Map<String, List<String>> policyToDataStreams;
38+
/** A map from policy name to list of indices that use that policy. */
39+
private final Map<String, List<String>> policyToIndices;
40+
41+
public LifecyclePolicyUsageCalculator(
42+
final IndexNameExpressionResolver indexNameExpressionResolver,
43+
ProjectMetadata project,
44+
List<String> requestedPolicyNames
45+
) {
46+
final IndexLifecycleMetadata ilmMetadata = project.custom(IndexLifecycleMetadata.TYPE);
47+
// We're making a bet here that if the `name` contains a wildcard, there's a large chance it'll simply match all policies.
48+
final var expectedSize = Regex.isSimpleMatchPattern(requestedPolicyNames.get(0))
49+
? ilmMetadata.getPolicyMetadatas().size()
50+
: requestedPolicyNames.size();
51+
52+
// We keep a map from composable template name to policy name to avoid having to resolve the template settings to determine
53+
// the template's policy twice.
54+
final Map<String, String> templateToPolicy = new HashMap<>();
55+
56+
// Build the map of which policy is used by which index templates.
57+
policyToTemplates = Maps.newHashMapWithExpectedSize(expectedSize);
58+
for (Map.Entry<String, ComposableIndexTemplate> entry : project.templatesV2().entrySet()) {
59+
Settings settings = MetadataIndexTemplateService.resolveSettings(entry.getValue(), project.componentTemplates());
60+
final var policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(settings);
61+
// We only store the template if its policy matched any of the requested names.
62+
if (doesPolicyMatchAnyName(policyName, requestedPolicyNames) == false) {
63+
continue;
64+
}
65+
policyToTemplates.computeIfAbsent(policyName, k -> new ArrayList<>()).add(entry.getKey());
66+
templateToPolicy.put(entry.getKey(), policyName);
67+
}
68+
69+
// Sort all templates by descending priority. That way, findV2Template can exit on the first-matched template.
70+
final var indexTemplates = new ArrayList<>(project.templatesV2().entrySet());
71+
CollectionUtil.timSort(indexTemplates, Comparator.comparing(entry -> entry.getValue().priorityOrZero(), Comparator.reverseOrder()));
72+
73+
// Build the map of which policy is used by which data streams.
74+
policyToDataStreams = Maps.newHashMapWithExpectedSize(expectedSize);
75+
final List<String> allDataStreams = indexNameExpressionResolver.dataStreamNames(
76+
project,
77+
IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN_NO_SELECTOR
78+
);
79+
for (String dataStream : allDataStreams) {
80+
// Find the index template with the highest priority that matches this data stream's name.
81+
String indexTemplate = MetadataIndexTemplateService.findV2TemplateFromSortedList(project, indexTemplates, dataStream, false);
82+
if (indexTemplate == null) {
83+
assert false : "Data stream [" + dataStream + "] has no matching template";
84+
continue;
85+
}
86+
final var policyName = templateToPolicy.get(indexTemplate);
87+
// If there was no entry, either the template didn't specify an ILM policy or the policy didn't match any of the requested names
88+
if (policyName == null) {
89+
continue;
90+
}
91+
policyToDataStreams.computeIfAbsent(policyName, k -> new ArrayList<>()).add(dataStream);
92+
}
93+
94+
// Build the map of which policy is used by which indices.
95+
policyToIndices = Maps.newHashMapWithExpectedSize(expectedSize);
96+
for (IndexMetadata indexMetadata : project.indices().values()) {
97+
final var policyName = indexMetadata.getLifecyclePolicyName();
98+
// We only store the index if its policy matched any of the specified names.
99+
if (doesPolicyMatchAnyName(policyName, requestedPolicyNames) == false) {
100+
continue;
101+
}
102+
policyToIndices.computeIfAbsent(policyName, k -> new ArrayList<>()).add(indexMetadata.getIndex().getName());
103+
}
104+
}
105+
106+
/**
107+
* Retrieves the pre-calculated indices, data streams, and composable templates that use the given policy.
108+
*/
109+
public ItemUsage retrieveCalculatedUsage(String policyName) {
110+
return new ItemUsage(
111+
policyToIndices.getOrDefault(policyName, List.of()),
112+
policyToDataStreams.getOrDefault(policyName, List.of()),
113+
policyToTemplates.getOrDefault(policyName, List.of())
114+
);
115+
}
116+
117+
private boolean doesPolicyMatchAnyName(String policyName, List<String> names) {
118+
for (var name : names) {
119+
if (Regex.simpleMatch(name, policyName)) {
120+
return true;
121+
}
122+
}
123+
return false;
124+
}
125+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,8 @@
88
package org.elasticsearch.xpack.core.ilm;
99

1010
import org.elasticsearch.ElasticsearchParseException;
11-
import org.elasticsearch.action.support.IndicesOptions;
12-
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
13-
import org.elasticsearch.cluster.metadata.ItemUsage;
14-
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
15-
import org.elasticsearch.cluster.metadata.ProjectMetadata;
1611
import org.elasticsearch.common.bytes.BytesArray;
1712
import org.elasticsearch.common.compress.NotXContentException;
18-
import org.elasticsearch.common.settings.Settings;
1913
import org.elasticsearch.common.xcontent.XContentHelper;
2014
import org.elasticsearch.xcontent.NamedXContentRegistry;
2115
import org.elasticsearch.xcontent.XContentParser;
@@ -24,7 +18,6 @@
2418
import org.elasticsearch.xpack.core.template.resources.TemplateResources;
2519

2620
import java.io.IOException;
27-
import java.util.List;
2821
import java.util.Map;
2922

3023
/**
@@ -104,43 +97,4 @@ private static void validate(String source) {
10497
throw new ElasticsearchParseException("invalid policy", e);
10598
}
10699
}
107-
108-
/**
109-
* Given a cluster state and ILM policy, calculate the {@link ItemUsage} of
110-
* the policy (what indices, data streams, and templates use the policy)
111-
*/
112-
public static ItemUsage calculateUsage(
113-
final IndexNameExpressionResolver indexNameExpressionResolver,
114-
final ProjectMetadata project,
115-
final String policyName
116-
) {
117-
final List<String> indices = project.indices()
118-
.values()
119-
.stream()
120-
.filter(indexMetadata -> policyName.equals(indexMetadata.getLifecyclePolicyName()))
121-
.map(indexMetadata -> indexMetadata.getIndex().getName())
122-
.toList();
123-
124-
final List<String> allDataStreams = indexNameExpressionResolver.dataStreamNames(
125-
project,
126-
IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN_NO_SELECTOR
127-
);
128-
129-
final List<String> dataStreams = allDataStreams.stream().filter(dsName -> {
130-
String indexTemplate = MetadataIndexTemplateService.findV2Template(project, dsName, false);
131-
if (indexTemplate != null) {
132-
Settings settings = MetadataIndexTemplateService.resolveSettings(project, indexTemplate);
133-
return policyName.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(settings));
134-
} else {
135-
return false;
136-
}
137-
}).toList();
138-
139-
final List<String> composableTemplates = project.templatesV2().keySet().stream().filter(templateName -> {
140-
Settings settings = MetadataIndexTemplateService.resolveSettings(project, templateName);
141-
return policyName.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(settings));
142-
}).toList();
143-
144-
return new ItemUsage(indices, dataStreams, composableTemplates);
145-
}
146100
}

0 commit comments

Comments
 (0)